@tanstack/start-plugin-core 1.163.3 → 1.163.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/constants.d.ts +1 -0
- package/dist/esm/constants.js +2 -0
- package/dist/esm/constants.js.map +1 -1
- package/dist/esm/import-protection-plugin/ast.d.ts +3 -0
- package/dist/esm/import-protection-plugin/ast.js +8 -0
- package/dist/esm/import-protection-plugin/ast.js.map +1 -0
- package/dist/esm/import-protection-plugin/constants.d.ts +6 -0
- package/dist/esm/import-protection-plugin/constants.js +24 -0
- package/dist/esm/import-protection-plugin/constants.js.map +1 -0
- package/dist/esm/import-protection-plugin/extensionlessAbsoluteIdResolver.d.ts +22 -0
- package/dist/esm/import-protection-plugin/extensionlessAbsoluteIdResolver.js +95 -0
- package/dist/esm/import-protection-plugin/extensionlessAbsoluteIdResolver.js.map +1 -0
- package/dist/esm/import-protection-plugin/plugin.d.ts +2 -13
- package/dist/esm/import-protection-plugin/plugin.js +684 -299
- package/dist/esm/import-protection-plugin/plugin.js.map +1 -1
- package/dist/esm/import-protection-plugin/postCompileUsage.js +4 -2
- package/dist/esm/import-protection-plugin/postCompileUsage.js.map +1 -1
- package/dist/esm/import-protection-plugin/rewriteDeniedImports.d.ts +4 -5
- package/dist/esm/import-protection-plugin/rewriteDeniedImports.js +225 -3
- package/dist/esm/import-protection-plugin/rewriteDeniedImports.js.map +1 -1
- package/dist/esm/import-protection-plugin/sourceLocation.d.ts +4 -7
- package/dist/esm/import-protection-plugin/sourceLocation.js +18 -73
- package/dist/esm/import-protection-plugin/sourceLocation.js.map +1 -1
- package/dist/esm/import-protection-plugin/types.d.ts +94 -0
- package/dist/esm/import-protection-plugin/utils.d.ts +33 -1
- package/dist/esm/import-protection-plugin/utils.js +69 -3
- package/dist/esm/import-protection-plugin/utils.js.map +1 -1
- package/dist/esm/import-protection-plugin/virtualModules.d.ts +30 -2
- package/dist/esm/import-protection-plugin/virtualModules.js +66 -23
- package/dist/esm/import-protection-plugin/virtualModules.js.map +1 -1
- package/dist/esm/start-compiler-plugin/plugin.d.ts +2 -1
- package/dist/esm/start-compiler-plugin/plugin.js +1 -2
- package/dist/esm/start-compiler-plugin/plugin.js.map +1 -1
- package/package.json +4 -4
- package/src/constants.ts +2 -0
- package/src/import-protection-plugin/INTERNALS.md +462 -60
- package/src/import-protection-plugin/ast.ts +7 -0
- package/src/import-protection-plugin/constants.ts +25 -0
- package/src/import-protection-plugin/extensionlessAbsoluteIdResolver.ts +121 -0
- package/src/import-protection-plugin/plugin.ts +1080 -597
- package/src/import-protection-plugin/postCompileUsage.ts +8 -2
- package/src/import-protection-plugin/rewriteDeniedImports.ts +141 -9
- package/src/import-protection-plugin/sourceLocation.ts +19 -89
- package/src/import-protection-plugin/types.ts +103 -0
- package/src/import-protection-plugin/utils.ts +123 -4
- package/src/import-protection-plugin/virtualModules.ts +117 -31
- package/src/start-compiler-plugin/plugin.ts +7 -2
|
@@ -16,7 +16,7 @@ The plugin must handle **two axes of configuration**:
|
|
|
16
16
|
|
|
17
17
|
## Plugin Architecture
|
|
18
18
|
|
|
19
|
-
`importProtectionPlugin()` returns **
|
|
19
|
+
`importProtectionPlugin()` returns **two** Vite plugins:
|
|
20
20
|
|
|
21
21
|
### 1. `tanstack-start-core:import-protection` (enforce: `'pre'`)
|
|
22
22
|
|
|
@@ -31,10 +31,18 @@ transformed code and composed sourcemaps for accurate source-location mapping
|
|
|
31
31
|
in violation messages. Also resolves post-transform imports and triggers
|
|
32
32
|
`processPendingViolations()` for the dev mock deferral path.
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
In both build and dev modes, this plugin performs **self-denial**: when a file
|
|
35
|
+
matches a deny pattern in the current environment (e.g. a `.server.ts` file
|
|
36
|
+
transformed in the client environment), its entire content is replaced with a
|
|
37
|
+
mock module. This is the core mechanism for preventing cross-environment cache
|
|
38
|
+
contamination — `resolveId` never returns virtual module IDs for file-based
|
|
39
|
+
violations, so there is nothing for third-party resolver caches
|
|
40
|
+
(e.g. `vite-tsconfig-paths`) to leak across environments. In dev mode, the
|
|
41
|
+
mock imports a `mock-runtime` module for runtime diagnostics; in build mode,
|
|
42
|
+
the mock is fully self-contained.
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
See the [Self-Denial Transform](#self-denial-transform-in-detail) section below
|
|
45
|
+
for a detailed walkthrough with code examples.
|
|
38
46
|
|
|
39
47
|
## Violation Types
|
|
40
48
|
|
|
@@ -71,15 +79,20 @@ and Rollup's tree-shaking then eliminates the `./db.server` dependency entirely.
|
|
|
71
79
|
|
|
72
80
|
During dev, Vite's `fetchModule(?SERVER_FN_LOOKUP)` call triggers resolves for
|
|
73
81
|
analysing a module's exports. These are tracked via `serverFnLookupModules` and
|
|
74
|
-
`isPreTransformResolve
|
|
82
|
+
`isPreTransformResolve`. In dev mock mode, pre-transform violations are
|
|
83
|
+
deferred like all other violations (verified via edge-survival and graph
|
|
84
|
+
reachability). In dev error mode, they are silenced because no deferred
|
|
85
|
+
verification path exists.
|
|
75
86
|
|
|
76
87
|
## Violation Handling Flow
|
|
77
88
|
|
|
78
89
|
### Central functions
|
|
79
90
|
|
|
80
91
|
- **`handleViolation()`**: Formats + reports (or silences) the violation. Returns
|
|
81
|
-
a
|
|
82
|
-
|
|
92
|
+
a resolve result so `resolveId` can substitute the offending import: for
|
|
93
|
+
file-based violations, returns the physical file path (self-denial handles
|
|
94
|
+
the rest in transform); for specifier/marker violations, returns a mock-edge
|
|
95
|
+
module ID. May also return `undefined` (suppressed by `onViolation` or
|
|
83
96
|
silent+error in dev) or throw via `ctx.error()` (dev+error).
|
|
84
97
|
- **`reportOrDeferViolation()`**: Dispatch layer. Either defers (stores for later
|
|
85
98
|
verification) or reports immediately, depending on `shouldDefer`.
|
|
@@ -87,11 +100,14 @@ analysing a module's exports. These are tracked via `serverFnLookupModules` and
|
|
|
87
100
|
### `shouldDefer` logic
|
|
88
101
|
|
|
89
102
|
```ts
|
|
90
|
-
shouldDefer =
|
|
103
|
+
shouldDefer = isBuild || isDevMock
|
|
91
104
|
```
|
|
92
105
|
|
|
93
|
-
- **Dev mock**:
|
|
94
|
-
|
|
106
|
+
- **Dev mock**: ALL violations (including pre-transform resolves) are deferred
|
|
107
|
+
to `pendingViolations` → verified via edge-survival and post-transform graph
|
|
108
|
+
reachability in `processPendingViolations()`. Pre-transform violations are
|
|
109
|
+
tagged with `fromPreTransformResolve` so the pending-violation processor
|
|
110
|
+
knows to wait for post-transform data before emitting.
|
|
95
111
|
- **Build (both mock and error)**: Defer to `deferredBuildViolations` → verify
|
|
96
112
|
via tree-shaking survival in `generateBundle`.
|
|
97
113
|
|
|
@@ -112,18 +128,43 @@ Violations fire immediately via `ctx.error()` in `resolveId`. No tree-shaking
|
|
|
112
128
|
is available, so false positives for barrel patterns are expected and accepted.
|
|
113
129
|
(Dev + error is typically used only during explicit validation.)
|
|
114
130
|
|
|
131
|
+
Pre-transform resolves (e.g. server-fn-lookup) are silenced in error mode
|
|
132
|
+
because they fire before the Start compiler runs — imports inside `.server()`
|
|
133
|
+
callbacks haven't been stripped yet, and error mode has no deferred verification
|
|
134
|
+
path.
|
|
135
|
+
|
|
115
136
|
### Dev + Mock
|
|
116
137
|
|
|
117
|
-
1. `resolveId` calls `
|
|
118
|
-
2.
|
|
119
|
-
|
|
120
|
-
|
|
138
|
+
1. `resolveId` detects the violation and calls `reportOrDeferViolation()`.
|
|
139
|
+
2. For **file-based violations**: `handleViolation()` returns the **physical
|
|
140
|
+
file path** (same as build mode). The self-denial transform in the
|
|
141
|
+
transform-cache plugin will replace the file's content with a dev mock
|
|
142
|
+
module that imports `mock-runtime` for runtime diagnostics.
|
|
143
|
+
3. For **specifier/marker violations**: `handleViolation()` returns a mock-edge
|
|
144
|
+
module ID so the dev server can serve a Proxy-based mock.
|
|
145
|
+
4. The violation is stored in `pendingViolations` keyed by the importer's file
|
|
121
146
|
path.
|
|
122
|
-
|
|
147
|
+
5. The transform-cache plugin, after resolving post-transform imports, calls
|
|
123
148
|
`processPendingViolations()`.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
149
|
+
6. `processPendingViolations()` first applies **edge-survival**: if
|
|
150
|
+
post-transform import data is available for the importer, it checks whether
|
|
151
|
+
the denied import survived the Start compiler transform. Imports stripped
|
|
152
|
+
by the compiler (e.g. inside `.server()` callbacks) are discarded. For
|
|
153
|
+
pre-transform violations (`fromPreTransformResolve`), the function waits
|
|
154
|
+
until post-transform data is available before proceeding.
|
|
155
|
+
7. After edge-survival, `processPendingViolations()` checks graph reachability
|
|
156
|
+
from entry points using only post-transform edges. If the violating
|
|
157
|
+
importer is reachable → confirm (warn). If unreachable → discard. If
|
|
158
|
+
unknown → keep pending or emit conservatively (warm-start fallback).
|
|
159
|
+
|
|
160
|
+
Warm-start stability guardrails:
|
|
161
|
+
|
|
162
|
+
- Resolve-time edges discovered through pre-transform paths
|
|
163
|
+
(`isPreTransformResolve`, especially `SERVER_FN_LOOKUP`) are **not** recorded
|
|
164
|
+
into the reachability graph.
|
|
165
|
+
- In `'unknown'` reachability status, pre-transform pending violations are
|
|
166
|
+
kept pending (not fallback-emitted) until non-lookup transform evidence is
|
|
167
|
+
available.
|
|
127
168
|
|
|
128
169
|
This approach can't fully eliminate barrel false-positives in dev because
|
|
129
170
|
there's no tree-shaking. The barrel's import of `.server` always resolves,
|
|
@@ -131,18 +172,163 @@ and the barrel is reachable. This is a known and accepted limitation.
|
|
|
131
172
|
|
|
132
173
|
### Dev mock modules
|
|
133
174
|
|
|
134
|
-
|
|
175
|
+
Dev violations are handled differently depending on their type:
|
|
176
|
+
|
|
177
|
+
- **File-based violations** use **self-denial** (same mechanism as build mode):
|
|
178
|
+
the denied file's content is replaced by the transform-cache plugin with a
|
|
179
|
+
mock that imports `mock-runtime` for runtime diagnostics. The export list
|
|
180
|
+
comes from the **denied file's AST** (what it exports), generated by
|
|
181
|
+
`generateDevSelfDenialModule()`. This approach avoids the cold-start problem
|
|
182
|
+
where the importer's AST is unavailable at `resolveId` time, and prevents
|
|
183
|
+
cross-environment cache contamination from third-party resolver plugins.
|
|
184
|
+
|
|
185
|
+
- **Specifier/marker violations** use **mock-edge modules**: each violation
|
|
186
|
+
gets a per-importer mock edge module that explicitly exports the names the
|
|
187
|
+
importer expects (extracted lazily by `resolveExportsForDeniedSpecifier()`
|
|
188
|
+
which parses the importer's AST) and delegates to a **runtime mock module**
|
|
189
|
+
that contains a recursive Proxy and optional runtime diagnostics.
|
|
190
|
+
|
|
191
|
+
### Mock edge modules (in detail)
|
|
192
|
+
|
|
193
|
+
A **mock edge module** is a lightweight, auto-generated virtual module that sits
|
|
194
|
+
between an importing file and a base mock module. Its purpose is to provide
|
|
195
|
+
**explicit ESM named exports** so that bundlers (Rollup, Rolldown) and Vite's dev
|
|
196
|
+
server can correctly resolve `import { foo } from './denied.server'` — even
|
|
197
|
+
though the real module has been replaced by a mock.
|
|
198
|
+
|
|
199
|
+
#### Why they exist
|
|
200
|
+
|
|
201
|
+
The base mock module (`\0tanstack-start-import-protection:mock`) exports only a
|
|
202
|
+
`default` export — a recursive Proxy. But consumers of the denied module may use
|
|
203
|
+
named imports:
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
import { getSecret, initDb } from './credentials.server'
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Without explicit named exports, this would fail: the bundler would complain that
|
|
210
|
+
`getSecret` and `initDb` don't exist on the mock module. Using
|
|
211
|
+
`syntheticNamedExports: true` in Rollup could solve this, but Rolldown (which
|
|
212
|
+
Vite can now use) doesn't support it. Mock edge modules solve the problem
|
|
213
|
+
portably by generating real ESM export statements.
|
|
214
|
+
|
|
215
|
+
#### Structure
|
|
216
|
+
|
|
217
|
+
Each mock edge module is identified by a virtual ID:
|
|
218
|
+
|
|
219
|
+
```text
|
|
220
|
+
\0tanstack-start-import-protection:mock-edge:<BASE64_PAYLOAD>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The Base64URL payload encodes two fields:
|
|
224
|
+
|
|
225
|
+
- `exports` — the list of named export identifiers the importer needs
|
|
226
|
+
- `runtimeId` — the module ID of the backing mock to import from
|
|
227
|
+
|
|
228
|
+
When Vite's `load` hook encounters this virtual ID, `loadMockEdgeModule()`
|
|
229
|
+
decodes the payload and generates code like this:
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
// Generated mock edge module for exports: ["getSecret", "initDb"]
|
|
233
|
+
import mock from '\0tanstack-start-import-protection:mock'
|
|
234
|
+
export const getSecret = mock.getSecret
|
|
235
|
+
export const initDb = mock.initDb
|
|
236
|
+
export default mock
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Each `mock.getSecret` access returns the Proxy itself (the Proxy's `get` trap
|
|
240
|
+
returns `mock` for any property), so the named exports are valid callable/
|
|
241
|
+
constructable values that won't crash at runtime.
|
|
242
|
+
|
|
243
|
+
#### How exports are determined
|
|
244
|
+
|
|
245
|
+
The export list comes from **parsing the importer's AST** — not the denied
|
|
246
|
+
file's AST. The function `resolveExportsForDeniedSpecifier()` performs this:
|
|
247
|
+
|
|
248
|
+
1. Gets the importer's code from the transform cache or `getModuleInfo`.
|
|
249
|
+
2. Calls `collectMockExportNamesBySource()` to extract which names the
|
|
250
|
+
importer uses from each import source (parses the AST internally):
|
|
251
|
+
- **Named imports**: `import { getSecret, initDb } from './creds.server'`
|
|
252
|
+
→ `['getSecret', 'initDb']`
|
|
253
|
+
- **Namespace member access**: `import * as creds from './creds.server'` then
|
|
254
|
+
`creds.getSecret()` → `['getSecret']`
|
|
255
|
+
- **Default import member access**: `import creds from './creds.server'` then
|
|
256
|
+
`creds.getSecret` → `['getSecret']`
|
|
257
|
+
- **Re-exports**: `export { getSecret } from './creds.server'`
|
|
258
|
+
→ `['getSecret']`
|
|
259
|
+
3. Caches the result per importer so multiple violations from the same file
|
|
260
|
+
don't re-parse.
|
|
261
|
+
|
|
262
|
+
This "importer-driven" approach means the mock edge module only exports the
|
|
263
|
+
names the consumer actually references — not all exports from the denied file.
|
|
264
|
+
|
|
265
|
+
#### Dev vs build mock edge modules
|
|
266
|
+
|
|
267
|
+
| Aspect | Dev mock edge | Build mock edge |
|
|
268
|
+
| ---------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
269
|
+
| **Backing mock** | `mock-runtime:BASE64` (runtime diagnostics) or `mock` (silent) | `mock:build:N` (per-violation unique, silent) |
|
|
270
|
+
| **Purpose** | Serve mocks in dev server + runtime warnings | Track tree-shaking survival in `generateBundle` |
|
|
271
|
+
| **Created for** | Specifier/marker violations only (file uses self-denial instead) | Specifier/marker violations only (file uses self-denial instead) |
|
|
272
|
+
| **Uniqueness** | Per-importer per-specifier | Per-violation (unique counter) |
|
|
273
|
+
|
|
274
|
+
In **dev**, the backing `runtimeId` is a `mock-runtime:BASE64` module that
|
|
275
|
+
includes diagnostic metadata (environment, import path, trace). When the mock
|
|
276
|
+
is accessed in the browser, it logs a console warning or error. For SSR or
|
|
277
|
+
when `mockAccess` is `'off'`, the backing mock is the shared silent
|
|
278
|
+
`MOCK_MODULE_ID`.
|
|
279
|
+
|
|
280
|
+
In **build**, the backing mock uses a unique counter ID
|
|
281
|
+
(`mock:build:0`, `mock:build:1`, ...) so `generateBundle` can check whether
|
|
282
|
+
each specific violation's mock survived tree-shaking.
|
|
283
|
+
|
|
284
|
+
#### Handling non-identifier export names
|
|
285
|
+
|
|
286
|
+
ES2022 allows string-keyed exports like `export { x as "foo-bar" }`. Mock edge
|
|
287
|
+
modules handle these via an intermediate variable:
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
import mock from '\0tanstack-start-import-protection:mock'
|
|
291
|
+
export const validName = mock.validName
|
|
292
|
+
const __tss_str_0 = mock['foo-bar']
|
|
293
|
+
export { __tss_str_0 as 'foo-bar' }
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
The `default` export name is always filtered out (handled separately as
|
|
297
|
+
`export default mock`).
|
|
298
|
+
|
|
299
|
+
### File-based violations: self-denial (dev and build)
|
|
300
|
+
|
|
301
|
+
File-based violations (e.g. `import './db.server'` in client env) use
|
|
302
|
+
self-denial in **both** dev and build modes. `handleViolation()` returns the
|
|
303
|
+
physical file path, and the transform-cache plugin replaces the file's
|
|
304
|
+
contents with a mock module.
|
|
305
|
+
|
|
306
|
+
- **Build mode**: Uses `generateSelfContainedMockModule()` — a fully
|
|
307
|
+
self-contained mock with an inlined Proxy factory (no `import` statements).
|
|
308
|
+
This is important because build-mode mocks must be tree-shakeable.
|
|
309
|
+
|
|
310
|
+
- **Dev mode**: Uses `generateDevSelfDenialModule()` — a mock that imports
|
|
311
|
+
`mock-runtime` for runtime diagnostics (console warnings/errors when the
|
|
312
|
+
mock is accessed in the browser). The `mock-runtime` module ID encodes
|
|
313
|
+
violation metadata (environment, import path, trace).
|
|
314
|
+
|
|
315
|
+
Self-denial avoids creating virtual mock-edge module IDs that could
|
|
316
|
+
contaminate third-party resolver caches across Vite environments.
|
|
317
|
+
|
|
318
|
+
### Non-file violations: mock-edge modules (dev and build)
|
|
135
319
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
- Delegates to a **runtime mock module** that contains a recursive Proxy and
|
|
139
|
-
optional runtime diagnostics (console warnings when mocked values are used).
|
|
320
|
+
Specifier and marker violations use **mock-edge modules** because the denied
|
|
321
|
+
specifier doesn't resolve to a physical file that could be "self-denied."
|
|
140
322
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
(`\0tanstack-start-import-protection:mock:build:N`). The edge module
|
|
144
|
-
the named exports the importer expects,
|
|
145
|
-
with both Rollup and Rolldown (which doesn't support
|
|
323
|
+
- **Build mode**: Each violation gets a **per-violation mock edge module**
|
|
324
|
+
wrapping a unique base mock module
|
|
325
|
+
(`\0tanstack-start-import-protection:mock:build:N`). The edge module
|
|
326
|
+
re-exports the named exports the importer expects, ensuring compatibility
|
|
327
|
+
with both Rollup and Rolldown (which doesn't support
|
|
328
|
+
`syntheticNamedExports`).
|
|
329
|
+
|
|
330
|
+
- **Dev mode**: Each violation gets a **per-importer mock edge module**
|
|
331
|
+
wrapping a `mock-runtime` module for runtime diagnostics.
|
|
146
332
|
|
|
147
333
|
## Build Mode Strategy
|
|
148
334
|
|
|
@@ -150,22 +336,27 @@ with both Rollup and Rolldown (which doesn't support `syntheticNamedExports`).
|
|
|
150
336
|
|
|
151
337
|
Both mock and error build modes follow the same pattern:
|
|
152
338
|
|
|
153
|
-
1. **`resolveId`**: Call `handleViolation({ silent: true })`.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
339
|
+
1. **`resolveId`**: Call `handleViolation({ silent: true })`.
|
|
340
|
+
- For **file-based violations**: Returns the physical file path. The
|
|
341
|
+
self-denial transform will replace its content.
|
|
342
|
+
- For **specifier/marker violations**: Generates a **unique per-violation
|
|
343
|
+
mock-edge module** wrapping a base mock module
|
|
344
|
+
(`\0tanstack-start-import-protection:mock:build:N`). Stores the violation +
|
|
345
|
+
mock-edge ID in `env.deferredBuildViolations`. Returns the mock-edge ID.
|
|
346
|
+
|
|
347
|
+
2. **`transform`** (self-denial): For file-based violations, the transform-cache
|
|
348
|
+
plugin detects that the current file is denied in this environment and
|
|
349
|
+
replaces its content with a self-contained mock module
|
|
350
|
+
(`generateSelfContainedMockModule()`).
|
|
351
|
+
|
|
352
|
+
3. **`load`**: For base mock modules, returns a silent Proxy-based mock. For
|
|
353
|
+
mock-edge modules, returns code that imports from the base mock and
|
|
162
354
|
re-exports the expected named bindings (e.g. `export const Foo = mock.Foo`).
|
|
163
355
|
|
|
164
|
-
|
|
165
|
-
the mock
|
|
166
|
-
modules are eliminated.
|
|
356
|
+
4. **Tree-shaking**: The bundler processes the bundle normally. If no binding from
|
|
357
|
+
the mock module is actually used at runtime, the modules are eliminated.
|
|
167
358
|
|
|
168
|
-
|
|
359
|
+
5. **`generateBundle`**: Inspect the output chunks. For each deferred violation,
|
|
169
360
|
check whether its unique mock module ID appears in any chunk's `modules`.
|
|
170
361
|
- **Survived** → real violation (the import wasn't tree-shaken away).
|
|
171
362
|
- Error mode: `ctx.error()` — fail the build.
|
|
@@ -199,19 +390,19 @@ per-environment verification.
|
|
|
199
390
|
|
|
200
391
|
## Marker violations vs. file/specifier violations
|
|
201
392
|
|
|
202
|
-
| | Marker |
|
|
203
|
-
| ----------------------------- | --------------------------------------------------- | -------------------------------------------- |
|
|
204
|
-
| **What triggers it** | The _importer_ has a conflicting directive | The _import
|
|
205
|
-
| **resolveId returns (dev)** | Marker module ID (`RESOLVED_MARKER_*`) \* | Mock edge module ID (per-importer) |
|
|
206
|
-
| **resolveId returns (build)** | Unique build mock ID
|
|
207
|
-
| **Can be tree-shaken away?** | Yes — if the importer is eliminated by tree-shaking | Yes — if no binding from the target survives |
|
|
208
|
-
| **Deferred in build?** | Yes — deferred to `generateBundle` | Yes — deferred to `generateBundle` |
|
|
209
|
-
| **Deferred in dev mock?** | Yes — deferred to pending graph | Yes — deferred to pending graph |
|
|
393
|
+
| | Marker | Specifier | File |
|
|
394
|
+
| ----------------------------- | --------------------------------------------------- | -------------------------------------------- | --------------------------------------------- |
|
|
395
|
+
| **What triggers it** | The _importer_ has a conflicting directive | The _import specifier_ matches a deny rule | The _resolved path_ matches a deny glob |
|
|
396
|
+
| **resolveId returns (dev)** | Marker module ID (`RESOLVED_MARKER_*`) \* | Mock edge module ID (per-importer) | Physical file path (self-denial in transform) |
|
|
397
|
+
| **resolveId returns (build)** | Unique build mock ID | Unique build mock ID | Physical file path (self-denial in transform) |
|
|
398
|
+
| **Can be tree-shaken away?** | Yes — if the importer is eliminated by tree-shaking | Yes — if no binding from the target survives | Yes — if no binding from the target survives |
|
|
399
|
+
| **Deferred in build?** | Yes — deferred to `generateBundle` | Yes — deferred to `generateBundle` | Yes — deferred to `generateBundle` |
|
|
400
|
+
| **Deferred in dev mock?** | Yes — deferred to pending graph | Yes — deferred to pending graph | Yes — deferred to pending graph |
|
|
210
401
|
|
|
211
|
-
\* In dev mock, `handleViolation` internally returns
|
|
402
|
+
\* In dev mock, `handleViolation` internally returns the physical file path
|
|
212
403
|
(stored as the deferred result in `pendingViolations`), but `resolveId` ignores
|
|
213
|
-
it for markers and falls through to return the marker module ID. The
|
|
214
|
-
|
|
404
|
+
it for markers and falls through to return the marker module ID. The file path
|
|
405
|
+
is only used for deferral bookkeeping, not for module resolution.
|
|
215
406
|
|
|
216
407
|
## State Management
|
|
217
408
|
|
|
@@ -239,13 +430,13 @@ Per-build/watch iteration, `buildStart` clears `pendingViolations` and resets
|
|
|
239
430
|
|
|
240
431
|
## Virtual Module IDs
|
|
241
432
|
|
|
242
|
-
| ID Pattern | Usage
|
|
243
|
-
| -------------------------------------------------------- |
|
|
244
|
-
| `\0tanstack-start-import-protection:mock` | Shared silent mock (dev mock only)
|
|
245
|
-
| `\0tanstack-start-import-protection:mock:build:N` | Per-violation build mock (unique counter)
|
|
246
|
-
| `\0tanstack-start-import-protection:mock-edge:BASE64` | Per-importer
|
|
247
|
-
| `\0tanstack-start-import-protection:mock-runtime:BASE64` | Runtime diagnostic mock (dev client, console warnings)
|
|
248
|
-
| `\0tanstack-start-import-protection:marker:*` | Marker module (empty `export {}`). Suffixed `server-only` or `client-only`; derived from `MARKER_PREFIX` in `
|
|
433
|
+
| ID Pattern | Usage |
|
|
434
|
+
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
435
|
+
| `\0tanstack-start-import-protection:mock` | Shared silent mock (dev mock only) |
|
|
436
|
+
| `\0tanstack-start-import-protection:mock:build:N` | Per-violation build mock (unique counter) |
|
|
437
|
+
| `\0tanstack-start-import-protection:mock-edge:BASE64` | Per-importer mock with explicit named exports (specifier/marker violations, dev + build) |
|
|
438
|
+
| `\0tanstack-start-import-protection:mock-runtime:BASE64` | Runtime diagnostic mock (dev client, console warnings) |
|
|
439
|
+
| `\0tanstack-start-import-protection:marker:*` | Marker module (empty `export {}`). Suffixed `server-only` or `client-only`; derived from `MARKER_PREFIX` in `virtualModules.ts` |
|
|
249
440
|
|
|
250
441
|
## Key Design Decisions
|
|
251
442
|
|
|
@@ -284,7 +475,10 @@ build mode provides definitive answers via tree-shaking.
|
|
|
284
475
|
|
|
285
476
|
## E2E Test Structure
|
|
286
477
|
|
|
287
|
-
Tests live in `e2e/react-start/import-protection
|
|
478
|
+
Tests live in `e2e/react-start/import-protection/` and
|
|
479
|
+
`e2e/react-start/import-protection-custom-config/`:
|
|
480
|
+
|
|
481
|
+
### Main test suite (`import-protection/`)
|
|
288
482
|
|
|
289
483
|
- **Mock mode** (default): `globalSetup` builds the app, captures build warnings
|
|
290
484
|
to `violations.build.json`, starts a dev server capturing to
|
|
@@ -296,3 +490,211 @@ Tests live in `e2e/react-start/import-protection/`:
|
|
|
296
490
|
re-exporting from both a `.server` file and a marker-protected file (`foo.ts`
|
|
297
491
|
with `import 'server-only'`), where all server-only bindings are tree-shaken
|
|
298
492
|
away, produces **zero** violations in the build log.
|
|
493
|
+
|
|
494
|
+
### Custom-config test suite (`import-protection-custom-config/`)
|
|
495
|
+
|
|
496
|
+
Uses non-default deny patterns (`**/*.backend.*` / `**/*.frontend.*`) to verify
|
|
497
|
+
that import protection works with user-configured file patterns. This ensures
|
|
498
|
+
the plugin doesn't hardcode any assumption about `.server`/`.client` naming
|
|
499
|
+
conventions. The `vite.config.ts` provides custom `client.files` and
|
|
500
|
+
`server.files` arrays; no `vite-tsconfig-paths` is used.
|
|
501
|
+
|
|
502
|
+
## Self-Denial Transform (In Detail)
|
|
503
|
+
|
|
504
|
+
The self-denial transform is the mechanism by which the plugin prevents cross-
|
|
505
|
+
environment contamination without creating virtual module IDs that could leak
|
|
506
|
+
through third-party resolver caches. It applies to **file-based violations in
|
|
507
|
+
both dev and build modes**.
|
|
508
|
+
|
|
509
|
+
### The problem it solves
|
|
510
|
+
|
|
511
|
+
In Vite 7+, client and SSR environments run within the same Vite process. Some
|
|
512
|
+
plugins (e.g. `vite-tsconfig-paths`) maintain a global resolution cache shared
|
|
513
|
+
across environments. If the import-protection plugin resolves a specifier to a
|
|
514
|
+
virtual mock module ID (e.g.
|
|
515
|
+
`\0tanstack-start-import-protection:mock-edge:...`) in the client environment,
|
|
516
|
+
that virtual ID can leak into the SSR environment's cache. When the SSR
|
|
517
|
+
environment later resolves the same specifier, it finds the cached virtual ID
|
|
518
|
+
instead of the real file — breaking the server.
|
|
519
|
+
|
|
520
|
+
In dev mode, self-denial also solves a **cold-start problem**: on cold start
|
|
521
|
+
(no `.vite` cache), the importer's AST is unavailable when `resolveId` runs
|
|
522
|
+
(neither the transform cache nor `getModuleInfo` have content yet). If mock-edge
|
|
523
|
+
module IDs were generated at `resolveId` time, the export list would be empty
|
|
524
|
+
(since it's derived from parsing the importer), producing a mock with no named
|
|
525
|
+
exports — causing runtime errors like
|
|
526
|
+
`does not provide an export named: 'getSecret'`.
|
|
527
|
+
|
|
528
|
+
### How it works
|
|
529
|
+
|
|
530
|
+
Instead of returning a virtual module ID from `resolveId`, the self-denial
|
|
531
|
+
transform works at the **transform** stage:
|
|
532
|
+
|
|
533
|
+
1. **`resolveId`**: For file-based violations, `handleViolation()` returns the
|
|
534
|
+
**physical file path**. The import resolves normally. No virtual ID is
|
|
535
|
+
created.
|
|
536
|
+
|
|
537
|
+
2. **transform-cache plugin**: When the transform-cache hook processes a file,
|
|
538
|
+
it checks whether the file matches any deny pattern for the current
|
|
539
|
+
environment using `checkFileDenial()`. If the file is denied, the plugin:
|
|
540
|
+
|
|
541
|
+
a. Extracts the file's named exports using `collectNamedExports()` (parses
|
|
542
|
+
the AST internally).
|
|
543
|
+
b. **Build mode**: Generates a self-contained mock module via
|
|
544
|
+
`generateSelfContainedMockModule()` — no imports, inlined Proxy factory.
|
|
545
|
+
c. **Dev mode**: Generates a dev mock module via
|
|
546
|
+
`generateDevSelfDenialModule()` — imports `mock-runtime` for runtime
|
|
547
|
+
diagnostics (console warnings/errors when the mock is accessed).
|
|
548
|
+
d. Returns the mock code as the transform result, completely replacing the
|
|
549
|
+
original file content.
|
|
550
|
+
|
|
551
|
+
3. **Fallback**: If AST parsing fails (e.g. due to syntax errors in the denied
|
|
552
|
+
file), `exportNames` defaults to `[]` (empty) and the mock module has no
|
|
553
|
+
named exports:
|
|
554
|
+
- Build: `generateSelfContainedMockModule([])`.
|
|
555
|
+
- Dev: `generateDevSelfDenialModule([], runtimeId)` where `runtimeId` is
|
|
556
|
+
computed via `mockRuntimeModuleIdFromViolation()` (which may internally
|
|
557
|
+
fall back to the shared silent `MOCK_MODULE_ID` for SSR or when
|
|
558
|
+
`mockAccess` is `'off'`).
|
|
559
|
+
|
|
560
|
+
### Code transformation example
|
|
561
|
+
|
|
562
|
+
**Original file** (`src/lib/credentials.server.ts`):
|
|
563
|
+
|
|
564
|
+
```typescript
|
|
565
|
+
import { db } from './database'
|
|
566
|
+
|
|
567
|
+
export function getSecret(): string {
|
|
568
|
+
return db.query('SELECT secret FROM config LIMIT 1')
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export const API_KEY = process.env.API_KEY!
|
|
572
|
+
|
|
573
|
+
export default { getSecret, API_KEY }
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
**After self-denial transform in build mode** (client environment):
|
|
577
|
+
|
|
578
|
+
```typescript
|
|
579
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
580
|
+
function createMock() {
|
|
581
|
+
const handler = {
|
|
582
|
+
get: (_, prop) => {
|
|
583
|
+
if (prop === Symbol.toPrimitive) return () => 'MOCK'
|
|
584
|
+
if (typeof prop === 'symbol') return undefined
|
|
585
|
+
return mock
|
|
586
|
+
},
|
|
587
|
+
apply: () => mock,
|
|
588
|
+
construct: () => mock,
|
|
589
|
+
}
|
|
590
|
+
const mock = /* @__PURE__ */ new Proxy(function () {}, handler)
|
|
591
|
+
return mock
|
|
592
|
+
}
|
|
593
|
+
const mock = /* @__PURE__ */ createMock()
|
|
594
|
+
export const getSecret = mock.getSecret
|
|
595
|
+
export const API_KEY = mock.API_KEY
|
|
596
|
+
export default mock
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
**After self-denial transform in dev mode** (client environment):
|
|
600
|
+
|
|
601
|
+
```typescript
|
|
602
|
+
import mock from '\0tanstack-start-import-protection:mock-runtime:eyJ...'
|
|
603
|
+
export const getSecret = mock.getSecret
|
|
604
|
+
export const API_KEY = mock.API_KEY
|
|
605
|
+
export default mock
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
The `mock-runtime` module ID encodes violation metadata (environment, importer,
|
|
609
|
+
specifier, trace) as a Base64URL payload. When the mock is accessed in the
|
|
610
|
+
browser, it logs a console warning or error with this metadata.
|
|
611
|
+
|
|
612
|
+
### Key properties
|
|
613
|
+
|
|
614
|
+
**Build-mode mock** (`generateSelfContainedMockModule`):
|
|
615
|
+
|
|
616
|
+
- **Self-contained**: No `import` statements — the Proxy factory is inlined.
|
|
617
|
+
- **Tree-shakeable**: All exports are marked pure (`@__NO_SIDE_EFFECTS__`,
|
|
618
|
+
`@__PURE__`), so the bundler can eliminate unused exports.
|
|
619
|
+
|
|
620
|
+
**Dev-mode mock** (`generateDevSelfDenialModule`):
|
|
621
|
+
|
|
622
|
+
- **Runtime diagnostics**: Imports `mock-runtime` which provides console
|
|
623
|
+
warnings/errors when the mock is accessed in the browser.
|
|
624
|
+
- **Lightweight**: No inlined Proxy factory — delegates to the mock-runtime
|
|
625
|
+
virtual module.
|
|
626
|
+
|
|
627
|
+
**Both modes**:
|
|
628
|
+
|
|
629
|
+
- **Same export interface**: Named exports match the original file's exports,
|
|
630
|
+
so consumers' import bindings remain valid.
|
|
631
|
+
- **Safe at runtime**: Every access returns the same Proxy, which is callable,
|
|
632
|
+
constructable, and returns "MOCK" when coerced to a string.
|
|
633
|
+
- **Export accuracy from denied file**: Unlike mock-edge modules (which parse
|
|
634
|
+
the importer's AST), self-denial parses the **denied file's own AST**.
|
|
635
|
+
This provides a safe over-approximation — all exports the file offers — and
|
|
636
|
+
avoids the cold-start problem where the importer hasn't been transformed yet.
|
|
637
|
+
|
|
638
|
+
### Why self-denial instead of virtual modules for file-based violations
|
|
639
|
+
|
|
640
|
+
| Approach | Virtual mock ID | Self-denial |
|
|
641
|
+
| ------------------ | -------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
642
|
+
| `resolveId` return | `\0mock-edge:BASE64` (virtual) | Physical file path (resolves normally) |
|
|
643
|
+
| Cache risk | Virtual ID stored in shared resolver cache → leaks to SSR | Physical path cached → correct in both environments |
|
|
644
|
+
| Module identity | New virtual module per violation | Same physical file, different content per environment |
|
|
645
|
+
| Export accuracy | From importer's AST (what it imports) | From denied file's AST (what it exports) |
|
|
646
|
+
| Cold-start safety | Fails — importer AST unavailable on cold start → empty exports | Safe — denied file's source code always available in transform |
|
|
647
|
+
|
|
648
|
+
### When self-denial is NOT used
|
|
649
|
+
|
|
650
|
+
Self-denial only applies to **file-based** violations.
|
|
651
|
+
|
|
652
|
+
- **Specifier violations** (e.g. `import '@tanstack/react-start/server'`):
|
|
653
|
+
These are bare specifiers, not file paths. They use virtual mock-edge module
|
|
654
|
+
IDs because the specifier doesn't resolve to a physical file that could be
|
|
655
|
+
"self-denied."
|
|
656
|
+
- **Marker violations**: Use virtual mock IDs in build mode for
|
|
657
|
+
`generateBundle` tracking (see marker section above). In dev mode, markers
|
|
658
|
+
resolve to the marker module ID directly.
|
|
659
|
+
|
|
660
|
+
## Module Dependency Architecture
|
|
661
|
+
|
|
662
|
+
The import-protection plugin is structured to avoid heavy transitive
|
|
663
|
+
dependencies in its utility modules:
|
|
664
|
+
|
|
665
|
+
```text
|
|
666
|
+
constants.ts ← lightweight (no heavy imports)
|
|
667
|
+
├── IMPORT_PROTECTION_DEBUG
|
|
668
|
+
├── KNOWN_SOURCE_EXTENSIONS
|
|
669
|
+
├── SERVER_FN_LOOKUP_QUERY ← imports SERVER_FN_LOOKUP from parent constants.ts
|
|
670
|
+
└── VITE_BROWSER_VIRTUAL_PREFIX
|
|
671
|
+
|
|
672
|
+
defaults.ts ← lightweight (type-only imports)
|
|
673
|
+
├── getDefaultImportProtectionRules
|
|
674
|
+
└── getMarkerSpecifiers
|
|
675
|
+
|
|
676
|
+
matchers.ts ← picomatch only
|
|
677
|
+
├── compileMatcher
|
|
678
|
+
├── compileMatchers
|
|
679
|
+
└── matchesAny
|
|
680
|
+
|
|
681
|
+
utils.ts ← vite + node:path + local constants
|
|
682
|
+
├── normalizeFilePath, stripViteQuery, ...
|
|
683
|
+
└── extractImportSources, dedupePatterns, ...
|
|
684
|
+
|
|
685
|
+
trace.ts ← imports from utils only
|
|
686
|
+
├── ImportGraph
|
|
687
|
+
├── buildTrace
|
|
688
|
+
└── formatViolation
|
|
689
|
+
|
|
690
|
+
virtualModules.ts ← imports ../utils (parent), ../constants (parent)
|
|
691
|
+
├── generateSelfContainedMockModule, generateDevSelfDenialModule
|
|
692
|
+
├── loadMockEdgeModule, loadMockRuntimeModule
|
|
693
|
+
└── MOCK_MODULE_ID, MOCK_EDGE_PREFIX, ...
|
|
694
|
+
|
|
695
|
+
plugin.ts ← main plugin, imports everything above
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
The `SERVER_FN_LOOKUP` constant lives in the shared parent `constants.ts`
|
|
699
|
+
(not in `start-compiler-plugin/plugin.ts`) to avoid pulling in
|
|
700
|
+
`@tanstack/start-server-core` when unit-testing import-protection modules.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { SERVER_FN_LOOKUP } from '../constants'
|
|
2
|
+
|
|
3
|
+
export const SERVER_FN_LOOKUP_QUERY = `?${SERVER_FN_LOOKUP}`
|
|
4
|
+
|
|
5
|
+
export const IMPORT_PROTECTION_DEBUG =
|
|
6
|
+
process.env.TSR_IMPORT_PROTECTION_DEBUG === '1' ||
|
|
7
|
+
process.env.TSR_IMPORT_PROTECTION_DEBUG === 'true'
|
|
8
|
+
|
|
9
|
+
export const IMPORT_PROTECTION_DEBUG_FILTER =
|
|
10
|
+
process.env.TSR_IMPORT_PROTECTION_DEBUG_FILTER
|
|
11
|
+
|
|
12
|
+
export const KNOWN_SOURCE_EXTENSIONS = new Set([
|
|
13
|
+
'.ts',
|
|
14
|
+
'.tsx',
|
|
15
|
+
'.mts',
|
|
16
|
+
'.cts',
|
|
17
|
+
'.js',
|
|
18
|
+
'.jsx',
|
|
19
|
+
'.mjs',
|
|
20
|
+
'.cjs',
|
|
21
|
+
'.json',
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
/** Vite's browser-visible prefix for virtual modules (replaces `\0`). */
|
|
25
|
+
export const VITE_BROWSER_VIRTUAL_PREFIX = '/@id/__x00__'
|