Skip to content

fix: alias and namespace import support for import protection#6784

Open
schiller-manuel wants to merge 2 commits intomainfrom
fix-6770
Open

fix: alias and namespace import support for import protection#6784
schiller-manuel wants to merge 2 commits intomainfrom
fix-6770

Conversation

@schiller-manuel
Copy link
Contributor

@schiller-manuel schiller-manuel commented Feb 28, 2026

fixes #6770

Summary by CodeRabbit

  • New Features

    • New example app routes and navigation illustrating client/server secret exposure and alias-path scenarios.
    • Configurable import-protection modes (mock vs error) selectable via environment.
  • Tests

    • Extensive new end-to-end suites validating custom-config, error-mode, violations parsing, and multiple leak/alias scenarios.
    • Build/dev tooling to capture and assert violation outputs.
  • Refactor

    • Import-protection internals reorganized for improved resolution, mocking, and richer violation reporting.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 28, 2026

📝 Walkthrough

Walkthrough

Refactors the import-protection plugin with AST-driven analysis, improved resolution (including extensionless/TS-path handling), self-denial mock generation, expanded caching/invalidation, pluggable import-location logic, and a large e2e test suite exercising custom deny patterns and error/mock behaviors.

Changes

Cohort / File(s) Summary
E2E: custom-config project
e2e/react-start/import-protection-custom-config/.gitignore, e2e/react-start/import-protection-custom-config/package.json, e2e/react-start/import-protection-custom-config/playwright.config.ts, e2e/react-start/import-protection-custom-config/tsconfig.json, e2e/react-start/import-protection-custom-config/vite.config.ts
Add new e2e project and configs for custom import-protection scenarios; Playwright setup supports mock vs error modes.
E2E: app routes & router
e2e/react-start/import-protection-custom-config/src/router.tsx, .../src/routes/__root.tsx, .../src/routes/index.tsx, .../src/routes/backend-leak.tsx, .../src/routes/frontend-leak.tsx, .../src/routeTree.gen.ts
Add app routes, generated route tree, and router factory used by custom-config tests (SSR-aware route typings included).
E2E: tests & helpers
e2e/react-start/import-protection-custom-config/tests/*, e2e/react-start/import-protection-custom-config/tests/utils/isErrorMode.ts
Add globalSetup scripts, violation parsing utils, and comprehensive tests (build/dev & error/mock modes) that capture and assert violations.
Existing E2E: alias-path leak tests
e2e/react-start/import-protection/src/routes/*, e2e/react-start/import-protection/tests/*, e2e/react-start/import-protection/package.json, e2e/react-start/import-protection/tests/violations.setup.ts
Add alias-path routes and expand tests for alias/namespace leak scenarios; integrate tsconfig-paths in Vite for path-mapping coverage and consolidate route definitions.
Plugin: core refactor
packages/start-plugin-core/src/import-protection-plugin/plugin.ts
Major refactor: move public types to a types module, add AST-based analysis/resolution, per-importer export tracking, richer violation reporting and deferral, self-denial transform/mocks, and substantial caching/invalidation logic.
Plugin: types & constants
packages/start-plugin-core/src/import-protection-plugin/types.ts, packages/start-plugin-core/src/import-protection-plugin/constants.ts, packages/start-plugin-core/src/constants.ts
Add centralized type declarations and new constants (including exported SERVER_FN_LOOKUP), debug/env flags, known extensions, and Vite virtual prefix constants.
Plugin: AST & rewrite
packages/start-plugin-core/src/import-protection-plugin/ast.ts, .../rewriteDeniedImports.ts, .../postCompileUsage.ts
Introduce parseImportProtectionAst wrapper, AST-driven export collection, rewriteDeniedImportsFromAst API, and AST-based post-compile usage helper.
Plugin: virtual modules & mocks
packages/start-plugin-core/src/import-protection-plugin/virtualModules.ts
Refactor mock/virtual-module generation to support self-contained mock modules, change makeMockEdgeModuleId signature, add generateSelfContainedMockModule & dev-denial helpers, and adjust loadMockEdgeModule generation.
Plugin: utils & resolver
packages/start-plugin-core/src/import-protection-plugin/utils.ts, .../extensionlessAbsoluteIdResolver.ts, .../sourceLocation.ts
Add candidate-building, deferral, canonicalization utilities; new ExtensionlessAbsoluteIdResolver with cache/invalidation; make import-location lookup pluggable via injected finder.
Plugin: docs & tests
packages/start-plugin-core/src/import-protection-plugin/INTERNALS.md, packages/start-plugin-core/tests/importProtection/*
Update internals doc to reflect self-denial transform flow; expand unit tests for rewrite, utils, virtual modules, and new utilities.
Minor surface & imports
packages/start-plugin-core/src/start-compiler-plugin/plugin.ts
Re-export SERVER_FN_LOOKUP from shared constants for backward compatibility; minor route/nav rendering tweaks in some e2e routes.

Sequence Diagram(s)

sequenceDiagram
    participant Dev as Dev/Build Pipeline
    participant AST as AST Parser
    participant Resolver as Path Resolver
    participant Cache as Cache/State
    participant MockGen as Mock Generator

    Dev->>AST: parse source (parseImportProtectionAst)
    AST->>AST: collect denied imports & named exports
    Dev->>Resolver: request resolution candidates (TS paths / extensionless)
    Resolver->>Cache: check/update resolution cache
    Cache-->>Resolver: cached/resolved variants
    Resolver->>Dev: resolved candidate paths
    Dev->>Dev: match candidates against deny rules
    alt File-based violation
        Dev->>MockGen: generate self-contained mock module
        MockGen->>AST: use named exports to build mock
        MockGen->>Dev: return mock module code
        Dev->>Cache: defer or emit violation (deferred build verification)
    else Specifier/marker violation
        Dev->>Dev: report/delay according to behavior (mock/error) and deferral rules
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • tannerlinsley
  • nlynzaad
  • chorobin

Poem

🐇
I nibbled AST leaves under soft starlight,
found hidden imports and bundled them tight.
I stitched mock carrots from names and trace,
and hid the secrets in their proper place.
Hop—tests green now, I bound them all right!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.97% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: alias and namespace import support for import protection' clearly and specifically describes the main change addressing TS path mapping support.
Linked Issues check ✅ Passed The PR addresses the linked issue #6770 by implementing AST-based import analysis, enhanced resolution logic with multiple resolution candidates, self-denial mechanisms for file-based violations, and comprehensive e2e test coverage for custom import patterns.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing import protection with alias and namespace imports. New e2e test projects validate custom configuration scenarios, updated plugin logic handles TS path mappings, and utility expansions support the new requirements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-6770

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

@nx-cloud
Copy link

nx-cloud bot commented Feb 28, 2026

View your CI Pipeline Execution ↗ for commit 92a9a30

Command Status Duration Result
nx run tanstack-router-e2e-bundle-size:build --... ✅ Succeeded 1m 27s View ↗

☁️ Nx Cloud last updated this comment at 2026-02-28 07:54:23 UTC

@github-actions
Copy link

github-actions bot commented Feb 28, 2026

Bundle Size Benchmarks

  • Commit: de66f0e0a0d6
  • Measured at: 2026-02-28T07:46:15.213Z
  • Baseline source: history:442ada1f6432
  • Dashboard: bundle-size history
Scenario Current (gzip) Delta vs baseline Raw Brotli Trend
react-router.minimal 86.58 KiB 0 B (0.00%) 272.45 KiB 75.22 KiB ▁▂▂▂▂▂▂▂▇▇█
react-router.full 89.61 KiB 0 B (0.00%) 282.78 KiB 77.90 KiB ▂▁▁▁▂▂▂▂▇▇█
solid-router.minimal 35.88 KiB 0 B (0.00%) 107.56 KiB 32.26 KiB ▁▂▂▂▅▅▅▅▇▇█
solid-router.full 40.21 KiB 0 B (0.00%) 120.61 KiB 36.13 KiB ▁▃▃▃▅▅▅▅▇▇█
vue-router.minimal 51.75 KiB 0 B (0.00%) 147.54 KiB 46.50 KiB ▁▂▂▂▅▅▅▅▇▇█
vue-router.full 56.55 KiB 0 B (0.00%) 163.12 KiB 50.86 KiB ▁▂▂▂▅▅▅▅▇▇█
react-start.minimal 99.11 KiB 0 B (0.00%) 311.58 KiB 85.68 KiB ▁▂▂▂▂▂▂▂▆▆█
react-start.full 102.49 KiB 0 B (0.00%) 321.36 KiB 88.62 KiB ▁▂▂▂▂▂▂▂▆▆█
solid-start.minimal 48.19 KiB 0 B (0.00%) 145.13 KiB 42.67 KiB ▁▃▃▃▅▅▅▅▇▇█
solid-start.full 53.67 KiB 0 B (0.00%) 161.05 KiB 47.33 KiB ▁▂▂▂▅▅▅▅▇▇█

Trend sparkline is historical gzip bytes ending with this PR measurement; lower is better.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 28, 2026

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@6784

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@6784

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@6784

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@6784

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@6784

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@6784

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@6784

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@6784

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@6784

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@6784

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@6784

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@6784

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@6784

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@6784

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@6784

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@6784

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@6784

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@6784

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@6784

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@6784

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@6784

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@6784

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@6784

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@6784

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@6784

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@6784

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@6784

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@6784

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@6784

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@6784

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@6784

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@6784

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@6784

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@6784

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@6784

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@6784

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@6784

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@6784

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@6784

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@6784

commit: cd83f48

Copy link
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: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
e2e/react-start/import-protection/vite.config.ts (1)

4-16: ⚠️ Potential issue | 🟡 Minor

Harden tsconfigPaths project resolution against cwd differences.

Using a relative projects path breaks alias resolution when the dev server is launched from a different working directory. Use an absolute path via path.resolve(import.meta.dirname, ...) to ensure consistent resolution regardless of cwd.

Proposed diff
   plugins: [
-    tsconfigPaths({ projects: ['./tsconfig.json'] }),
+    tsconfigPaths({
+      projects: [path.resolve(import.meta.dirname, './tsconfig.json')],
+    }),
     tanstackStart({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-start/import-protection/vite.config.ts` around lines 4 - 16, The
tsconfigPaths plugin uses a relative projects path which can break when the
server is started from a different CWD; update the plugin invocation in
vite.config.ts to resolve the tsconfig.json to an absolute path by
importing/using path.resolve with import.meta.url (or import.meta.dirname) and
pass that absolute path to tsconfigPaths({ projects: [resolvedPath] }) so alias
resolution is stable regardless of working directory.
🧹 Nitpick comments (7)
e2e/react-start/import-protection-custom-config/package.json (1)

4-4: Consider sideEffects: true for this e2e application.

"sideEffects": false is typically for libraries. For an application with routes and components, this could cause unexpected tree-shaking behavior. However, since this is an e2e test project and Vite handles module resolution differently, the practical impact may be minimal.

🔧 Suggested fix
-  "sideEffects": false,
+  "sideEffects": true,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-start/import-protection-custom-config/package.json` at line 4, The
package.json currently sets "sideEffects": false which is intended for libraries
and can cause unexpected tree-shaking for an application; update the
package.json to either set "sideEffects": true or remove the "sideEffects" field
so the e2e app's routes/components are not inadvertently eliminated—locate the
"sideEffects" entry in package.json and change its value to true (or delete the
key) and run the e2e build to verify no runtime component/route stripping
occurs.
e2e/react-start/import-protection-custom-config/vite.config.ts (1)

4-7: Consider validating the behavior value.

The type assertion as 'mock' | 'error' doesn't validate at runtime. If an invalid value is passed, it may cause unexpected behavior.

🔧 Optional: Add runtime validation
-const behavior = (process.env.BEHAVIOR ?? 'mock') as 'mock' | 'error'
+const behaviorEnv = process.env.BEHAVIOR ?? 'mock'
+if (behaviorEnv !== 'mock' && behaviorEnv !== 'error') {
+  throw new Error(`Invalid BEHAVIOR: ${behaviorEnv}. Expected 'mock' or 'error'.`)
+}
+const behavior = behaviorEnv

That said, since this is test infrastructure with controlled env var usage from package.json scripts, the current approach is acceptable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-start/import-protection-custom-config/vite.config.ts` around lines
4 - 7, The code uses a type assertion for the environment variable into 'mock' |
'error' but does no runtime validation; update the logic that sets the behavior
variable (the behavior constant in vite.config.ts) to validate
process.env.BEHAVIOR against the allowed values ('mock' and 'error') at runtime
and fall back to 'mock' (or throw) on invalid input—i.e., read
process.env.BEHAVIOR, check if it === 'mock' or === 'error', and assign that
value or the default 'mock' (or raise a clear error) so you don’t rely solely on
the type assertion.
e2e/react-start/import-protection-custom-config/src/routes/__root.tsx (1)

1-8: Sort imports alphabetically to satisfy ESLint.

ESLint flags that HeadContent should come before Link alphabetically.

♻️ Proposed fix
 import {
   createRootRoute,
+  HeadContent,
   Link,
-  HeadContent,
   linkOptions,
   Outlet,
   Scripts,
 } from '@tanstack/react-router'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-start/import-protection-custom-config/src/routes/__root.tsx` around
lines 1 - 8, The import specifiers from '@tanstack/react-router' are not
alphabetized (HeadContent should come before Link); reorder the named imports in
the import statement so they are sorted alphabetically (e.g., createRootRoute,
HeadContent, Link, linkOptions, Outlet, Scripts) to satisfy ESLint and keep the
existing import path and usage of createRootRoute/HeadContent/Link/etc.
unchanged.
e2e/react-start/import-protection-custom-config/tests/error-mode.setup.ts (1)

9-52: Consider extracting shared utilities.

The waitForHttpOk and killChild functions are duplicated between this file and violations.setup.ts. If the test suite grows, consider extracting these to a shared utility file to reduce maintenance burden.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-start/import-protection-custom-config/tests/error-mode.setup.ts`
around lines 9 - 52, Both waitForHttpOk and killChild are duplicated; extract
them into a shared test utility module and import it from both setup files.
Create a new utility file exporting waitForHttpOk and killChild (preserve their
signatures and behavior, including use of AbortSignal.timeout and spawn
ReturnType), update this file to export types if needed, then replace the
duplicated implementations in error-mode.setup.ts and violations.setup.ts with
imports from the new module (update any relative import paths). Run tests to
ensure no behavior change.
e2e/react-start/import-protection-custom-config/tests/custom-config.spec.ts (1)

112-113: Redundant dynamic imports of fs and path modules.

These modules are already available - path is imported at the top of the file (line 1). Consider reusing the existing import and importing fs at the module level for consistency.

Suggested fix

At the top of the file, add:

 import path from 'node:path'
+import fs from 'node:fs'
 import { expect } from '@playwright/test'

Then in the test:

 test('build: client JS bundle does not contain real backend secret', async () => {
-  const fs = await import('node:fs')
-  const path = await import('node:path')
   const clientDir = path.resolve(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-start/import-protection-custom-config/tests/custom-config.spec.ts`
around lines 112 - 113, Remove the redundant dynamic imports of node:fs and
node:path in the test body and instead import fs at module level and reuse the
existing top-level path import; specifically, delete the two lines "const fs =
await import('node:fs')" and "const path = await import('node:path')" in
custom-config.spec.ts and add a top-level import for fs (so tests referencing fs
and path use the module-level identifiers) to keep imports consistent and avoid
dynamic requires.
packages/start-plugin-core/tests/importProtection/utils.test.ts (1)

141-203: Add a boundary test for srcDirectory prefix collisions.

Please add a case ensuring /app/src2/... is not treated as inside /app/src.

🧪 Suggested test
 describe('shouldDeferResolvedViolation', () => {
@@
   test('does not defer pre-transform or non-src alias', () => {
@@
   })
+
+  test('does not treat sibling prefix path as inside srcDirectory', () => {
+    expect(
+      shouldDeferResolvedViolation({
+        isDevMock: true,
+        isBuild: false,
+        source: 'src2/x',
+        resolvedPath: '/app/src2/x.ts',
+        srcDirectory: '/app/src',
+        isPreTransformResolve: false,
+      }),
+    ).toBe(false)
+  })
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/start-plugin-core/tests/importProtection/utils.test.ts` around lines
141 - 203, Add a boundary test inside the existing
describe('shouldDeferResolvedViolation') block that verifies paths under
'/app/src2' are not treated as inside '/app/src': call
shouldDeferResolvedViolation with isDevMock: true, isBuild: false, source:
'src/x', resolvedPath: '/app/src2/x.ts', srcDirectory: '/app/src',
isPreTransformResolve: false and assert the result is false; this ensures the
prefix '/app/src' match is not satisfied by '/app/src2'.
packages/start-plugin-core/tests/importProtection/rewriteDeniedImports.test.ts (1)

175-201: Add regression tests for destructured and string-keyed named exports.

The new collectNamedExports coverage is good, but these two cases are still uncovered and are high-value for this PR’s export-surface logic.

🧪 Suggested tests
 describe('collectNamedExports', () => {
@@
   test('ignores default and type-only exports', () => {
@@
     expect(collectNamedExports(code)).toEqual([])
   })
+
+  test('collects destructured variable declaration exports', () => {
+    const code = `export const { alpha: renamed, beta } = obj`
+    expect(collectNamedExports(code)).toEqual(['beta', 'renamed'])
+  })
+
+  test('collects string-literal exported names', () => {
+    const code = [`const local = 1`, `export { local as "foo-bar" }`].join('\n')
+    expect(collectNamedExports(code)).toEqual(['foo-bar'])
+  })
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/start-plugin-core/tests/importProtection/rewriteDeniedImports.test.ts`
around lines 175 - 201, Add two regression tests to the collectNamedExports
suite: one that verifies destructured exports are collected (e.g. a snippet
using "export const { a, b: renamed } = obj" and asserting collectNamedExports
returns ['a','renamed']) and one that verifies string-keyed export specifiers
are handled (e.g. a snippet with "const x = 1; export { x as 'str' }" and
asserting collectNamedExports returns ['str']). Place these tests alongside the
existing cases in the same describe('collectNamedExports') block so they
exercise the collectNamedExports function handling of destructured and
string-keyed named exports.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@e2e/react-start/import-protection-custom-config/tests/violations.utils.ts`:
- Around line 59-67: The current parsing uses non-null assertions like
importerLine.split('Importer')[1]! which can throw if the delimiter is missing;
update the parsing for importer/specifier/resolved to safely handle missing
delimiters by checking for the delimiter or split result length before indexing
(e.g. inspect importerLine/includes('Importer') or const parts =
importerLine.split('Importer'); const importer = parts.length > 1 ?
parts[1].trim() : ''), apply the same pattern for specLine/specifier and
resolvedLine/resolved (and preserve the existing replace for specifier when
present) so the code never assumes split()[1] exists.

In `@e2e/react-start/import-protection/package.json`:
- Around line 21-22: Update the dependency declaration for vite-tsconfig-paths
in package.json from "vite-tsconfig-paths": "^5.1.4" to a Vite 7–compatible
v6.1.x release (e.g. "^6.1.0"); then reinstall/update lockfile (npm install /
yarn install) so the lockfile reflects the new version and run the
import-protection e2e tests; locate the dependency entry by the
"vite-tsconfig-paths" key in package.json to make the change.

In `@e2e/react-start/import-protection/tests/import-protection.spec.ts`:
- Around line 50-57: The empty object destructuring in test.beforeEach causes
lint errors; update the parameter list for test.beforeEach to avoid an unused
destructured object — e.g. replace "({}, testInfo)" with "(_, testInfo)"
(underscore for the unused first fixture) or otherwise accept only the testInfo
position permitted by Playwright; update the test.beforeEach callback signature
so testInfo is still used while the unused first fixture is named "_" to satisfy
linters.

In
`@packages/start-plugin-core/src/import-protection-plugin/extensionlessAbsoluteIdResolver.ts`:
- Around line 91-100: The HMR deps built in buildDepsForKey only add
dir:${dirname(key)}, which misses tracking the directory named by an
extensionless absolute ID (e.g. /src/foo.server) and allows stale cache entries;
modify buildDepsForKey to also add deps.add(`dir:${key}`) when isAbsolute(key)
so both the parent directory and the directory represented by the extensionless
absolute ID are indexed (refer to buildDepsForKey, the existing deps Set,
isAbsolute(key) and dirname(key)).

In `@packages/start-plugin-core/src/import-protection-plugin/INTERNALS.md`:
- Around line 175-177: The fenced code block containing the marker string
"\0tanstack-start-import-protection:mock-edge:<BASE64_PAYLOAD>" is missing a
language identifier (triggering markdownlint MD040); update the opening fence to
include a language (for example use "text") so the block reads like a fenced
code block with a language identifier and re-run linting to confirm the MD040
warning is resolved.

In `@packages/start-plugin-core/src/import-protection-plugin/plugin.ts`:
- Around line 13-29: The utils import list violates sort-imports and also pulls
in an unused type (DeferredBuildViolation); reorder the named imports from
'./utils' into alphabetical order (e.g., buildResolutionCandidates,
buildSourceCandidates, canonicalizeResolvedId, clearNormalizeFilePathCache,
debugLog, dedupePatterns, escapeRegExp, extractImportSources, getOrCreate,
isRelativeSpecifier, matchesDebugFilter, normalizeFilePath, relativizePath,
shouldDeferResolveViolation, shouldDeferResolvedViolation, stripQuery) to
satisfy the linter, and remove the unused DeferredBuildViolation import (or
change it to a type-only import "type DeferredBuildViolation" if it will be used
purely as a type elsewhere). Apply the same fix for the other occurrence
mentioned.

In `@packages/start-plugin-core/src/import-protection-plugin/postCompileUsage.ts`:
- Around line 3-4: Reorder the imports in postCompileUsage.ts so the value
import parseImportProtectionAst comes before the type-only import ParsedAst;
specifically swap the two import lines so parseImportProtectionAst is imported
first and then import type { ParsedAst } from './ast' to satisfy the
import/order rule.

In
`@packages/start-plugin-core/src/import-protection-plugin/rewriteDeniedImports.ts`:
- Around line 6-8: Combine and reorder the imports so type and value imports are
grouped consistently: replace the separate imports from './ast' with a single
statement that includes the value and the type (e.g. import {
parseImportProtectionAst, type ParsedAst } from './ast') and keep the type-only
import for SourceMapLike as import type { SourceMapLike } from
'./sourceLocation'; this groups exports by module and keeps type imports
explicit to satisfy import/order.
- Around line 145-173: collectNamedExportsFromAst is under-collecting because it
only adds simple Identifier names and skips destructured variable declarators;
update it to (1) extract identifiers recursively from VariableDeclarator.id when
the id is an ObjectPattern or ArrayPattern (walk patterns and add each
Identifier name via the existing add helper), and (2) ensure export specifiers
handle non-Identifier exported nodes (e.g., StringLiteral) by converting
exported.value to a string before calling add; keep using isValidExportName in
add but feed it every discovered stringified export name. Reference: function
collectNamedExportsFromAst, VariableDeclaration -> decl.declarations,
VariableDeclarator.id, ObjectPattern/ArrayPattern cases, and export specifier
handling where s.exported is not an Identifier.

In `@packages/start-plugin-core/src/import-protection-plugin/utils.ts`:
- Around line 1-2: The import order in utils.ts violates the import/order rule:
move the Node builtin import before third-party imports so that "node:path" (the
extname, isAbsolute, resolve as resolvePath imports) appears before the 'vite'
import (normalizePath); update the top of file to import extname, isAbsolute,
resolvePath from 'node:path' first and then import { normalizePath } from 'vite'
to satisfy linting.
- Line 151: The containment check using resolvedPath.startsWith(srcDirectory) is
boundary-unsafe; instead normalize both paths with path.resolve and use
path.relative(srcDirectory, resolvedPath) to determine containment — return true
only when the relative path does not start with '..' and is not an absolute
path. Update the check that currently references resolvedPath and srcDirectory
to use the normalized path and path.relative approach so paths like /app/src2
are not treated as inside /app/src.

---

Outside diff comments:
In `@e2e/react-start/import-protection/vite.config.ts`:
- Around line 4-16: The tsconfigPaths plugin uses a relative projects path which
can break when the server is started from a different CWD; update the plugin
invocation in vite.config.ts to resolve the tsconfig.json to an absolute path by
importing/using path.resolve with import.meta.url (or import.meta.dirname) and
pass that absolute path to tsconfigPaths({ projects: [resolvedPath] }) so alias
resolution is stable regardless of working directory.

---

Nitpick comments:
In `@e2e/react-start/import-protection-custom-config/package.json`:
- Line 4: The package.json currently sets "sideEffects": false which is intended
for libraries and can cause unexpected tree-shaking for an application; update
the package.json to either set "sideEffects": true or remove the "sideEffects"
field so the e2e app's routes/components are not inadvertently eliminated—locate
the "sideEffects" entry in package.json and change its value to true (or delete
the key) and run the e2e build to verify no runtime component/route stripping
occurs.

In `@e2e/react-start/import-protection-custom-config/src/routes/__root.tsx`:
- Around line 1-8: The import specifiers from '@tanstack/react-router' are not
alphabetized (HeadContent should come before Link); reorder the named imports in
the import statement so they are sorted alphabetically (e.g., createRootRoute,
HeadContent, Link, linkOptions, Outlet, Scripts) to satisfy ESLint and keep the
existing import path and usage of createRootRoute/HeadContent/Link/etc.
unchanged.

In `@e2e/react-start/import-protection-custom-config/tests/custom-config.spec.ts`:
- Around line 112-113: Remove the redundant dynamic imports of node:fs and
node:path in the test body and instead import fs at module level and reuse the
existing top-level path import; specifically, delete the two lines "const fs =
await import('node:fs')" and "const path = await import('node:path')" in
custom-config.spec.ts and add a top-level import for fs (so tests referencing fs
and path use the module-level identifiers) to keep imports consistent and avoid
dynamic requires.

In `@e2e/react-start/import-protection-custom-config/tests/error-mode.setup.ts`:
- Around line 9-52: Both waitForHttpOk and killChild are duplicated; extract
them into a shared test utility module and import it from both setup files.
Create a new utility file exporting waitForHttpOk and killChild (preserve their
signatures and behavior, including use of AbortSignal.timeout and spawn
ReturnType), update this file to export types if needed, then replace the
duplicated implementations in error-mode.setup.ts and violations.setup.ts with
imports from the new module (update any relative import paths). Run tests to
ensure no behavior change.

In `@e2e/react-start/import-protection-custom-config/vite.config.ts`:
- Around line 4-7: The code uses a type assertion for the environment variable
into 'mock' | 'error' but does no runtime validation; update the logic that sets
the behavior variable (the behavior constant in vite.config.ts) to validate
process.env.BEHAVIOR against the allowed values ('mock' and 'error') at runtime
and fall back to 'mock' (or throw) on invalid input—i.e., read
process.env.BEHAVIOR, check if it === 'mock' or === 'error', and assign that
value or the default 'mock' (or raise a clear error) so you don’t rely solely on
the type assertion.

In
`@packages/start-plugin-core/tests/importProtection/rewriteDeniedImports.test.ts`:
- Around line 175-201: Add two regression tests to the collectNamedExports
suite: one that verifies destructured exports are collected (e.g. a snippet
using "export const { a, b: renamed } = obj" and asserting collectNamedExports
returns ['a','renamed']) and one that verifies string-keyed export specifiers
are handled (e.g. a snippet with "const x = 1; export { x as 'str' }" and
asserting collectNamedExports returns ['str']). Place these tests alongside the
existing cases in the same describe('collectNamedExports') block so they
exercise the collectNamedExports function handling of destructured and
string-keyed named exports.

In `@packages/start-plugin-core/tests/importProtection/utils.test.ts`:
- Around line 141-203: Add a boundary test inside the existing
describe('shouldDeferResolvedViolation') block that verifies paths under
'/app/src2' are not treated as inside '/app/src': call
shouldDeferResolvedViolation with isDevMock: true, isBuild: false, source:
'src/x', resolvedPath: '/app/src2/x.ts', srcDirectory: '/app/src',
isPreTransformResolve: false and assert the result is false; this ensures the
prefix '/app/src' match is not satisfied by '/app/src2'.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between de66f0e and 32101ec.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (41)
  • e2e/react-start/import-protection-custom-config/.gitignore
  • e2e/react-start/import-protection-custom-config/package.json
  • e2e/react-start/import-protection-custom-config/playwright.config.ts
  • e2e/react-start/import-protection-custom-config/src/routeTree.gen.ts
  • e2e/react-start/import-protection-custom-config/src/router.tsx
  • e2e/react-start/import-protection-custom-config/src/routes/__root.tsx
  • e2e/react-start/import-protection-custom-config/src/routes/backend-leak.tsx
  • e2e/react-start/import-protection-custom-config/src/routes/frontend-leak.tsx
  • e2e/react-start/import-protection-custom-config/src/routes/index.tsx
  • e2e/react-start/import-protection-custom-config/tests/custom-config.spec.ts
  • e2e/react-start/import-protection-custom-config/tests/error-mode.setup.ts
  • e2e/react-start/import-protection-custom-config/tests/error-mode.spec.ts
  • e2e/react-start/import-protection-custom-config/tests/utils/isErrorMode.ts
  • e2e/react-start/import-protection-custom-config/tests/violations.setup.ts
  • e2e/react-start/import-protection-custom-config/tests/violations.utils.ts
  • e2e/react-start/import-protection-custom-config/tsconfig.json
  • e2e/react-start/import-protection-custom-config/vite.config.ts
  • e2e/react-start/import-protection/package.json
  • e2e/react-start/import-protection/src/routes/__root.tsx
  • e2e/react-start/import-protection/src/routes/alias-path-leak.tsx
  • e2e/react-start/import-protection/src/routes/alias-path-namespace-leak.tsx
  • e2e/react-start/import-protection/src/routes/non-alias-namespace-leak.tsx
  • e2e/react-start/import-protection/tests/import-protection.spec.ts
  • e2e/react-start/import-protection/tests/violations.setup.ts
  • e2e/react-start/import-protection/vite.config.ts
  • packages/start-plugin-core/src/constants.ts
  • packages/start-plugin-core/src/import-protection-plugin/INTERNALS.md
  • packages/start-plugin-core/src/import-protection-plugin/ast.ts
  • packages/start-plugin-core/src/import-protection-plugin/constants.ts
  • packages/start-plugin-core/src/import-protection-plugin/extensionlessAbsoluteIdResolver.ts
  • packages/start-plugin-core/src/import-protection-plugin/plugin.ts
  • packages/start-plugin-core/src/import-protection-plugin/postCompileUsage.ts
  • packages/start-plugin-core/src/import-protection-plugin/rewriteDeniedImports.ts
  • packages/start-plugin-core/src/import-protection-plugin/sourceLocation.ts
  • packages/start-plugin-core/src/import-protection-plugin/types.ts
  • packages/start-plugin-core/src/import-protection-plugin/utils.ts
  • packages/start-plugin-core/src/import-protection-plugin/virtualModules.ts
  • packages/start-plugin-core/src/start-compiler-plugin/plugin.ts
  • packages/start-plugin-core/tests/importProtection/rewriteDeniedImports.test.ts
  • packages/start-plugin-core/tests/importProtection/utils.test.ts
  • packages/start-plugin-core/tests/importProtection/virtualModules.test.ts

Comment on lines +59 to +67
const importer = importerLine
? importerLine.split('Importer:')[1]!.trim()
: ''
const specifier = specLine
? specLine.split('Import:')[1]!.trim().replace(/^"|"$/g, '')
: ''
const resolved = resolvedLine
? resolvedLine.split('Resolved:')[1]!.trim()
: undefined
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Non-null assertions after split() may throw if delimiter is missing.

If the log format changes or a line doesn't contain the expected delimiter, split()[1] returns undefined, and the ! assertion will cause a runtime error. Consider adding fallback handling.

🛡️ Proposed safer handling
-    const importer = importerLine
-      ? importerLine.split('Importer:')[1]!.trim()
-      : ''
-    const specifier = specLine
-      ? specLine.split('Import:')[1]!.trim().replace(/^"|"$/g, '')
-      : ''
-    const resolved = resolvedLine
-      ? resolvedLine.split('Resolved:')[1]!.trim()
-      : undefined
+    const importer = importerLine?.split('Importer:')[1]?.trim() ?? ''
+    const specifier =
+      specLine?.split('Import:')[1]?.trim().replace(/^"|"$/g, '') ?? ''
+    const resolved = resolvedLine?.split('Resolved:')[1]?.trim()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-start/import-protection-custom-config/tests/violations.utils.ts`
around lines 59 - 67, The current parsing uses non-null assertions like
importerLine.split('Importer')[1]! which can throw if the delimiter is missing;
update the parsing for importer/specifier/resolved to safely handle missing
delimiters by checking for the delimiter or split result length before indexing
(e.g. inspect importerLine/includes('Importer') or const parts =
importerLine.split('Importer'); const importer = parts.length > 1 ?
parts[1].trim() : ''), apply the same pattern for specLine/specifier and
resolvedLine/resolved (and preserve the existing replace for specifier when
present) so the code never assumes split()[1] exists.

Comment on lines +50 to +57
test.beforeEach(({}, testInfo) => {
const baseURL = testInfo.project.use.baseURL
if (!baseURL) {
throw new Error(
'Missing Playwright baseURL for import-protection e2e. Run with `pnpm exec playwright test -c e2e/react-start/import-protection/playwright.config.ts` (or configure `use.baseURL`).',
)
}
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix empty object pattern flagged by static analysis.

The empty object destructuring ({}, testInfo) triggers both Biome and ESLint errors. Since you're not using fixture parameters, you can remove the empty destructuring.

🐛 Proposed fix
-test.beforeEach(({}, testInfo) => {
+test.beforeEach(({ }, testInfo) => {

Actually, since Playwright's beforeEach provides fixtures as the first argument, and you're only using testInfo, consider using the underscore convention or accessing testInfo differently:

-test.beforeEach(({}, testInfo) => {
+test.beforeEach((_, testInfo) => {
   const baseURL = testInfo.project.use.baseURL

Or alternatively, if Playwright allows:

-test.beforeEach(({}, testInfo) => {
+test.beforeEach(({ baseURL }, testInfo) => {
+  if (!baseURL) {
-  const baseURL = testInfo.project.use.baseURL
-  if (!baseURL) {
📝 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
test.beforeEach(({}, testInfo) => {
const baseURL = testInfo.project.use.baseURL
if (!baseURL) {
throw new Error(
'Missing Playwright baseURL for import-protection e2e. Run with `pnpm exec playwright test -c e2e/react-start/import-protection/playwright.config.ts` (or configure `use.baseURL`).',
)
}
})
test.beforeEach((_, testInfo) => {
const baseURL = testInfo.project.use.baseURL
if (!baseURL) {
throw new Error(
'Missing Playwright baseURL for import-protection e2e. Run with `pnpm exec playwright test -c e2e/react-start/import-protection/playwright.config.ts` (or configure `use.baseURL`).',
)
}
})
🧰 Tools
🪛 Biome (2.4.4)

[error] 50-50: Unexpected empty object pattern.

(lint/correctness/noEmptyPattern)

🪛 ESLint

[error] 50-50: Unexpected empty object pattern.

(no-empty-pattern)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-start/import-protection/tests/import-protection.spec.ts` around
lines 50 - 57, The empty object destructuring in test.beforeEach causes lint
errors; update the parameter list for test.beforeEach to avoid an unused
destructured object — e.g. replace "({}, testInfo)" with "(_, testInfo)"
(underscore for the unused first fixture) or otherwise accept only the testInfo
position permitted by Playwright; update the test.beforeEach callback signature
so testInfo is still used while the unused first fixture is named "_" to satisfy
linters.

Comment on lines +91 to +100
private buildDepsForKey(key: string, resolvedFile: string | undefined) {
const deps = new Set<DepKey>()
deps.add(`file:${key}`)

if (isAbsolute(key)) {
deps.add(`dir:${dirname(key)}`)
}
if (resolvedFile) {
deps.add(`file:${resolvedFile}`)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Track dir:${key} for extensionless absolute IDs to prevent stale HMR cache

Line 96 only records dir:${dirname(key)}. For IDs like /src/foo.server, resolution can depend on /src/foo.server/index.*. Changes under that directory invalidate dir:/src/foo.server, but this key dependency is never indexed, so stale entries can persist.

💡 Suggested fix
-  private buildDepsForKey(key: string, resolvedFile: string | undefined) {
+  private buildDepsForKey(
+    key: string,
+    resolvedFile: string | undefined,
+  ): Set<DepKey> {
     const deps = new Set<DepKey>()
     deps.add(`file:${key}`)
 
     if (isAbsolute(key)) {
       deps.add(`dir:${dirname(key)}`)
+      // `./<basename(key)>` may resolve via `<key>/index.*`
+      if (!FILE_RESOLUTION_EXTENSIONS.includes(extname(key))) {
+        deps.add(`dir:${key}`)
+      }
     }
     if (resolvedFile) {
       deps.add(`file:${resolvedFile}`)
     }
📝 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
private buildDepsForKey(key: string, resolvedFile: string | undefined) {
const deps = new Set<DepKey>()
deps.add(`file:${key}`)
if (isAbsolute(key)) {
deps.add(`dir:${dirname(key)}`)
}
if (resolvedFile) {
deps.add(`file:${resolvedFile}`)
}
private buildDepsForKey(
key: string,
resolvedFile: string | undefined,
): Set<DepKey> {
const deps = new Set<DepKey>()
deps.add(`file:${key}`)
if (isAbsolute(key)) {
deps.add(`dir:${dirname(key)}`)
// `./<basename(key)>` may resolve via `<key>/index.*`
if (!FILE_RESOLUTION_EXTENSIONS.includes(extname(key))) {
deps.add(`dir:${key}`)
}
}
if (resolvedFile) {
deps.add(`file:${resolvedFile}`)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/start-plugin-core/src/import-protection-plugin/extensionlessAbsoluteIdResolver.ts`
around lines 91 - 100, The HMR deps built in buildDepsForKey only add
dir:${dirname(key)}, which misses tracking the directory named by an
extensionless absolute ID (e.g. /src/foo.server) and allows stale cache entries;
modify buildDepsForKey to also add deps.add(`dir:${key}`) when isAbsolute(key)
so both the parent directory and the directory represented by the extensionless
absolute ID are indexed (refer to buildDepsForKey, the existing deps Set,
isAbsolute(key) and dirname(key)).

Comment on lines +175 to +177
```
\0tanstack-start-import-protection:mock-edge:<BASE64_PAYLOAD>
```
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add a language identifier to the fenced code block.

Line 175 opens a fenced block without a language, which triggers markdownlint MD040.

🛠️ Suggested fix
-```
+```text
\0tanstack-start-import-protection:mock-edge:<BASE64_PAYLOAD>

</details>

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.21.0)</summary>

[warning] 175-175: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @packages/start-plugin-core/src/import-protection-plugin/INTERNALS.md around
lines 175 - 177, The fenced code block containing the marker string
"\0tanstack-start-import-protection:mock-edge:<BASE64_PAYLOAD>" is missing a
language identifier (triggering markdownlint MD040); update the opening fence to
include a language (for example use "text") so the block reads like a fenced
code block with a language identifier and re-run linting to confirm the MD040
warning is resolved.


</details>

<!-- fingerprinting:phantom:medusa:hawk -->

<!-- This is an auto-generated comment by CodeRabbit -->

Comment on lines 6 to 8
import type { SourceMapLike } from './sourceLocation'
import { parseImportProtectionAst } from './ast'
import type { ParsedAst } from './ast'
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix import ordering to satisfy import/order.

The current value/type import order around ./ast and ./sourceLocation triggers lint failure.

🧰 Tools
🪛 ESLint

[error] 7-7: ./ast import should occur before type import of ./sourceLocation

(import/order)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/start-plugin-core/src/import-protection-plugin/rewriteDeniedImports.ts`
around lines 6 - 8, Combine and reorder the imports so type and value imports
are grouped consistently: replace the separate imports from './ast' with a
single statement that includes the value and the type (e.g. import {
parseImportProtectionAst, type ParsedAst } from './ast') and keep the type-only
import for SourceMapLike as import type { SourceMapLike } from
'./sourceLocation'; this groups exports by module and keeps type imports
explicit to satisfy import/order.

Copy link
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: 3

♻️ Duplicate comments (4)
packages/start-plugin-core/src/import-protection-plugin/extensionlessAbsoluteIdResolver.ts (1)

91-100: ⚠️ Potential issue | 🟠 Major

Track dir:${key} for extensionless absolute IDs to prevent stale HMR cache entries.

Line 96 only indexes dir:${dirname(key)}. For a key like /src/foo.server, changes under /src/foo.server/index.* invalidate dir:/src/foo.server, which is not tracked here, so stale entries can persist.

💡 Suggested fix
-  private buildDepsForKey(key: string, resolvedFile: string | undefined) {
+  private buildDepsForKey(
+    key: string,
+    resolvedFile: string | undefined,
+  ): Set<DepKey> {
     const deps = new Set<DepKey>()
     deps.add(`file:${key}`)
 
     if (isAbsolute(key)) {
       deps.add(`dir:${dirname(key)}`)
+      // Extensionless absolute IDs may resolve via <key>/index.*
+      if (!FILE_RESOLUTION_EXTENSIONS.includes(extname(key))) {
+        deps.add(`dir:${key}`)
+      }
     }
     if (resolvedFile) {
       deps.add(`file:${resolvedFile}`)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/start-plugin-core/src/import-protection-plugin/extensionlessAbsoluteIdResolver.ts`
around lines 91 - 100, In buildDepsForKey, when handling absolute keys (function
buildDepsForKey) also add a dependency for the directory named by the key itself
for extensionless absolute IDs: inside the isAbsolute(key) branch, if
path.extname(key) === '' (i.e. key has no extension) call deps.add(`dir:${key}`)
in addition to deps.add(`dir:${dirname(key)}`) so changes to
/src/foo.server/index.* invalidate dir:/src/foo.server; add path.extname import
if missing.
e2e/react-start/import-protection/tests/import-protection.spec.ts (1)

50-57: ⚠️ Potential issue | 🟡 Minor

Replace empty object destructuring in beforeEach.

Line 50 still uses an empty object pattern (({}, testInfo)), which triggers both Biome and ESLint no-empty-pattern.

🐛 Proposed fix
-test.beforeEach(({}, testInfo) => {
+test.beforeEach((_, testInfo) => {
   const baseURL = testInfo.project.use.baseURL
   if (!baseURL) {
     throw new Error(
       'Missing Playwright baseURL for import-protection e2e. Run with `pnpm exec playwright test -c e2e/react-start/import-protection/playwright.config.ts` (or configure `use.baseURL`).',
     )
   }
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-start/import-protection/tests/import-protection.spec.ts` around
lines 50 - 57, The beforeEach callback is using an empty object pattern "({},
testInfo)" which triggers no-empty-pattern; change the parameter list to name
the unused first arg instead (e.g., "_fixtures" or simply "_" as the first
parameter) so it becomes "( _ , testInfo )" or "( _fixtures, testInfo )", and
leave the body unchanged (still read baseURL from testInfo). Update the
test.beforeEach declaration accordingly to reference testInfo as before.
packages/start-plugin-core/src/import-protection-plugin/rewriteDeniedImports.ts (1)

170-174: ⚠️ Potential issue | 🟠 Major

collectNamedExportsFromAst still drops valid string-keyed exports.

Line 173 gates on isValidExportName, so names like "foo-bar" are discarded even though the mock generators already support string-keyed re-exports. That can produce self-denial modules missing real exports.

Suggested fix
 export function collectNamedExportsFromAst(ast: ParsedAst): Array<string> {
   const names = new Set<string>()
   const add = (name: string) => {
-    if (isValidExportName(name)) names.add(name)
+    if (name.length > 0 && name !== 'default') names.add(name)
   }
#!/bin/bash
# Verify collection-vs-emission mismatch for non-identifier export names.

rg -n "collectNamedExportsFromAst|isValidExportName|generateExportLines|filterExportNames" \
  packages/start-plugin-core/src/import-protection-plugin/rewriteDeniedImports.ts \
  packages/start-plugin-core/src/import-protection-plugin/virtualModules.ts

# Check whether tests currently cover string-keyed exports through collectNamedExportsFromAst.
rg -n 'collectNamedExportsFromAst|string-keyed|export \{.*as ".*"\}' \
  packages/start-plugin-core/tests/importProtection || true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/start-plugin-core/src/import-protection-plugin/rewriteDeniedImports.ts`
around lines 170 - 174, collectNamedExportsFromAst currently filters export
names with isValidExportName which drops valid string-keyed exports (e.g.,
"foo-bar"); remove or bypass that validation inside collectNamedExportsFromAst
so it collects raw export keys (including quoted/string keys) and let downstream
logic (generateExportLines or filterExportNames) decide how to emit or sanitize
identifiers; update collectNamedExportsFromAst to add every export name
encountered to the Set and ensure generateExportLines/filterExportNames handle
any necessary quoting or renaming.
packages/start-plugin-core/src/import-protection-plugin/INTERNALS.md (1)

192-194: ⚠️ Potential issue | 🟡 Minor

Add a language identifier to this fenced block.

Line 192 still opens a fence without a language, which trips markdownlint MD040.

Suggested fix
-```
+```text
\0tanstack-start-import-protection:mock-edge:<BASE64_PAYLOAD>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @packages/start-plugin-core/src/import-protection-plugin/INTERNALS.md around
lines 192 - 194, The fenced code block containing the literal
"\0tanstack-start-import-protection:mock-edge:<BASE64_PAYLOAD>" lacks a language
identifier (tripping markdownlint MD040); fix it by changing the opening fence
from totext so the block becomes a fenced "text" code block (i.e., add
the language identifier text immediately after the opening backticks).


</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (6)</summary><blockquote>

<details>
<summary>e2e/react-start/import-protection/src/routes/alias-path-namespace-leak.tsx (1)</summary><blockquote>

`2-2`: **Consider documenting this as an intentional violation fixture.**

Please add a short inline comment at Line 2 that this server-only import is deliberate for import-protection e2e coverage, to prevent accidental “cleanup” later.


Based on learnings, in TanStack/router e2e tests intentionally non-production patterns should stay test-only and be clearly documented as test-only.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-start/import-protection/src/routes/alias-path-namespace-leak.tsx`
at line 2, Add a short inline comment on the import line that makes clear the
server-only import of secretModule (import * as secretModule from
'~/violations/secret.server') is an intentional test-only violation for
import-protection e2e coverage; update the line to include a brief note
referencing that this pattern is deliberate for alias/namespace leak testing so
it isn't accidentally removed or "cleaned up" later.
```

</details>

</blockquote></details>
<details>
<summary>e2e/react-start/import-protection-custom-config/tests/error-mode.setup.ts (3)</summary><blockquote>

`73-77`: **Use type narrowing instead of `any` for error handling.**

Using `err: any` bypasses TypeScript's type checking. Consider narrowing the error type for safer property access.

<details>
<summary>♻️ Proposed type-safe error handling</summary>

```diff
-  } catch (err: any) {
-    exitCode = err.status ?? 1
-    stdout = err.stdout?.toString() ?? ''
-    stderr = err.stderr?.toString() ?? ''
+  } catch (err: unknown) {
+    if (err && typeof err === 'object') {
+      exitCode = 'status' in err && typeof err.status === 'number' ? err.status : 1
+      stdout = 'stdout' in err && err.stdout ? String(err.stdout) : ''
+      stderr = 'stderr' in err && err.stderr ? String(err.stderr) : ''
+    } else {
+      exitCode = 1
+    }
   }
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-start/import-protection-custom-config/tests/error-mode.setup.ts`
around lines 73 - 77, The catch currently types the caught error as `any`;
change it to `unknown` and narrow it before accessing `status`, `stdout`, and
`stderr`: in the catch block for the code that sets `exitCode`, `stdout`, and
`stderr`, declare the parameter as `err: unknown` and then use type guards (e.g.
`instanceof Error` and/or checking for specific properties like `status`,
`stdout`, `stderr`) or narrow to the NodeJS ExecException-like shape before
reading those properties, falling back to safe defaults if checks fail.
```

</details>

---

`9-52`: **Consider extracting shared utilities.**

The `waitForHttpOk` and `killChild` functions are duplicated across multiple setup files (`violations.setup.ts` in both projects, and `error-mode.setup.ts`). Consider extracting these to a shared utility file to reduce maintenance burden.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-start/import-protection-custom-config/tests/error-mode.setup.ts`
around lines 9 - 52, Extract the duplicated utilities waitForHttpOk and
killChild into a shared test-utilities module and update callers to import them
instead of duplicating; create a new module exporting async function
waitForHttpOk(url: string, timeoutMs: number) and async function
killChild(child: ReturnType<typeof spawn>): Promise<void> with the same logic,
then replace the local definitions in error-mode.setup.ts (and the other setup
files like violations.setup.ts) with imports from the new module and run tests
to ensure nothing breaks.
```

</details>

---

`133-141`: **Consider capturing actual dev server exit code.**

The result always writes `exitCode: 0` regardless of the actual child process outcome. If the error-mode tests need to verify server behavior, capturing the real exit code could be useful.

<details>
<summary>♻️ Proposed fix to capture actual exit code</summary>

```diff
+  let actualExitCode = 0
+  child.once('exit', (code) => {
+    actualExitCode = code ?? 0
+  })
+
   child.stdout?.on('data', (d: Buffer) => logChunks.push(d.toString()))
   // ... rest of function ...
   
   const combined = logChunks.join('')
   fs.writeFileSync(
     path.resolve(cwd, 'error-dev-result.json'),
     JSON.stringify(
-      { exitCode: 0, stdout: combined, stderr: '', combined },
+      { exitCode: actualExitCode, stdout: combined, stderr: '', combined },
       null,
       2,
     ),
   )
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-start/import-protection-custom-config/tests/error-mode.setup.ts`
around lines 133 - 141, The file currently writes exitCode: 0 into the result
JSON; change this to record the real dev server exit code by using the actual
variable that tracks the child process outcome (e.g., use the exit code captured
from the spawned process's 'exit'/'close' handler, commonly named exitCode or
serverExitCode) instead of 0; ensure the handler sets that variable before
writing the file (refer to logChunks, combined, cwd, and the
error-dev-result.json write site) so the JSON contains the real exitCode value.
```

</details>

</blockquote></details>
<details>
<summary>e2e/react-start/import-protection-custom-config/tests/violations.utils.ts (1)</summary><blockquote>

`8-11`: **`CodeSnippet` type differs from the source definition.**

The local `CodeSnippet` type is missing the `highlightLine` property and makes `location` optional, whereas the source definition in `packages/start-plugin-core/src/import-protection-plugin/sourceLocation.ts` (lines 385-392) has `highlightLine: number` as required and `location: string` as required. This inconsistency could cause confusion or type mismatches if the types are expected to align.

Consider either importing the type from the source package or aligning the properties if this is intentionally a subset for test purposes.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-start/import-protection-custom-config/tests/violations.utils.ts`
around lines 8 - 11, The local CodeSnippet type in violations.utils.ts is
inconsistent with the canonical definition (missing required
highlightLine:number and marking location optional); either import the
CodeSnippet type from
packages/start-plugin-core/src/import-protection-plugin/sourceLocation.ts or
update the local declaration to include highlightLine: number and make location:
string required so the shapes match (adjust any tests that relied on optional
location accordingly).
```

</details>

</blockquote></details>
<details>
<summary>e2e/react-start/import-protection-custom-config/tests/violations.setup.ts (1)</summary><blockquote>

`148-157`: **This setup only captures cold violations.**

Unlike `e2e/react-start/import-protection/tests/violations.setup.ts` which captures both cold and warm dev violations, this file only does a single cold pass. This may be intentional for the custom-config tests, but worth verifying if warm cache behavior should also be tested here.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-start/import-protection-custom-config/tests/violations.setup.ts`
around lines 148 - 157, The current captureDevViolations function only performs
a single cold run via runDevPass and writes that result; update
captureDevViolations to also perform a warm/dev cached run (either by invoking
runDevPass a second time after the initial pass or by calling the existing
warm-run helper if present) and include both results when writing the output
(e.g. an object with cold and warm keys or separate files). Locate
captureDevViolations and runDevPass in this file, perform the second invocation
using the same cwd and port, and persist both outputs so warm-cache behavior is
tested alongside cold behavior.
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Inline comments:
In @e2e/react-start/import-protection-custom-config/src/routes/__root.tsx:

  • Around line 1-8: The named imports from '@tanstack/react-router' in the import
    statement (createRootRoute, HeadContent, Link, linkOptions, Outlet, Scripts) are
    not in the order required by ESLint sort-imports; reorder the members to match
    the project's alphabetical/sort-imports policy (e.g., alphabetically:
    HeadContent, Link, Scripts, createRootRoute, linkOptions, Outlet or whatever
    ordering your config enforces) so the import statement for these symbols is
    lint-compliant, updating the import clause that references createRootRoute,
    HeadContent, Link, linkOptions, Outlet, Scripts.

In @e2e/react-start/import-protection/tests/import-protection.spec.ts:

  • Around line 551-552: The key-generation currently strips everything after the
    first colon with v.importer.replace(/:./, ''), which collapses Windows drive
    letters (e.g., C:) and causes collisions; update the logic that builds the
    dedupe key to only remove URL/file scheme prefixes (e.g., strip "file:" or
    "file://" or other protocol:// prefixes) while preserving Windows drive
    letters—use a targeted replace like removing /^file:/// or a conditional that,
    if v.importer startsWith('file:') or contains '://', strips the scheme,
    otherwise leaves v.importer intact; reference the use site where the key is
    composed alongside normalizeSpec and v.importer and replace the global /:.
    /
    replace with the safer scheme-only handling.

In @packages/start-plugin-core/src/import-protection-plugin/plugin.ts:

  • Around line 1842-1863: The current check treats importerSurvived as sufficient
    for reporting a violation, which can yield false positives when the denied edge
    was fully eliminated; change the logic in the block around
    mockSurvived/importerSurvived so that importerSurvived alone does not trigger a
    report for file/specifier cases — after computing importerVariantIds (from
    info.importer and
    env.transformResultKeysByFile.get(normalizeFilePath(info.importer))) and
    toModuleIdCandidates(checkId), additionally verify that at least one surviving
    candidate for checkId is actually referenced by the importer's transform keys
    (i.e., an import edge still exists between any importerVariantIds and any
    candidate in toModuleIdCandidates(checkId) within survivingModules or the
    transform results) before treating importerSurvived as sufficient; keep the
    existing mockSurvived check as-is and only report when a real surviving import
    edge is confirmed.

Duplicate comments:
In @e2e/react-start/import-protection/tests/import-protection.spec.ts:

  • Around line 50-57: The beforeEach callback is using an empty object pattern
    "({}, testInfo)" which triggers no-empty-pattern; change the parameter list to
    name the unused first arg instead (e.g., "fixtures" or simply "" as the first
    parameter) so it becomes "( _ , testInfo )" or "( _fixtures, testInfo )", and
    leave the body unchanged (still read baseURL from testInfo). Update the
    test.beforeEach declaration accordingly to reference testInfo as before.

In
@packages/start-plugin-core/src/import-protection-plugin/extensionlessAbsoluteIdResolver.ts:

  • Around line 91-100: In buildDepsForKey, when handling absolute keys (function
    buildDepsForKey) also add a dependency for the directory named by the key itself
    for extensionless absolute IDs: inside the isAbsolute(key) branch, if
    path.extname(key) === '' (i.e. key has no extension) call deps.add(dir:${key})
    in addition to deps.add(dir:${dirname(key)}) so changes to
    /src/foo.server/index.* invalidate dir:/src/foo.server; add path.extname import
    if missing.

In @packages/start-plugin-core/src/import-protection-plugin/INTERNALS.md:

  • Around line 192-194: The fenced code block containing the literal
    "\0tanstack-start-import-protection:mock-edge:<BASE64_PAYLOAD>" lacks a language
    identifier (tripping markdownlint MD040); fix it by changing the opening fence
    from totext so the block becomes a fenced "text" code block (i.e., add
    the language identifier text immediately after the opening backticks).

In
@packages/start-plugin-core/src/import-protection-plugin/rewriteDeniedImports.ts:

  • Around line 170-174: collectNamedExportsFromAst currently filters export names
    with isValidExportName which drops valid string-keyed exports (e.g., "foo-bar");
    remove or bypass that validation inside collectNamedExportsFromAst so it
    collects raw export keys (including quoted/string keys) and let downstream logic
    (generateExportLines or filterExportNames) decide how to emit or sanitize
    identifiers; update collectNamedExportsFromAst to add every export name
    encountered to the Set and ensure generateExportLines/filterExportNames handle
    any necessary quoting or renaming.

Nitpick comments:
In @e2e/react-start/import-protection-custom-config/tests/error-mode.setup.ts:

  • Around line 73-77: The catch currently types the caught error as any; change
    it to unknown and narrow it before accessing status, stdout, and stderr:
    in the catch block for the code that sets exitCode, stdout, and stderr,
    declare the parameter as err: unknown and then use type guards (e.g.
    instanceof Error and/or checking for specific properties like status,
    stdout, stderr) or narrow to the NodeJS ExecException-like shape before
    reading those properties, falling back to safe defaults if checks fail.
  • Around line 9-52: Extract the duplicated utilities waitForHttpOk and killChild
    into a shared test-utilities module and update callers to import them instead of
    duplicating; create a new module exporting async function waitForHttpOk(url:
    string, timeoutMs: number) and async function killChild(child: ReturnType): Promise with the same logic, then replace the local definitions
    in error-mode.setup.ts (and the other setup files like violations.setup.ts) with
    imports from the new module and run tests to ensure nothing breaks.
  • Around line 133-141: The file currently writes exitCode: 0 into the result
    JSON; change this to record the real dev server exit code by using the actual
    variable that tracks the child process outcome (e.g., use the exit code captured
    from the spawned process's 'exit'/'close' handler, commonly named exitCode or
    serverExitCode) instead of 0; ensure the handler sets that variable before
    writing the file (refer to logChunks, combined, cwd, and the
    error-dev-result.json write site) so the JSON contains the real exitCode value.

In @e2e/react-start/import-protection-custom-config/tests/violations.setup.ts:

  • Around line 148-157: The current captureDevViolations function only performs a
    single cold run via runDevPass and writes that result; update
    captureDevViolations to also perform a warm/dev cached run (either by invoking
    runDevPass a second time after the initial pass or by calling the existing
    warm-run helper if present) and include both results when writing the output
    (e.g. an object with cold and warm keys or separate files). Locate
    captureDevViolations and runDevPass in this file, perform the second invocation
    using the same cwd and port, and persist both outputs so warm-cache behavior is
    tested alongside cold behavior.

In @e2e/react-start/import-protection-custom-config/tests/violations.utils.ts:

  • Around line 8-11: The local CodeSnippet type in violations.utils.ts is
    inconsistent with the canonical definition (missing required
    highlightLine:number and marking location optional); either import the
    CodeSnippet type from
    packages/start-plugin-core/src/import-protection-plugin/sourceLocation.ts or
    update the local declaration to include highlightLine: number and make location:
    string required so the shapes match (adjust any tests that relied on optional
    location accordingly).

In @e2e/react-start/import-protection/src/routes/alias-path-namespace-leak.tsx:

  • Line 2: Add a short inline comment on the import line that makes clear the
    server-only import of secretModule (import * as secretModule from
    '~/violations/secret.server') is an intentional test-only violation for
    import-protection e2e coverage; update the line to include a brief note
    referencing that this pattern is deliberate for alias/namespace leak testing so
    it isn't accidentally removed or "cleaned up" later.

</details>

---

<details>
<summary>ℹ️ Review info</summary>

**Configuration used**: defaults

**Review profile**: CHILL

**Plan**: Pro

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 32101ec71e9d3f26621290d13aff97d3b265906d and 0761ff6d6011581f655950a86e1ee24bebf507a3.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (41)</summary>

* `e2e/react-start/import-protection-custom-config/.gitignore`
* `e2e/react-start/import-protection-custom-config/package.json`
* `e2e/react-start/import-protection-custom-config/playwright.config.ts`
* `e2e/react-start/import-protection-custom-config/src/routeTree.gen.ts`
* `e2e/react-start/import-protection-custom-config/src/router.tsx`
* `e2e/react-start/import-protection-custom-config/src/routes/__root.tsx`
* `e2e/react-start/import-protection-custom-config/src/routes/backend-leak.tsx`
* `e2e/react-start/import-protection-custom-config/src/routes/frontend-leak.tsx`
* `e2e/react-start/import-protection-custom-config/src/routes/index.tsx`
* `e2e/react-start/import-protection-custom-config/tests/custom-config.spec.ts`
* `e2e/react-start/import-protection-custom-config/tests/error-mode.setup.ts`
* `e2e/react-start/import-protection-custom-config/tests/error-mode.spec.ts`
* `e2e/react-start/import-protection-custom-config/tests/utils/isErrorMode.ts`
* `e2e/react-start/import-protection-custom-config/tests/violations.setup.ts`
* `e2e/react-start/import-protection-custom-config/tests/violations.utils.ts`
* `e2e/react-start/import-protection-custom-config/tsconfig.json`
* `e2e/react-start/import-protection-custom-config/vite.config.ts`
* `e2e/react-start/import-protection/package.json`
* `e2e/react-start/import-protection/src/routes/__root.tsx`
* `e2e/react-start/import-protection/src/routes/alias-path-leak.tsx`
* `e2e/react-start/import-protection/src/routes/alias-path-namespace-leak.tsx`
* `e2e/react-start/import-protection/src/routes/non-alias-namespace-leak.tsx`
* `e2e/react-start/import-protection/tests/import-protection.spec.ts`
* `e2e/react-start/import-protection/tests/violations.setup.ts`
* `e2e/react-start/import-protection/vite.config.ts`
* `packages/start-plugin-core/src/constants.ts`
* `packages/start-plugin-core/src/import-protection-plugin/INTERNALS.md`
* `packages/start-plugin-core/src/import-protection-plugin/ast.ts`
* `packages/start-plugin-core/src/import-protection-plugin/constants.ts`
* `packages/start-plugin-core/src/import-protection-plugin/extensionlessAbsoluteIdResolver.ts`
* `packages/start-plugin-core/src/import-protection-plugin/plugin.ts`
* `packages/start-plugin-core/src/import-protection-plugin/postCompileUsage.ts`
* `packages/start-plugin-core/src/import-protection-plugin/rewriteDeniedImports.ts`
* `packages/start-plugin-core/src/import-protection-plugin/sourceLocation.ts`
* `packages/start-plugin-core/src/import-protection-plugin/types.ts`
* `packages/start-plugin-core/src/import-protection-plugin/utils.ts`
* `packages/start-plugin-core/src/import-protection-plugin/virtualModules.ts`
* `packages/start-plugin-core/src/start-compiler-plugin/plugin.ts`
* `packages/start-plugin-core/tests/importProtection/rewriteDeniedImports.test.ts`
* `packages/start-plugin-core/tests/importProtection/utils.test.ts`
* `packages/start-plugin-core/tests/importProtection/virtualModules.test.ts`

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (24)</summary>

* e2e/react-start/import-protection-custom-config/tests/utils/isErrorMode.ts
* e2e/react-start/import-protection/package.json
* e2e/react-start/import-protection-custom-config/.gitignore
* packages/start-plugin-core/src/import-protection-plugin/postCompileUsage.ts
* e2e/react-start/import-protection-custom-config/playwright.config.ts
* e2e/react-start/import-protection-custom-config/src/routes/backend-leak.tsx
* packages/start-plugin-core/src/import-protection-plugin/constants.ts
* e2e/react-start/import-protection-custom-config/package.json
* e2e/react-start/import-protection/src/routes/non-alias-namespace-leak.tsx
* e2e/react-start/import-protection-custom-config/vite.config.ts
* packages/start-plugin-core/tests/importProtection/rewriteDeniedImports.test.ts
* e2e/react-start/import-protection-custom-config/tests/error-mode.spec.ts
* e2e/react-start/import-protection-custom-config/src/routes/frontend-leak.tsx
* e2e/react-start/import-protection-custom-config/src/routeTree.gen.ts
* packages/start-plugin-core/tests/importProtection/utils.test.ts
* e2e/react-start/import-protection/vite.config.ts
* packages/start-plugin-core/src/constants.ts
* e2e/react-start/import-protection/src/routes/__root.tsx
* e2e/react-start/import-protection-custom-config/src/router.tsx
* e2e/react-start/import-protection-custom-config/src/routes/index.tsx
* packages/start-plugin-core/src/import-protection-plugin/types.ts
* packages/start-plugin-core/src/start-compiler-plugin/plugin.ts
* e2e/react-start/import-protection-custom-config/tests/custom-config.spec.ts
* e2e/react-start/import-protection-custom-config/tsconfig.json

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment on lines +1 to +8
import {
createRootRoute,
HeadContent,
Link,
linkOptions,
Outlet,
Scripts,
} from '@tanstack/react-router'
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix import member ordering to satisfy ESLint sort-imports.

Line 3 indicates member order is currently not lint-compliant.

Proposed diff
 import {
-  createRootRoute,
   HeadContent,
+  createRootRoute,
   Link,
   linkOptions,
   Outlet,
   Scripts,
 } from '@tanstack/react-router'
🧰 Tools
🪛 ESLint

[error] 3-3: Member 'HeadContent' of the import declaration should be sorted alphabetically.

(sort-imports)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-start/import-protection-custom-config/src/routes/__root.tsx` around
lines 1 - 8, The named imports from '@tanstack/react-router' in the import
statement (createRootRoute, HeadContent, Link, linkOptions, Outlet, Scripts) are
not in the order required by ESLint sort-imports; reorder the members to match
the project's alphabetical/sort-imports policy (e.g., alphabetically:
HeadContent, Link, Scripts, createRootRoute, linkOptions, Outlet or whatever
ordering your config enforces) so the import statement for these symbols is
lint-compliant, updating the import clause that references createRootRoute,
HeadContent, Link, linkOptions, Outlet, Scripts.

Comment on lines +551 to 552
`${v.envType}|${v.type}|${normalizeSpec(v)}|${v.importer.replace(/:.*/, '')}`

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid colon-stripping that breaks Windows paths in warm/cold dedupe keys.

v.importer.replace(/:.*/, '') can reduce C:\... to C, causing key collisions and incorrect warm-vs-cold comparisons.

🐛 Proposed fix
 const normalizeSpec = (v: Violation) =>
   (v.resolved ?? v.specifier).replace(/\.[cm]?[tj]sx?$/, '')
+const stripLineCol = (importer: string) => importer.replace(/:\d+:\d+$/, '')
 const uniqueKey = (v: Violation) =>
-  `${v.envType}|${v.type}|${normalizeSpec(v)}|${v.importer.replace(/:.*/, '')}`
+  `${v.envType}|${v.type}|${normalizeSpec(v)}|${stripLineCol(v.importer)}`
📝 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
`${v.envType}|${v.type}|${normalizeSpec(v)}|${v.importer.replace(/:.*/, '')}`
const normalizeSpec = (v: Violation) =>
(v.resolved ?? v.specifier).replace(/\.[cm]?[tj]sx?$/, '')
const stripLineCol = (importer: string) => importer.replace(/:\d+:\d+$/, '')
const uniqueKey = (v: Violation) =>
`${v.envType}|${v.type}|${normalizeSpec(v)}|${stripLineCol(v.importer)}`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/react-start/import-protection/tests/import-protection.spec.ts` around
lines 551 - 552, The key-generation currently strips everything after the first
colon with v.importer.replace(/:.*/, ''), which collapses Windows drive letters
(e.g., C:\) and causes collisions; update the logic that builds the dedupe key
to only remove URL/file scheme prefixes (e.g., strip "file:" or "file://" or
other protocol:// prefixes) while preserving Windows drive letters—use a
targeted replace like removing /^file:\/\// or a conditional that, if v.importer
startsWith('file:') or contains '://', strips the scheme, otherwise leaves
v.importer intact; reference the use site where the key is composed alongside
normalizeSpec and v.importer and replace the global /:.*/ replace with the safer
scheme-only handling.

Copy link
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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/start-plugin-core/src/import-protection-plugin/plugin.ts`:
- Around line 1871-1886: The branch guarded by "else if (importerSurvived &&
checkModuleId == null)" is currently re-checking module existence (via
toModuleIdCandidates(checkId) and survivingModules) which is redundant with the
earlier mockSurvived check and makes the branch unreachable; change the check to
test the actual surviving import-edge between the importer and each candidate
target instead of just module presence: use the data structure that tracks
surviving import edges (e.g., survivingEdges or the map that records
importer→target relations) to see if any candidate c has a surviving edge from
the importer (use importerId/importerSurvived and checkId/checkModuleId with
that edges map), and only continue when no such surviving edge exists; reference
symbols: importerSurvived, mockSurvived, checkModuleId, checkId,
toModuleIdCandidates, survivingModules, edgeSurvived—replace the
survivingModules-based edgeSurvived test with an import-edge existence check
against the surviving edges collection so file/specifier (barrel) violations can
be reported.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0761ff6 and 92a9a30.

📒 Files selected for processing (1)
  • packages/start-plugin-core/src/import-protection-plugin/plugin.ts

Comment on lines +1871 to +1886
} else if (importerSurvived && checkModuleId == null) {
// File/specifier violation: importerSurvived alone is NOT
// sufficient. The importer may survive for unrelated imports
// (barrel pattern) while tree-shaking eliminated the denied
// edge. Verify that the denied target's module candidates
// actually exist in survivingModules (i.e., an import edge
// between the importer and the denied target still exists).
const checkCandidates = toModuleIdCandidates(checkId)
const edgeSurvived = checkCandidates.some((c) =>
survivingModules.has(c),
)
if (!edgeSurvived) continue
} else {
// Neither the mock nor the importer survived — skip.
continue
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

sed -n '1835,1895p' packages/start-plugin-core/src/import-protection-plugin/plugin.ts

Repository: TanStack/router

Length of output: 2625


🏁 Script executed:

sed -n '1835,1920p' packages/start-plugin-core/src/import-protection-plugin/plugin.ts

Repository: TanStack/router

Length of output: 3450


🏁 Script executed:

fd "\.test\.(ts|tsx)$" packages/start-plugin-core/ | head -20

Repository: TanStack/router

Length of output: 1204


🏁 Script executed:

rg "edgeSurvived|deferredBuildViolations" --type=ts packages/start-plugin-core/ -B 3 -A 3

Repository: TanStack/router

Length of output: 6568


🏁 Script executed:

rg "generateBundle|deferredBuildViolations|edgeSurvived" packages/start-plugin-core/tests/ --type=ts -B 2 -A 2

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

sed -n '1200,1250p' packages/start-plugin-core/src/import-protection-plugin/plugin.ts

Repository: TanStack/router

Length of output: 2117


🏁 Script executed:

rg "mockModuleId.*checkModuleId" packages/start-plugin-core/src/import-protection-plugin/plugin.ts -B 3 -A 3

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

ls -la packages/start-plugin-core/tests/importProtection/

Repository: TanStack/router

Length of output: 802


🏁 Script executed:

grep -l "generateBundle\|tree-shak" packages/start-plugin-core/tests/importProtection/*.test.ts

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

head -100 packages/start-plugin-core/tests/importProtection/postCompileUsage.test.ts

Repository: TanStack/router

Length of output: 3134


🏁 Script executed:

rg "checkModuleId.*null" packages/start-plugin-core/src/ --type=ts -B 5 -A 5

Repository: TanStack/router

Length of output: 2301


🏁 Script executed:

rg "handleViolation" packages/start-plugin-core/tests/ --type=ts | head -20

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

sed -n '1350,1400p' packages/start-plugin-core/src/import-protection-plugin/plugin.ts

Repository: TanStack/router

Length of output: 1887


🏁 Script executed:

rg "DeferredBuildViolation|checkModuleId:" packages/start-plugin-core/src/import-protection-plugin/types.ts -B 2 -A 2

Repository: TanStack/router

Length of output: 318


🏁 Script executed:

rg "checkModuleId.*:" packages/start-plugin-core/src/import-protection-plugin/plugin.ts | head -20

Repository: TanStack/router

Length of output: 135


🏁 Script executed:

sed -n '1,150p' packages/start-plugin-core/src/import-protection-plugin/types.ts | tail -30

Repository: TanStack/router

Length of output: 813


🏁 Script executed:

rg "DeferredBuildViolation" packages/start-plugin-core/src/import-protection-plugin/types.ts -A 5

Repository: TanStack/router

Length of output: 371


🏁 Script executed:

rg "bundle|generateBundle" packages/start-plugin-core/tests/ --type=ts -l

Repository: TanStack/router

Length of output: 41


The edgeSurvived check is redundant with mockSurvived and makes this branch unreachable for violations.

In the else if (importerSurvived && checkModuleId == null) branch (lines 1871-1886), we only arrive when mockSurvived is false. However, the edgeSurvived check examines the identical condition: toModuleIdCandidates(checkId) where checkId == mockModuleId in this branch. Since mockSurvived already evaluated this against survivingModules and returned false, edgeSurvived will also be false, causing the if (!edgeSurvived) continue to always execute without reporting a violation.

The comment indicates intent to verify import edge survival for file/specifier violations (barrel pattern case), but the implementation only rechecks module existence rather than examining actual import edges. This branch never triggers violation reporting—it's dead code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/start-plugin-core/src/import-protection-plugin/plugin.ts` around
lines 1871 - 1886, The branch guarded by "else if (importerSurvived &&
checkModuleId == null)" is currently re-checking module existence (via
toModuleIdCandidates(checkId) and survivingModules) which is redundant with the
earlier mockSurvived check and makes the branch unreachable; change the check to
test the actual surviving import-edge between the importer and each candidate
target instead of just module presence: use the data structure that tracks
surviving import edges (e.g., survivingEdges or the map that records
importer→target relations) to see if any candidate c has a surviving edge from
the importer (use importerId/importerSurvived and checkId/checkModuleId with
that edges map), and only continue when no such surviving edge exists; reference
symbols: importerSurvived, mockSurvived, checkModuleId, checkId,
toModuleIdCandidates, survivingModules, edgeSurvived—replace the
survivingModules-based edgeSurvived test with an import-edge existence check
against the surviving edges collection so file/specifier (barrel) violations can
be reported.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Import protection doesn't work with TS path mapping

1 participant