@tanstack/start-plugin-core 1.163.3 → 1.163.5
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
|
@@ -2,26 +2,37 @@ import { normalizePath } from 'vite'
|
|
|
2
2
|
|
|
3
3
|
import { resolveViteId } from '../utils'
|
|
4
4
|
import { VITE_ENVIRONMENT_NAMES } from '../constants'
|
|
5
|
-
import { SERVER_FN_LOOKUP } from '../start-compiler-plugin/plugin'
|
|
6
5
|
import { ImportGraph, buildTrace, formatViolation } from './trace'
|
|
7
6
|
import {
|
|
8
7
|
getDefaultImportProtectionRules,
|
|
9
8
|
getMarkerSpecifiers,
|
|
10
9
|
} from './defaults'
|
|
11
|
-
import { findPostCompileUsagePos } from './postCompileUsage'
|
|
12
10
|
import { compileMatchers, matchesAny } from './matchers'
|
|
13
11
|
import {
|
|
12
|
+
buildResolutionCandidates,
|
|
13
|
+
buildSourceCandidates,
|
|
14
|
+
canonicalizeResolvedId,
|
|
14
15
|
clearNormalizeFilePathCache,
|
|
16
|
+
debugLog,
|
|
15
17
|
dedupePatterns,
|
|
16
18
|
escapeRegExp,
|
|
17
19
|
extractImportSources,
|
|
18
20
|
getOrCreate,
|
|
21
|
+
isInsideDirectory,
|
|
22
|
+
matchesDebugFilter,
|
|
19
23
|
normalizeFilePath,
|
|
20
24
|
relativizePath,
|
|
25
|
+
shouldDeferViolation,
|
|
21
26
|
} from './utils'
|
|
22
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
collectMockExportNamesBySource,
|
|
29
|
+
collectNamedExports,
|
|
30
|
+
rewriteDeniedImports,
|
|
31
|
+
} from './rewriteDeniedImports'
|
|
23
32
|
import {
|
|
24
33
|
MOCK_BUILD_PREFIX,
|
|
34
|
+
generateDevSelfDenialModule,
|
|
35
|
+
generateSelfContainedMockModule,
|
|
25
36
|
getResolvedVirtualModuleMatchers,
|
|
26
37
|
loadResolvedVirtualModule,
|
|
27
38
|
makeMockEdgeModuleId,
|
|
@@ -29,12 +40,17 @@ import {
|
|
|
29
40
|
resolveInternalVirtualModuleId,
|
|
30
41
|
resolvedMarkerVirtualModuleId,
|
|
31
42
|
} from './virtualModules'
|
|
43
|
+
import { ExtensionlessAbsoluteIdResolver } from './extensionlessAbsoluteIdResolver'
|
|
44
|
+
import {
|
|
45
|
+
IMPORT_PROTECTION_DEBUG,
|
|
46
|
+
SERVER_FN_LOOKUP_QUERY,
|
|
47
|
+
VITE_BROWSER_VIRTUAL_PREFIX,
|
|
48
|
+
} from './constants'
|
|
32
49
|
import {
|
|
33
50
|
ImportLocCache,
|
|
34
51
|
addTraceImportLocations,
|
|
35
52
|
buildCodeSnippet,
|
|
36
53
|
buildLineIndex,
|
|
37
|
-
clearImportPatternCache,
|
|
38
54
|
findImportStatementLocationFromTransformed,
|
|
39
55
|
findPostCompileUsageLocation,
|
|
40
56
|
pickOriginalCodeFromSourcesContent,
|
|
@@ -47,201 +63,52 @@ import type {
|
|
|
47
63
|
TransformResult,
|
|
48
64
|
TransformResultProvider,
|
|
49
65
|
} from './sourceLocation'
|
|
66
|
+
import type { ImportProtectionOptions } from '../schema'
|
|
50
67
|
import type {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
function debugLog(...args: Array<unknown>) {
|
|
64
|
-
if (!IMPORT_PROTECTION_DEBUG) return
|
|
65
|
-
console.warn('[import-protection:debug]', ...args)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Check if a value matches the debug filter (when set). */
|
|
69
|
-
function matchesDebugFilter(...values: Array<string>): boolean {
|
|
70
|
-
if (!IMPORT_PROTECTION_DEBUG_FILTER) return true
|
|
71
|
-
return values.some((v) => v.includes(IMPORT_PROTECTION_DEBUG_FILTER))
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export { rewriteDeniedImports } from './rewriteDeniedImports'
|
|
75
|
-
export { dedupePatterns, extractImportSources } from './utils'
|
|
76
|
-
export type { Pattern } from './utils'
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Immutable plugin configuration — set once in `configResolved`, never mutated
|
|
80
|
-
* per-env or per-request afterward.
|
|
81
|
-
*/
|
|
82
|
-
interface PluginConfig {
|
|
83
|
-
enabled: boolean
|
|
84
|
-
root: string
|
|
85
|
-
command: 'build' | 'serve'
|
|
86
|
-
srcDirectory: string
|
|
87
|
-
framework: CompileStartFrameworkOptions
|
|
88
|
-
|
|
89
|
-
/** Absolute, query-free entry file ids used for trace roots. */
|
|
90
|
-
entryFiles: Array<string>
|
|
91
|
-
|
|
92
|
-
effectiveBehavior: ImportProtectionBehavior
|
|
93
|
-
mockAccess: 'error' | 'warn' | 'off'
|
|
94
|
-
logMode: 'once' | 'always'
|
|
95
|
-
maxTraceDepth: number
|
|
96
|
-
|
|
97
|
-
compiledRules: {
|
|
98
|
-
client: {
|
|
99
|
-
specifiers: Array<CompiledMatcher>
|
|
100
|
-
files: Array<CompiledMatcher>
|
|
101
|
-
excludeFiles: Array<CompiledMatcher>
|
|
102
|
-
}
|
|
103
|
-
server: {
|
|
104
|
-
specifiers: Array<CompiledMatcher>
|
|
105
|
-
files: Array<CompiledMatcher>
|
|
106
|
-
excludeFiles: Array<CompiledMatcher>
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
includeMatchers: Array<CompiledMatcher>
|
|
110
|
-
excludeMatchers: Array<CompiledMatcher>
|
|
111
|
-
ignoreImporterMatchers: Array<CompiledMatcher>
|
|
112
|
-
|
|
113
|
-
markerSpecifiers: { serverOnly: Set<string>; clientOnly: Set<string> }
|
|
114
|
-
envTypeMap: Map<string, 'client' | 'server'>
|
|
115
|
-
|
|
116
|
-
onViolation?: (
|
|
117
|
-
info: ViolationInfo,
|
|
118
|
-
) => boolean | void | Promise<boolean | void>
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Per-Vite-environment mutable state. One instance per environment name,
|
|
123
|
-
* stored in `envStates: Map<string, EnvState>`.
|
|
124
|
-
*
|
|
125
|
-
* All caches that previously lived on `PluginState` with `${envName}:` key
|
|
126
|
-
* prefixes now live here without any prefix.
|
|
127
|
-
*/
|
|
128
|
-
interface EnvState {
|
|
129
|
-
graph: ImportGraph
|
|
130
|
-
/** Specifiers that resolved to the mock module (for transform-time rewriting). */
|
|
131
|
-
deniedSources: Set<string>
|
|
132
|
-
/** Per-importer denied edges (for dev ESM mock modules). */
|
|
133
|
-
deniedEdges: Map<string, Set<string>>
|
|
134
|
-
/**
|
|
135
|
-
* During `vite dev` in mock mode, we generate a per-importer mock module that
|
|
136
|
-
* exports the names the importer expects.
|
|
137
|
-
* Populated in the transform hook (no disk reads).
|
|
138
|
-
*/
|
|
139
|
-
mockExportsByImporter: Map<string, Map<string, Array<string>>>
|
|
140
|
-
|
|
141
|
-
/** Resolve cache. Key: `${normalizedImporter}:${source}` (no env prefix). */
|
|
142
|
-
resolveCache: Map<string, string | null>
|
|
143
|
-
/** Reverse index: file path → Set of resolveCache keys involving that file. */
|
|
144
|
-
resolveCacheByFile: Map<string, Set<string>>
|
|
145
|
-
|
|
146
|
-
/** Import location cache. Key: `${importerFile}::${source}`. */
|
|
147
|
-
importLocCache: ImportLocCache
|
|
148
|
-
|
|
149
|
-
/** Deduplication of logged violations (no env prefix in key). */
|
|
150
|
-
seenViolations: Set<string>
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Modules transitively loaded during a `fetchModule(?SERVER_FN_LOOKUP)` call.
|
|
154
|
-
* In dev mode the compiler calls `fetchModule(id + '?' + SERVER_FN_LOOKUP)` to
|
|
155
|
-
* analyse a module's exports. The direct target carries the query parameter so
|
|
156
|
-
* `isPreTransformResolve` is `true`. But Vite also resolves the target's own
|
|
157
|
-
* imports (and their imports, etc.) with the plain file path as the importer —
|
|
158
|
-
* those would otherwise fire false-positive violations.
|
|
159
|
-
*
|
|
160
|
-
* We record every module resolved while walking a SERVER_FN_LOOKUP chain so
|
|
161
|
-
* that their child imports are also treated as pre-transform resolves.
|
|
162
|
-
*/
|
|
163
|
-
serverFnLookupModules: Set<string>
|
|
164
|
-
|
|
165
|
-
/** Transform result cache (code + composed sourcemap + original source). */
|
|
166
|
-
transformResultCache: Map<string, TransformResult>
|
|
167
|
-
/** Reverse index: physical file path → Set of transformResultCache keys. */
|
|
168
|
-
transformResultKeysByFile: Map<string, Set<string>>
|
|
169
|
-
|
|
170
|
-
/** Cached provider that reads from {@link transformResultCache}. */
|
|
171
|
-
transformResultProvider: TransformResultProvider
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Post-transform resolved imports. Populated by the transform-cache hook
|
|
175
|
-
* after resolving every import source found in the transformed code.
|
|
176
|
-
* Key: transform cache key (normalised module ID incl. query params).
|
|
177
|
-
* Value: set of resolved child file paths.
|
|
178
|
-
*/
|
|
179
|
-
postTransformImports: Map<string, Set<string>>
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Violations deferred in dev mock mode. Keyed by the violating importer's
|
|
183
|
-
* normalized file path. Violations are confirmed or discarded by the
|
|
184
|
-
* transform-cache hook once enough post-transform data is available to
|
|
185
|
-
* determine whether the importer is still reachable from an entry point.
|
|
186
|
-
*/
|
|
187
|
-
pendingViolations: Map<string, Array<PendingViolation>>
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Violations deferred in build mode (both mock and error). Each gets a
|
|
191
|
-
* unique mock module ID so we can check which ones survived tree-shaking
|
|
192
|
-
* in `generateBundle`.
|
|
193
|
-
*/
|
|
194
|
-
deferredBuildViolations: Array<DeferredBuildViolation>
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
interface PendingViolation {
|
|
198
|
-
info: ViolationInfo
|
|
199
|
-
/** The mock module ID that resolveId already returned for this violation. */
|
|
200
|
-
mockReturnValue: string
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
interface DeferredBuildViolation {
|
|
204
|
-
info: ViolationInfo
|
|
205
|
-
/** Unique mock module ID assigned to this violation. */
|
|
206
|
-
mockModuleId: string
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Module ID to check for tree-shaking survival in `generateBundle`.
|
|
210
|
-
*
|
|
211
|
-
* For most violations we check the unique mock module ID.
|
|
212
|
-
* For `marker` violations the import is a bare side-effect import that often
|
|
213
|
-
* gets optimized away regardless of whether the importer survives, so we
|
|
214
|
-
* instead check whether the importer module itself survived.
|
|
215
|
-
*/
|
|
216
|
-
checkModuleId?: string
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Intentionally cross-env shared mutable state.
|
|
221
|
-
*
|
|
222
|
-
* A file's `'use server'`/`'use client'` directive is inherent to the file
|
|
223
|
-
* content, not the environment that happens to discover it first.
|
|
224
|
-
*/
|
|
225
|
-
interface SharedState {
|
|
226
|
-
fileMarkerKind: Map<string, 'server' | 'client'>
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
export interface ImportProtectionPluginOptions {
|
|
230
|
-
getConfig: GetConfigFn
|
|
231
|
-
framework: CompileStartFrameworkOptions
|
|
232
|
-
environments: Array<{ name: string; type: 'client' | 'server' }>
|
|
233
|
-
providerEnvName: string
|
|
234
|
-
}
|
|
68
|
+
EnvRules,
|
|
69
|
+
EnvState,
|
|
70
|
+
HandleViolationResult,
|
|
71
|
+
ImportProtectionPluginOptions,
|
|
72
|
+
ModuleGraphNode,
|
|
73
|
+
PendingViolation,
|
|
74
|
+
PluginConfig,
|
|
75
|
+
SharedState,
|
|
76
|
+
ViolationReporter,
|
|
77
|
+
} from './types'
|
|
78
|
+
|
|
79
|
+
export type { ImportProtectionPluginOptions } from './types'
|
|
235
80
|
|
|
236
81
|
export function importProtectionPlugin(
|
|
237
82
|
opts: ImportProtectionPluginOptions,
|
|
238
83
|
): PluginOption {
|
|
239
84
|
let devServer: ViteDevServer | null = null
|
|
85
|
+
const extensionlessIdResolver = new ExtensionlessAbsoluteIdResolver()
|
|
86
|
+
const resolveExtensionlessAbsoluteId = (id: string) =>
|
|
87
|
+
extensionlessIdResolver.resolve(id)
|
|
88
|
+
|
|
89
|
+
const importPatternCache = new Map<string, Array<RegExp>>()
|
|
90
|
+
|
|
91
|
+
function findFirstImportSpecifierIndex(code: string, source: string): number {
|
|
92
|
+
let patterns = importPatternCache.get(source)
|
|
93
|
+
if (!patterns) {
|
|
94
|
+
const escaped = escapeRegExp(source)
|
|
95
|
+
patterns = [
|
|
96
|
+
new RegExp(`\\bimport\\s+(['"])${escaped}\\1`),
|
|
97
|
+
new RegExp(`\\bfrom\\s+(['"])${escaped}\\1`),
|
|
98
|
+
new RegExp(`\\bimport\\s*\\(\\s*(['"])${escaped}\\1\\s*\\)`),
|
|
99
|
+
]
|
|
100
|
+
importPatternCache.set(source, patterns)
|
|
101
|
+
}
|
|
240
102
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
103
|
+
let best = -1
|
|
104
|
+
for (const re of patterns) {
|
|
105
|
+
const m = re.exec(code)
|
|
106
|
+
if (!m) continue
|
|
107
|
+
const idx = m.index + m[0].indexOf(source)
|
|
108
|
+
if (idx === -1) continue
|
|
109
|
+
if (best === -1 || idx < best) best = idx
|
|
110
|
+
}
|
|
111
|
+
return best
|
|
245
112
|
}
|
|
246
113
|
|
|
247
114
|
/**
|
|
@@ -344,7 +211,6 @@ export function importProtectionPlugin(
|
|
|
344
211
|
command: 'build',
|
|
345
212
|
srcDirectory: '',
|
|
346
213
|
framework: opts.framework,
|
|
347
|
-
entryFiles: [],
|
|
348
214
|
effectiveBehavior: 'error',
|
|
349
215
|
mockAccess: 'error',
|
|
350
216
|
logMode: 'once',
|
|
@@ -364,18 +230,6 @@ export function importProtectionPlugin(
|
|
|
364
230
|
const envStates = new Map<string, EnvState>()
|
|
365
231
|
const shared: SharedState = { fileMarkerKind: new Map() }
|
|
366
232
|
|
|
367
|
-
function getMarkerKindForFile(
|
|
368
|
-
fileId: string,
|
|
369
|
-
): 'server' | 'client' | undefined {
|
|
370
|
-
const file = normalizeFilePath(fileId)
|
|
371
|
-
return shared.fileMarkerKind.get(file)
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
type ViolationReporter = {
|
|
375
|
-
warn: (msg: string) => void
|
|
376
|
-
error: (msg: string) => never
|
|
377
|
-
}
|
|
378
|
-
|
|
379
233
|
/**
|
|
380
234
|
* Build the best available trace for a module and enrich each step with
|
|
381
235
|
* line/column locations. Tries the plugin's own ImportGraph first, then
|
|
@@ -407,7 +261,12 @@ export function importProtectionPlugin(
|
|
|
407
261
|
trace = mgTrace
|
|
408
262
|
}
|
|
409
263
|
}
|
|
410
|
-
await addTraceImportLocations(
|
|
264
|
+
await addTraceImportLocations(
|
|
265
|
+
provider,
|
|
266
|
+
trace,
|
|
267
|
+
env.importLocCache,
|
|
268
|
+
findFirstImportSpecifierIndex,
|
|
269
|
+
)
|
|
411
270
|
|
|
412
271
|
if (trace.length > 0) {
|
|
413
272
|
const last = trace[trace.length - 1]!
|
|
@@ -448,19 +307,20 @@ export function importProtectionPlugin(
|
|
|
448
307
|
>,
|
|
449
308
|
traceOverride?: Array<TraceStep>,
|
|
450
309
|
): Promise<ViolationInfo> {
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
310
|
+
const sourceCandidates = buildSourceCandidates(
|
|
311
|
+
source,
|
|
312
|
+
'resolved' in overrides && typeof overrides.resolved === 'string'
|
|
313
|
+
? overrides.resolved
|
|
314
|
+
: undefined,
|
|
315
|
+
config.root,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
const loc = await resolveImporterLocation(
|
|
319
|
+
provider,
|
|
320
|
+
env,
|
|
321
|
+
importer,
|
|
322
|
+
sourceCandidates,
|
|
323
|
+
)
|
|
464
324
|
|
|
465
325
|
const trace = await rebuildAndAnnotateTrace(
|
|
466
326
|
provider,
|
|
@@ -487,6 +347,27 @@ export function importProtectionPlugin(
|
|
|
487
347
|
}
|
|
488
348
|
}
|
|
489
349
|
|
|
350
|
+
async function resolveImporterLocation(
|
|
351
|
+
provider: TransformResultProvider,
|
|
352
|
+
env: EnvState,
|
|
353
|
+
importer: string,
|
|
354
|
+
sourceCandidates: Iterable<string>,
|
|
355
|
+
): Promise<Loc | undefined> {
|
|
356
|
+
for (const candidate of sourceCandidates) {
|
|
357
|
+
const loc =
|
|
358
|
+
(await findPostCompileUsageLocation(provider, importer, candidate)) ||
|
|
359
|
+
(await findImportStatementLocationFromTransformed(
|
|
360
|
+
provider,
|
|
361
|
+
importer,
|
|
362
|
+
candidate,
|
|
363
|
+
env.importLocCache,
|
|
364
|
+
findFirstImportSpecifierIndex,
|
|
365
|
+
))
|
|
366
|
+
if (loc) return loc
|
|
367
|
+
}
|
|
368
|
+
return undefined
|
|
369
|
+
}
|
|
370
|
+
|
|
490
371
|
/**
|
|
491
372
|
* Check if a resolved import violates marker restrictions (e.g. importing
|
|
492
373
|
* a server-only module in the client env). If so, build and return the
|
|
@@ -505,7 +386,8 @@ export function importProtectionPlugin(
|
|
|
505
386
|
relativePath: string,
|
|
506
387
|
traceOverride?: Array<TraceStep>,
|
|
507
388
|
): Promise<ViolationInfo | undefined> {
|
|
508
|
-
const
|
|
389
|
+
const normalizedResolvedId = normalizeFilePath(resolvedId)
|
|
390
|
+
const markerKind = shared.fileMarkerKind.get(normalizedResolvedId)
|
|
509
391
|
const violates =
|
|
510
392
|
(envType === 'client' && markerKind === 'server') ||
|
|
511
393
|
(envType === 'server' && markerKind === 'client')
|
|
@@ -523,11 +405,49 @@ export function importProtectionPlugin(
|
|
|
523
405
|
source,
|
|
524
406
|
{
|
|
525
407
|
type: 'marker',
|
|
526
|
-
resolved:
|
|
527
|
-
message:
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
408
|
+
resolved: normalizedResolvedId,
|
|
409
|
+
message: buildMarkerViolationMessage(relativePath, markerKind),
|
|
410
|
+
},
|
|
411
|
+
traceOverride,
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function buildMarkerViolationMessage(
|
|
416
|
+
relativePath: string,
|
|
417
|
+
markerKind: 'server' | 'client' | undefined,
|
|
418
|
+
): string {
|
|
419
|
+
return markerKind === 'server'
|
|
420
|
+
? `Module "${relativePath}" is marked server-only but is imported in the client environment`
|
|
421
|
+
: `Module "${relativePath}" is marked client-only but is imported in the server environment`
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function buildFileViolationInfo(
|
|
425
|
+
provider: TransformResultProvider,
|
|
426
|
+
env: EnvState,
|
|
427
|
+
envName: string,
|
|
428
|
+
envType: 'client' | 'server',
|
|
429
|
+
importer: string,
|
|
430
|
+
normalizedImporter: string,
|
|
431
|
+
source: string,
|
|
432
|
+
resolvedPath: string,
|
|
433
|
+
pattern: string | RegExp,
|
|
434
|
+
traceOverride?: Array<TraceStep>,
|
|
435
|
+
): Promise<ViolationInfo> {
|
|
436
|
+
const relativePath = getRelativePath(resolvedPath)
|
|
437
|
+
|
|
438
|
+
return buildViolationInfo(
|
|
439
|
+
provider,
|
|
440
|
+
env,
|
|
441
|
+
envName,
|
|
442
|
+
envType,
|
|
443
|
+
importer,
|
|
444
|
+
normalizedImporter,
|
|
445
|
+
source,
|
|
446
|
+
{
|
|
447
|
+
type: 'file',
|
|
448
|
+
pattern,
|
|
449
|
+
resolved: resolvedPath,
|
|
450
|
+
message: `Import "${source}" (resolved to "${relativePath}") is denied in the ${envType} environment`,
|
|
531
451
|
},
|
|
532
452
|
traceOverride,
|
|
533
453
|
)
|
|
@@ -537,17 +457,36 @@ export function importProtectionPlugin(
|
|
|
537
457
|
return config.envTypeMap.get(envName) ?? 'server'
|
|
538
458
|
}
|
|
539
459
|
|
|
540
|
-
function getRulesForEnvironment(envName: string): {
|
|
541
|
-
specifiers: Array<CompiledMatcher>
|
|
542
|
-
files: Array<CompiledMatcher>
|
|
543
|
-
excludeFiles: Array<CompiledMatcher>
|
|
544
|
-
} {
|
|
460
|
+
function getRulesForEnvironment(envName: string): EnvRules {
|
|
545
461
|
const type = getEnvType(envName)
|
|
546
462
|
return type === 'client'
|
|
547
463
|
? config.compiledRules.client
|
|
548
464
|
: config.compiledRules.server
|
|
549
465
|
}
|
|
550
466
|
|
|
467
|
+
/**
|
|
468
|
+
* Check if a relative path matches any denied file pattern for the given
|
|
469
|
+
* environment, respecting `excludeFiles`. Returns the matching pattern
|
|
470
|
+
* or `undefined` if the file is not denied.
|
|
471
|
+
*/
|
|
472
|
+
function checkFileDenial(
|
|
473
|
+
relativePath: string,
|
|
474
|
+
matchers: {
|
|
475
|
+
files: Array<CompiledMatcher>
|
|
476
|
+
excludeFiles: Array<CompiledMatcher>
|
|
477
|
+
},
|
|
478
|
+
): CompiledMatcher | undefined {
|
|
479
|
+
if (
|
|
480
|
+
matchers.excludeFiles.length > 0 &&
|
|
481
|
+
matchesAny(relativePath, matchers.excludeFiles)
|
|
482
|
+
) {
|
|
483
|
+
return undefined
|
|
484
|
+
}
|
|
485
|
+
return matchers.files.length > 0
|
|
486
|
+
? matchesAny(relativePath, matchers.files)
|
|
487
|
+
: undefined
|
|
488
|
+
}
|
|
489
|
+
|
|
551
490
|
const environmentNames = new Set<string>([
|
|
552
491
|
VITE_ENVIRONMENT_NAMES.client,
|
|
553
492
|
VITE_ENVIRONMENT_NAMES.server,
|
|
@@ -563,8 +502,6 @@ export function importProtectionPlugin(
|
|
|
563
502
|
const transformResultCache = new Map<string, TransformResult>()
|
|
564
503
|
envState = {
|
|
565
504
|
graph: new ImportGraph(),
|
|
566
|
-
deniedSources: new Set(),
|
|
567
|
-
deniedEdges: new Map(),
|
|
568
505
|
mockExportsByImporter: new Map(),
|
|
569
506
|
resolveCache: new Map(),
|
|
570
507
|
resolveCacheByFile: new Map(),
|
|
@@ -593,6 +530,186 @@ export function importProtectionPlugin(
|
|
|
593
530
|
return envState
|
|
594
531
|
}
|
|
595
532
|
|
|
533
|
+
/**
|
|
534
|
+
* Search a parsed export-names map for an entry matching any of the
|
|
535
|
+
* specifier candidates. Returns matching names or empty array.
|
|
536
|
+
*/
|
|
537
|
+
function findExportsInMap(
|
|
538
|
+
exportMap: Map<string, Array<string>>,
|
|
539
|
+
candidates: Array<string>,
|
|
540
|
+
): Array<string> {
|
|
541
|
+
for (const candidate of candidates) {
|
|
542
|
+
const hit = exportMap.get(candidate)
|
|
543
|
+
if (hit && hit.length > 0) return hit
|
|
544
|
+
}
|
|
545
|
+
return []
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Build deduped resolution candidates for a module ID, including the
|
|
550
|
+
* extensionless absolute path when the ID looks like a file path.
|
|
551
|
+
*/
|
|
552
|
+
function buildIdCandidates(id: string, extra?: string): Array<string> {
|
|
553
|
+
const set = new Set(buildResolutionCandidates(id))
|
|
554
|
+
if (extra) {
|
|
555
|
+
for (const c of buildResolutionCandidates(extra)) set.add(c)
|
|
556
|
+
set.add(resolveExtensionlessAbsoluteId(extra))
|
|
557
|
+
}
|
|
558
|
+
return Array.from(set)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Resolve which named exports the importer needs from a denied specifier,
|
|
563
|
+
* so mock-edge modules can provide explicit ESM named exports.
|
|
564
|
+
*
|
|
565
|
+
* Tries multiple strategies: cached export maps, AST parsing, and
|
|
566
|
+
* resolver-based comparison.
|
|
567
|
+
*/
|
|
568
|
+
async function resolveExportsForDeniedSpecifier(
|
|
569
|
+
env: EnvState,
|
|
570
|
+
ctx: ViolationReporter,
|
|
571
|
+
info: ViolationInfo,
|
|
572
|
+
importerIdHint?: string,
|
|
573
|
+
): Promise<Array<string>> {
|
|
574
|
+
const importerFile = normalizeFilePath(info.importer)
|
|
575
|
+
const specifierCandidates = buildIdCandidates(info.specifier, info.resolved)
|
|
576
|
+
|
|
577
|
+
// Only parse AST when a violation occurs (this function is only called
|
|
578
|
+
// while handling a violation). Cache per-importer to avoid repeated parses
|
|
579
|
+
// across multiple violations.
|
|
580
|
+
let parsedBySource = env.mockExportsByImporter.get(importerFile)
|
|
581
|
+
if (!parsedBySource) {
|
|
582
|
+
// Try transform-cache result first, then moduleInfo fallback.
|
|
583
|
+
const importerCode =
|
|
584
|
+
env.transformResultProvider.getTransformResult(importerFile)?.code ??
|
|
585
|
+
(importerIdHint && ctx.getModuleInfo
|
|
586
|
+
? (ctx.getModuleInfo(importerIdHint)?.code ?? undefined)
|
|
587
|
+
: undefined)
|
|
588
|
+
if (typeof importerCode !== 'string' || importerCode.length === 0)
|
|
589
|
+
return []
|
|
590
|
+
|
|
591
|
+
try {
|
|
592
|
+
parsedBySource = collectMockExportNamesBySource(importerCode)
|
|
593
|
+
|
|
594
|
+
// Also index by resolved physical IDs so later lookups match.
|
|
595
|
+
await recordMockExportsForImporter(
|
|
596
|
+
env,
|
|
597
|
+
importerFile,
|
|
598
|
+
parsedBySource,
|
|
599
|
+
async (src) => {
|
|
600
|
+
const cacheKey = `${importerFile}:${src}`
|
|
601
|
+
if (env.resolveCache.has(cacheKey)) {
|
|
602
|
+
return env.resolveCache.get(cacheKey) ?? undefined
|
|
603
|
+
}
|
|
604
|
+
if (!ctx.resolve) return undefined
|
|
605
|
+
const resolved = await ctx.resolve(src, info.importer, {
|
|
606
|
+
skipSelf: true,
|
|
607
|
+
})
|
|
608
|
+
if (!resolved || resolved.external) return undefined
|
|
609
|
+
return resolved.id
|
|
610
|
+
},
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
// Keep the parsed-by-source map for direct lookups.
|
|
614
|
+
parsedBySource =
|
|
615
|
+
env.mockExportsByImporter.get(importerFile) ?? parsedBySource
|
|
616
|
+
} catch {
|
|
617
|
+
return []
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// 1. Direct candidate match
|
|
622
|
+
const direct = findExportsInMap(parsedBySource, specifierCandidates)
|
|
623
|
+
if (direct.length > 0) return direct
|
|
624
|
+
|
|
625
|
+
// 2. Resolve each source key and compare candidates.
|
|
626
|
+
const candidateSet = new Set(specifierCandidates)
|
|
627
|
+
for (const [sourceKey, names] of parsedBySource) {
|
|
628
|
+
if (!names.length) continue
|
|
629
|
+
|
|
630
|
+
const resolvedId = await resolveSourceKey(
|
|
631
|
+
env,
|
|
632
|
+
ctx,
|
|
633
|
+
importerFile,
|
|
634
|
+
sourceKey,
|
|
635
|
+
info.importer,
|
|
636
|
+
)
|
|
637
|
+
if (!resolvedId) continue
|
|
638
|
+
|
|
639
|
+
const resolvedCandidates = buildIdCandidates(resolvedId)
|
|
640
|
+
resolvedCandidates.push(resolveExtensionlessAbsoluteId(resolvedId))
|
|
641
|
+
if (resolvedCandidates.some((v) => candidateSet.has(v))) {
|
|
642
|
+
return names
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return []
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** Best-effort resolve a source key using the cache or ctx.resolve. */
|
|
650
|
+
async function resolveSourceKey(
|
|
651
|
+
env: EnvState,
|
|
652
|
+
ctx: ViolationReporter,
|
|
653
|
+
importerFile: string,
|
|
654
|
+
sourceKey: string,
|
|
655
|
+
importerId: string,
|
|
656
|
+
): Promise<string | undefined> {
|
|
657
|
+
const cacheKey = `${importerFile}:${sourceKey}`
|
|
658
|
+
if (env.resolveCache.has(cacheKey)) {
|
|
659
|
+
return env.resolveCache.get(cacheKey) ?? undefined
|
|
660
|
+
}
|
|
661
|
+
if (!ctx.resolve) return undefined
|
|
662
|
+
try {
|
|
663
|
+
const resolved = await ctx.resolve(sourceKey, importerId, {
|
|
664
|
+
skipSelf: true,
|
|
665
|
+
})
|
|
666
|
+
if (!resolved || resolved.external) return undefined
|
|
667
|
+
return resolved.id
|
|
668
|
+
} catch {
|
|
669
|
+
return undefined
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
async function recordMockExportsForImporter(
|
|
674
|
+
env: EnvState,
|
|
675
|
+
importerId: string,
|
|
676
|
+
namesBySource: Map<string, Array<string>>,
|
|
677
|
+
resolveSource: (source: string) => Promise<string | undefined>,
|
|
678
|
+
): Promise<void> {
|
|
679
|
+
const importerFile = normalizeFilePath(importerId)
|
|
680
|
+
|
|
681
|
+
if (namesBySource.size === 0) return
|
|
682
|
+
|
|
683
|
+
for (const [source, names] of namesBySource) {
|
|
684
|
+
try {
|
|
685
|
+
const resolvedId = await resolveSource(source)
|
|
686
|
+
if (!resolvedId) continue
|
|
687
|
+
|
|
688
|
+
namesBySource.set(normalizeFilePath(resolvedId), names)
|
|
689
|
+
namesBySource.set(resolveExtensionlessAbsoluteId(resolvedId), names)
|
|
690
|
+
} catch {
|
|
691
|
+
// Best-effort only
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const existing = env.mockExportsByImporter.get(importerFile)
|
|
696
|
+
if (!existing) {
|
|
697
|
+
env.mockExportsByImporter.set(importerFile, namesBySource)
|
|
698
|
+
return
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
for (const [source, names] of namesBySource) {
|
|
702
|
+
const prev = existing.get(source)
|
|
703
|
+
if (!prev) {
|
|
704
|
+
existing.set(source, names)
|
|
705
|
+
continue
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const union = new Set([...prev, ...names])
|
|
709
|
+
existing.set(source, Array.from(union).sort())
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
596
713
|
const shouldCheckImporterCache = new Map<string, boolean>()
|
|
597
714
|
function shouldCheckImporter(importer: string): boolean {
|
|
598
715
|
let result = shouldCheckImporterCache.get(importer)
|
|
@@ -600,20 +717,19 @@ export function importProtectionPlugin(
|
|
|
600
717
|
|
|
601
718
|
const relativePath = relativizePath(importer, config.root)
|
|
602
719
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
) {
|
|
720
|
+
// Excluded or ignored importers are never checked.
|
|
721
|
+
const excluded =
|
|
722
|
+
(config.excludeMatchers.length > 0 &&
|
|
723
|
+
matchesAny(relativePath, config.excludeMatchers)) ||
|
|
724
|
+
(config.ignoreImporterMatchers.length > 0 &&
|
|
725
|
+
matchesAny(relativePath, config.ignoreImporterMatchers))
|
|
726
|
+
|
|
727
|
+
if (excluded) {
|
|
612
728
|
result = false
|
|
613
729
|
} else if (config.includeMatchers.length > 0) {
|
|
614
730
|
result = !!matchesAny(relativePath, config.includeMatchers)
|
|
615
731
|
} else if (config.srcDirectory) {
|
|
616
|
-
result = importer
|
|
732
|
+
result = isInsideDirectory(importer, config.srcDirectory)
|
|
617
733
|
} else {
|
|
618
734
|
result = true
|
|
619
735
|
}
|
|
@@ -622,13 +738,8 @@ export function importProtectionPlugin(
|
|
|
622
738
|
return result
|
|
623
739
|
}
|
|
624
740
|
|
|
625
|
-
function dedupeKey(
|
|
626
|
-
type
|
|
627
|
-
importer: string,
|
|
628
|
-
specifier: string,
|
|
629
|
-
resolved?: string,
|
|
630
|
-
): string {
|
|
631
|
-
return `${type}:${importer}:${specifier}:${resolved ?? ''}`
|
|
741
|
+
function dedupeKey(info: ViolationInfo): string {
|
|
742
|
+
return `${info.type}:${info.importer}:${info.specifier}:${info.resolved ?? ''}`
|
|
632
743
|
}
|
|
633
744
|
|
|
634
745
|
function hasSeen(env: EnvState, key: string): boolean {
|
|
@@ -642,6 +753,72 @@ export function importProtectionPlugin(
|
|
|
642
753
|
return relativizePath(normalizePath(absolutePath), config.root)
|
|
643
754
|
}
|
|
644
755
|
|
|
756
|
+
/** Reset all caches on an EnvState (called from buildStart). */
|
|
757
|
+
function clearEnvState(envState: EnvState): void {
|
|
758
|
+
envState.resolveCache.clear()
|
|
759
|
+
envState.resolveCacheByFile.clear()
|
|
760
|
+
envState.importLocCache.clear()
|
|
761
|
+
envState.seenViolations.clear()
|
|
762
|
+
envState.transformResultCache.clear()
|
|
763
|
+
envState.transformResultKeysByFile.clear()
|
|
764
|
+
envState.postTransformImports.clear()
|
|
765
|
+
envState.serverFnLookupModules.clear()
|
|
766
|
+
envState.pendingViolations.clear()
|
|
767
|
+
envState.deferredBuildViolations.length = 0
|
|
768
|
+
envState.graph.clear()
|
|
769
|
+
envState.mockExportsByImporter.clear()
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/** Invalidate all env-level caches that reference a specific file. */
|
|
773
|
+
function invalidateFileFromEnv(envState: EnvState, file: string): void {
|
|
774
|
+
envState.importLocCache.deleteByFile(file)
|
|
775
|
+
|
|
776
|
+
// Resolve cache (keyed "importer:source")
|
|
777
|
+
const resolveKeys = envState.resolveCacheByFile.get(file)
|
|
778
|
+
if (resolveKeys) {
|
|
779
|
+
for (const key of resolveKeys) envState.resolveCache.delete(key)
|
|
780
|
+
envState.resolveCacheByFile.delete(file)
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
envState.graph.invalidate(file)
|
|
784
|
+
envState.mockExportsByImporter.delete(file)
|
|
785
|
+
envState.serverFnLookupModules.delete(file)
|
|
786
|
+
envState.pendingViolations.delete(file)
|
|
787
|
+
|
|
788
|
+
// Transform result cache + post-transform imports
|
|
789
|
+
const transformKeys = envState.transformResultKeysByFile.get(file)
|
|
790
|
+
if (transformKeys) {
|
|
791
|
+
for (const key of transformKeys) {
|
|
792
|
+
envState.transformResultCache.delete(key)
|
|
793
|
+
envState.postTransformImports.delete(key)
|
|
794
|
+
}
|
|
795
|
+
envState.transformResultKeysByFile.delete(file)
|
|
796
|
+
} else {
|
|
797
|
+
envState.transformResultCache.delete(file)
|
|
798
|
+
envState.postTransformImports.delete(file)
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/** Store a transform result under both the cacheKey and physical file path. */
|
|
803
|
+
function cacheTransformResult(
|
|
804
|
+
envState: EnvState,
|
|
805
|
+
file: string,
|
|
806
|
+
cacheKey: string,
|
|
807
|
+
result: TransformResult,
|
|
808
|
+
): void {
|
|
809
|
+
envState.transformResultCache.set(cacheKey, result)
|
|
810
|
+
const keySet = getOrCreate(
|
|
811
|
+
envState.transformResultKeysByFile,
|
|
812
|
+
file,
|
|
813
|
+
() => new Set<string>(),
|
|
814
|
+
)
|
|
815
|
+
keySet.add(cacheKey)
|
|
816
|
+
if (cacheKey !== file) {
|
|
817
|
+
envState.transformResultCache.set(file, result)
|
|
818
|
+
keySet.add(file)
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
645
822
|
/** Register known Start entrypoints as trace roots for all environments. */
|
|
646
823
|
function registerEntries(): void {
|
|
647
824
|
const { resolvedStartConfig } = opts.getConfig()
|
|
@@ -660,6 +837,82 @@ export function importProtectionPlugin(
|
|
|
660
837
|
}
|
|
661
838
|
}
|
|
662
839
|
|
|
840
|
+
/**
|
|
841
|
+
* Get the merged set of post-transform imports for a file, checking all
|
|
842
|
+
* code-split variants. Returns `null` if no post-transform data exists
|
|
843
|
+
* yet (transform hasn't run).
|
|
844
|
+
*
|
|
845
|
+
* Skips `SERVER_FN_LOOKUP` variants because they contain untransformed
|
|
846
|
+
* code — the Start compiler excludes them.
|
|
847
|
+
*/
|
|
848
|
+
function getPostTransformImports(
|
|
849
|
+
env: EnvState,
|
|
850
|
+
file: string,
|
|
851
|
+
): Set<string> | null {
|
|
852
|
+
const keySet = env.transformResultKeysByFile.get(file)
|
|
853
|
+
let merged: Set<string> | null = null
|
|
854
|
+
|
|
855
|
+
if (keySet) {
|
|
856
|
+
for (const k of keySet) {
|
|
857
|
+
if (k.includes(SERVER_FN_LOOKUP_QUERY)) continue
|
|
858
|
+
const imports = env.postTransformImports.get(k)
|
|
859
|
+
if (imports) {
|
|
860
|
+
if (!merged) merged = new Set(imports)
|
|
861
|
+
else for (const v of imports) merged.add(v)
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Fallback: direct file-path key
|
|
867
|
+
if (!merged) {
|
|
868
|
+
const imports = env.postTransformImports.get(file)
|
|
869
|
+
if (imports) merged = new Set(imports)
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
return merged
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Check whether an import edge from `parent` to `target` survived
|
|
877
|
+
* post-transform compilation.
|
|
878
|
+
*
|
|
879
|
+
* Returns:
|
|
880
|
+
* - `'live'` — target appears in a non-lookup variant's post-transform imports
|
|
881
|
+
* - `'dead'` — post-transform data exists but target is absent (compiler stripped it)
|
|
882
|
+
* - `'pending'` — transform ran but import data not yet posted
|
|
883
|
+
* - `'no-data'` — transform never ran (warm-start cached module)
|
|
884
|
+
*/
|
|
885
|
+
function checkEdgeLiveness(
|
|
886
|
+
env: EnvState,
|
|
887
|
+
parent: string,
|
|
888
|
+
target: string,
|
|
889
|
+
): 'live' | 'dead' | 'pending' | 'no-data' {
|
|
890
|
+
const keySet = env.transformResultKeysByFile.get(parent)
|
|
891
|
+
let anyVariantCached = false
|
|
892
|
+
|
|
893
|
+
if (keySet) {
|
|
894
|
+
for (const k of keySet) {
|
|
895
|
+
if (k.includes(SERVER_FN_LOOKUP_QUERY)) continue
|
|
896
|
+
const imports = env.postTransformImports.get(k)
|
|
897
|
+
if (imports) {
|
|
898
|
+
anyVariantCached = true
|
|
899
|
+
if (imports.has(target)) return 'live'
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
if (!anyVariantCached) {
|
|
905
|
+
const imports = env.postTransformImports.get(parent)
|
|
906
|
+
if (imports) return imports.has(target) ? 'live' : 'dead'
|
|
907
|
+
const hasTransformResult =
|
|
908
|
+
env.transformResultCache.has(parent) ||
|
|
909
|
+
(keySet ? keySet.size > 0 : false)
|
|
910
|
+
return hasTransformResult ? 'pending' : 'no-data'
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
return 'dead'
|
|
914
|
+
}
|
|
915
|
+
|
|
663
916
|
function checkPostTransformReachability(
|
|
664
917
|
env: EnvState,
|
|
665
918
|
file: string,
|
|
@@ -678,73 +931,60 @@ export function importProtectionPlugin(
|
|
|
678
931
|
return 'reachable'
|
|
679
932
|
}
|
|
680
933
|
|
|
681
|
-
// Walk reverse edges
|
|
682
934
|
const importers = env.graph.reverseEdges.get(current)
|
|
683
935
|
if (!importers) continue
|
|
684
936
|
|
|
685
937
|
for (const [parent] of importers) {
|
|
686
938
|
if (visited.has(parent)) continue
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
// Skip SERVER_FN_LOOKUP variants — they contain untransformed
|
|
691
|
-
// code (the compiler excludes them), so their import lists
|
|
692
|
-
// include imports that the compiler would normally strip.
|
|
693
|
-
const keySet = env.transformResultKeysByFile.get(parent)
|
|
694
|
-
let anyVariantCached = false
|
|
695
|
-
let edgeLive = false
|
|
696
|
-
|
|
697
|
-
if (keySet) {
|
|
698
|
-
for (const k of keySet) {
|
|
699
|
-
if (k.includes(SERVER_FN_LOOKUP_QUERY)) continue
|
|
700
|
-
const resolvedImports = env.postTransformImports.get(k)
|
|
701
|
-
if (resolvedImports) {
|
|
702
|
-
anyVariantCached = true
|
|
703
|
-
if (resolvedImports.has(current)) {
|
|
704
|
-
edgeLive = true
|
|
705
|
-
break
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
// Fallback: direct file-path key
|
|
712
|
-
if (!anyVariantCached) {
|
|
713
|
-
const resolvedImports = env.postTransformImports.get(parent)
|
|
714
|
-
if (resolvedImports) {
|
|
715
|
-
anyVariantCached = true
|
|
716
|
-
if (resolvedImports.has(current)) {
|
|
717
|
-
edgeLive = true
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
if (!anyVariantCached) {
|
|
723
|
-
const hasTransformResult =
|
|
724
|
-
env.transformResultCache.has(parent) ||
|
|
725
|
-
(keySet ? keySet.size > 0 : false)
|
|
726
|
-
|
|
727
|
-
if (hasTransformResult) {
|
|
728
|
-
// Transform ran but postTransformImports not yet populated
|
|
729
|
-
hasUnknownEdge = true
|
|
730
|
-
continue
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
// Transform never ran — Vite served from cache (warm start).
|
|
734
|
-
// Conservatively treat edge as live.
|
|
735
|
-
queue.push(parent)
|
|
736
|
-
continue
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
if (edgeLive) {
|
|
939
|
+
const liveness = checkEdgeLiveness(env, parent, current)
|
|
940
|
+
if (liveness === 'live' || liveness === 'no-data') {
|
|
941
|
+
// Live edge or warm-start (no transform data) — follow it
|
|
740
942
|
queue.push(parent)
|
|
943
|
+
} else if (liveness === 'pending') {
|
|
944
|
+
hasUnknownEdge = true
|
|
741
945
|
}
|
|
946
|
+
// 'dead' — edge was stripped by compiler, skip
|
|
742
947
|
}
|
|
743
948
|
}
|
|
744
949
|
|
|
745
950
|
return hasUnknownEdge ? 'unknown' : 'unreachable'
|
|
746
951
|
}
|
|
747
952
|
|
|
953
|
+
/**
|
|
954
|
+
* Filter pending violations using edge-survival data. Returns the subset
|
|
955
|
+
* of violations whose resolved import survived the Start compiler (or all
|
|
956
|
+
* violations when no post-transform data is available yet).
|
|
957
|
+
*
|
|
958
|
+
* Returns `undefined` when all violations were stripped or when we must wait
|
|
959
|
+
* for post-transform data before proceeding.
|
|
960
|
+
*/
|
|
961
|
+
function filterEdgeSurvival(
|
|
962
|
+
env: EnvState,
|
|
963
|
+
file: string,
|
|
964
|
+
violations: Array<PendingViolation>,
|
|
965
|
+
):
|
|
966
|
+
| { active: Array<PendingViolation>; edgeSurvivalApplied: boolean }
|
|
967
|
+
| 'all-stripped'
|
|
968
|
+
| 'await-transform' {
|
|
969
|
+
const postTransform = getPostTransformImports(env, file)
|
|
970
|
+
|
|
971
|
+
if (postTransform) {
|
|
972
|
+
const surviving = violations.filter(
|
|
973
|
+
(pv) => !pv.info.resolved || postTransform.has(pv.info.resolved),
|
|
974
|
+
)
|
|
975
|
+
if (surviving.length === 0) return 'all-stripped'
|
|
976
|
+
env.pendingViolations.set(file, surviving)
|
|
977
|
+
return { active: surviving, edgeSurvivalApplied: true }
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Pre-transform violations need edge-survival verification first.
|
|
981
|
+
if (violations.some((pv) => pv.fromPreTransformResolve)) {
|
|
982
|
+
return 'await-transform'
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
return { active: violations, edgeSurvivalApplied: false }
|
|
986
|
+
}
|
|
987
|
+
|
|
748
988
|
/**
|
|
749
989
|
* Process pending violations for the given environment. Called from the
|
|
750
990
|
* transform-cache hook after each module transform is cached, because new
|
|
@@ -761,6 +1001,16 @@ export function importProtectionPlugin(
|
|
|
761
1001
|
const toDelete: Array<string> = []
|
|
762
1002
|
|
|
763
1003
|
for (const [file, violations] of env.pendingViolations) {
|
|
1004
|
+
const filtered = filterEdgeSurvival(env, file, violations)
|
|
1005
|
+
|
|
1006
|
+
if (filtered === 'all-stripped') {
|
|
1007
|
+
toDelete.push(file)
|
|
1008
|
+
continue
|
|
1009
|
+
}
|
|
1010
|
+
if (filtered === 'await-transform') continue
|
|
1011
|
+
|
|
1012
|
+
const { active, edgeSurvivalApplied } = filtered
|
|
1013
|
+
|
|
764
1014
|
// Wait for entries before running reachability. registerEntries()
|
|
765
1015
|
// populates entries at buildStart; resolveId(!importer) may add more.
|
|
766
1016
|
const status =
|
|
@@ -769,36 +1019,35 @@ export function importProtectionPlugin(
|
|
|
769
1019
|
: 'unknown'
|
|
770
1020
|
|
|
771
1021
|
if (status === 'reachable') {
|
|
772
|
-
for (const pv of
|
|
773
|
-
|
|
774
|
-
pv.info.type,
|
|
775
|
-
pv.info.importer,
|
|
776
|
-
pv.info.specifier,
|
|
777
|
-
pv.info.resolved,
|
|
778
|
-
)
|
|
779
|
-
if (!hasSeen(env, key)) {
|
|
780
|
-
const freshTrace = await rebuildAndAnnotateTrace(
|
|
781
|
-
env.transformResultProvider,
|
|
782
|
-
env,
|
|
783
|
-
pv.info.env,
|
|
784
|
-
pv.info.importer,
|
|
785
|
-
pv.info.specifier,
|
|
786
|
-
pv.info.importerLoc,
|
|
787
|
-
)
|
|
788
|
-
if (freshTrace.length > pv.info.trace.length) {
|
|
789
|
-
pv.info.trace = freshTrace
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
if (config.onViolation) {
|
|
793
|
-
const result = await config.onViolation(pv.info)
|
|
794
|
-
if (result === false) continue
|
|
795
|
-
}
|
|
796
|
-
warnFn(formatViolation(pv.info, config.root))
|
|
797
|
-
}
|
|
1022
|
+
for (const pv of active) {
|
|
1023
|
+
await emitPendingViolation(env, warnFn, pv)
|
|
798
1024
|
}
|
|
799
1025
|
toDelete.push(file)
|
|
800
1026
|
} else if (status === 'unreachable') {
|
|
801
1027
|
toDelete.push(file)
|
|
1028
|
+
} else if (config.command === 'serve') {
|
|
1029
|
+
// 'unknown' reachability — some graph edges lack transform data.
|
|
1030
|
+
// When edge-survival was applied, surviving violations are confirmed
|
|
1031
|
+
// real. Without it (warm start), emit conservatively.
|
|
1032
|
+
let emittedAny = false
|
|
1033
|
+
for (const pv of active) {
|
|
1034
|
+
if (pv.fromPreTransformResolve) continue
|
|
1035
|
+
|
|
1036
|
+
const shouldEmit =
|
|
1037
|
+
edgeSurvivalApplied ||
|
|
1038
|
+
(pv.info.type === 'file' &&
|
|
1039
|
+
!!pv.info.resolved &&
|
|
1040
|
+
isInsideDirectory(pv.info.resolved, config.srcDirectory))
|
|
1041
|
+
|
|
1042
|
+
if (shouldEmit) {
|
|
1043
|
+
emittedAny =
|
|
1044
|
+
(await emitPendingViolation(env, warnFn, pv)) || emittedAny
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
if (emittedAny) {
|
|
1049
|
+
toDelete.push(file)
|
|
1050
|
+
}
|
|
802
1051
|
}
|
|
803
1052
|
// 'unknown' — keep pending for next transform-cache invocation.
|
|
804
1053
|
}
|
|
@@ -808,6 +1057,59 @@ export function importProtectionPlugin(
|
|
|
808
1057
|
}
|
|
809
1058
|
}
|
|
810
1059
|
|
|
1060
|
+
async function emitPendingViolation(
|
|
1061
|
+
env: EnvState,
|
|
1062
|
+
warnFn: (msg: string) => void,
|
|
1063
|
+
pv: PendingViolation,
|
|
1064
|
+
): Promise<boolean> {
|
|
1065
|
+
if (!pv.info.importerLoc) {
|
|
1066
|
+
const sourceCandidates = buildSourceCandidates(
|
|
1067
|
+
pv.info.specifier,
|
|
1068
|
+
pv.info.resolved,
|
|
1069
|
+
config.root,
|
|
1070
|
+
)
|
|
1071
|
+
const loc = await resolveImporterLocation(
|
|
1072
|
+
env.transformResultProvider,
|
|
1073
|
+
env,
|
|
1074
|
+
pv.info.importer,
|
|
1075
|
+
sourceCandidates,
|
|
1076
|
+
)
|
|
1077
|
+
|
|
1078
|
+
if (loc) {
|
|
1079
|
+
pv.info.importerLoc = loc
|
|
1080
|
+
pv.info.snippet = buildCodeSnippet(
|
|
1081
|
+
env.transformResultProvider,
|
|
1082
|
+
pv.info.importer,
|
|
1083
|
+
loc,
|
|
1084
|
+
)
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
if (hasSeen(env, dedupeKey(pv.info))) {
|
|
1089
|
+
return false
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
const freshTrace = await rebuildAndAnnotateTrace(
|
|
1093
|
+
env.transformResultProvider,
|
|
1094
|
+
env,
|
|
1095
|
+
pv.info.env,
|
|
1096
|
+
pv.info.importer,
|
|
1097
|
+
pv.info.specifier,
|
|
1098
|
+
pv.info.importerLoc,
|
|
1099
|
+
)
|
|
1100
|
+
if (freshTrace.length > pv.info.trace.length) {
|
|
1101
|
+
pv.info.trace = freshTrace
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
if (config.onViolation) {
|
|
1105
|
+
const result = await config.onViolation(pv.info)
|
|
1106
|
+
if (result === false) return false
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
warnFn(formatViolation(pv.info, config.root))
|
|
1110
|
+
return true
|
|
1111
|
+
}
|
|
1112
|
+
|
|
811
1113
|
/**
|
|
812
1114
|
* Record a violation as pending for later confirmation via graph
|
|
813
1115
|
* reachability. Called from `resolveId` when `shouldDefer` is true.
|
|
@@ -816,38 +1118,28 @@ export function importProtectionPlugin(
|
|
|
816
1118
|
env: EnvState,
|
|
817
1119
|
importerFile: string,
|
|
818
1120
|
info: ViolationInfo,
|
|
819
|
-
|
|
1121
|
+
isPreTransformResolve?: boolean,
|
|
820
1122
|
): void {
|
|
821
1123
|
getOrCreate(env.pendingViolations, importerFile, () => []).push({
|
|
822
1124
|
info,
|
|
823
|
-
|
|
1125
|
+
fromPreTransformResolve: isPreTransformResolve,
|
|
824
1126
|
})
|
|
825
1127
|
}
|
|
826
1128
|
|
|
827
1129
|
/** Counter for generating unique per-violation mock module IDs in build mode. */
|
|
828
1130
|
let buildViolationCounter = 0
|
|
829
1131
|
|
|
830
|
-
type HandleViolationResult = string | undefined
|
|
831
|
-
|
|
832
1132
|
async function handleViolation(
|
|
833
1133
|
ctx: ViolationReporter,
|
|
834
1134
|
env: EnvState,
|
|
835
1135
|
info: ViolationInfo,
|
|
1136
|
+
importerIdHint?: string,
|
|
836
1137
|
violationOpts?: { silent?: boolean },
|
|
837
1138
|
): Promise<HandleViolationResult> {
|
|
838
|
-
const key = dedupeKey(
|
|
839
|
-
info.type,
|
|
840
|
-
info.importer,
|
|
841
|
-
info.specifier,
|
|
842
|
-
info.resolved,
|
|
843
|
-
)
|
|
844
|
-
|
|
845
1139
|
if (!violationOpts?.silent) {
|
|
846
1140
|
if (config.onViolation) {
|
|
847
1141
|
const result = await config.onViolation(info)
|
|
848
|
-
if (result === false)
|
|
849
|
-
return undefined
|
|
850
|
-
}
|
|
1142
|
+
if (result === false) return undefined
|
|
851
1143
|
}
|
|
852
1144
|
|
|
853
1145
|
if (config.effectiveBehavior === 'error') {
|
|
@@ -862,71 +1154,56 @@ export function importProtectionPlugin(
|
|
|
862
1154
|
//
|
|
863
1155
|
// Build mode never reaches here — all build violations are
|
|
864
1156
|
// deferred via shouldDefer and handled silently.
|
|
865
|
-
|
|
866
1157
|
return ctx.error(formatViolation(info, config.root))
|
|
867
1158
|
}
|
|
868
1159
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
if (!seen) {
|
|
1160
|
+
if (!hasSeen(env, dedupeKey(info))) {
|
|
872
1161
|
ctx.warn(formatViolation(info, config.root))
|
|
873
1162
|
}
|
|
874
|
-
} else
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1163
|
+
} else if (
|
|
1164
|
+
config.effectiveBehavior === 'error' &&
|
|
1165
|
+
config.command !== 'build'
|
|
1166
|
+
) {
|
|
1167
|
+
return undefined
|
|
878
1168
|
}
|
|
879
1169
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
if (config.command === 'serve') {
|
|
886
|
-
const runtimeId = mockRuntimeModuleIdFromViolation(
|
|
887
|
-
info,
|
|
888
|
-
config.mockAccess,
|
|
889
|
-
config.root,
|
|
890
|
-
)
|
|
891
|
-
const importerFile = normalizeFilePath(info.importer)
|
|
892
|
-
const exports =
|
|
893
|
-
env.mockExportsByImporter.get(importerFile)?.get(info.specifier) ?? []
|
|
894
|
-
return resolveViteId(
|
|
895
|
-
makeMockEdgeModuleId(exports, info.specifier, runtimeId),
|
|
896
|
-
)
|
|
897
|
-
}
|
|
1170
|
+
// File violations: return resolved path — the self-denial transform
|
|
1171
|
+
// will replace the file's content with a mock module. This avoids
|
|
1172
|
+
// virtual module IDs that could leak across environments via
|
|
1173
|
+
// third-party resolver caches.
|
|
1174
|
+
if (info.type === 'file') return info.resolved
|
|
898
1175
|
|
|
899
|
-
//
|
|
900
|
-
//
|
|
901
|
-
//
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
// so the mock-edge module's `import mock from "..."` goes through
|
|
908
|
-
// resolveId, which adds the \0 prefix. Using the resolved ID directly
|
|
909
|
-
// would cause Rollup/Vite to skip resolveId and fail to find the module.
|
|
910
|
-
const baseMockId = `${MOCK_BUILD_PREFIX}${buildViolationCounter++}`
|
|
911
|
-
const importerFile = normalizeFilePath(info.importer)
|
|
912
|
-
const exports =
|
|
913
|
-
env.mockExportsByImporter.get(importerFile)?.get(info.specifier) ?? []
|
|
914
|
-
return resolveViteId(
|
|
915
|
-
makeMockEdgeModuleId(exports, info.specifier, baseMockId),
|
|
1176
|
+
// Non-file violations (specifier/marker): create mock-edge module.
|
|
1177
|
+
// Dev mode uses a runtime diagnostics ID; build mode uses a unique
|
|
1178
|
+
// per-violation ID so generateBundle can check tree-shaking survival.
|
|
1179
|
+
const exports = await resolveExportsForDeniedSpecifier(
|
|
1180
|
+
env,
|
|
1181
|
+
ctx,
|
|
1182
|
+
info,
|
|
1183
|
+
importerIdHint,
|
|
916
1184
|
)
|
|
1185
|
+
const baseMockId =
|
|
1186
|
+
config.command === 'serve'
|
|
1187
|
+
? mockRuntimeModuleIdFromViolation(info, config.mockAccess, config.root)
|
|
1188
|
+
: `${MOCK_BUILD_PREFIX}${buildViolationCounter++}`
|
|
1189
|
+
return resolveViteId(makeMockEdgeModuleId(exports, baseMockId))
|
|
917
1190
|
}
|
|
918
1191
|
|
|
919
1192
|
/**
|
|
920
1193
|
* Unified violation dispatch: either defers or reports immediately.
|
|
921
1194
|
*
|
|
922
|
-
* When `shouldDefer` is true
|
|
923
|
-
* the mock module ID, then stores
|
|
924
|
-
*
|
|
925
|
-
*
|
|
1195
|
+
* When `shouldDefer` is true (dev mock + build modes), calls
|
|
1196
|
+
* `handleViolation` silently to obtain the mock module ID, then stores
|
|
1197
|
+
* the violation for later verification:
|
|
1198
|
+
* - Dev mock mode: all violations are deferred to `pendingViolations`
|
|
1199
|
+
* for edge-survival and graph-reachability checking via
|
|
1200
|
+
* `processPendingViolations`.
|
|
926
1201
|
* - Build mode (mock + error): defers to `deferredBuildViolations` for
|
|
927
1202
|
* tree-shaking verification in `generateBundle`.
|
|
928
1203
|
*
|
|
929
|
-
* Otherwise reports (
|
|
1204
|
+
* Otherwise reports immediately (dev error mode). Pre-transform
|
|
1205
|
+
* resolves are silenced in error mode because they fire before the
|
|
1206
|
+
* compiler runs and there is no deferred verification path.
|
|
930
1207
|
*
|
|
931
1208
|
* Returns the mock module ID / resolve result from `handleViolation`.
|
|
932
1209
|
*/
|
|
@@ -934,12 +1211,15 @@ export function importProtectionPlugin(
|
|
|
934
1211
|
ctx: ViolationReporter,
|
|
935
1212
|
env: EnvState,
|
|
936
1213
|
importerFile: string,
|
|
1214
|
+
importerIdHint: string | undefined,
|
|
937
1215
|
info: ViolationInfo,
|
|
938
1216
|
shouldDefer: boolean,
|
|
939
1217
|
isPreTransformResolve: boolean,
|
|
940
1218
|
): Promise<HandleViolationResult> {
|
|
941
1219
|
if (shouldDefer) {
|
|
942
|
-
const result = await handleViolation(ctx, env, info, {
|
|
1220
|
+
const result = await handleViolation(ctx, env, info, importerIdHint, {
|
|
1221
|
+
silent: true,
|
|
1222
|
+
})
|
|
943
1223
|
|
|
944
1224
|
if (config.command === 'build') {
|
|
945
1225
|
// Build mode: store for generateBundle tree-shaking check.
|
|
@@ -953,13 +1233,18 @@ export function importProtectionPlugin(
|
|
|
953
1233
|
})
|
|
954
1234
|
} else {
|
|
955
1235
|
// Dev mock: store for graph-reachability check.
|
|
956
|
-
deferViolation(env, importerFile, info,
|
|
1236
|
+
deferViolation(env, importerFile, info, isPreTransformResolve)
|
|
957
1237
|
await processPendingViolations(env, ctx.warn.bind(ctx))
|
|
958
1238
|
}
|
|
959
1239
|
|
|
960
1240
|
return result
|
|
961
1241
|
}
|
|
962
|
-
|
|
1242
|
+
|
|
1243
|
+
// Non-deferred path: dev error mode only.
|
|
1244
|
+
// Pre-transform resolves are silenced because they fire before the
|
|
1245
|
+
// compiler runs — imports inside `.server()` callbacks haven't been
|
|
1246
|
+
// stripped yet and error mode has no deferred verification.
|
|
1247
|
+
return handleViolation(ctx, env, info, importerIdHint, {
|
|
963
1248
|
silent: isPreTransformResolve,
|
|
964
1249
|
})
|
|
965
1250
|
}
|
|
@@ -983,11 +1268,6 @@ export function importProtectionPlugin(
|
|
|
983
1268
|
const { startConfig, resolvedStartConfig } = opts.getConfig()
|
|
984
1269
|
config.srcDirectory = resolvedStartConfig.srcDirectory
|
|
985
1270
|
|
|
986
|
-
config.entryFiles = [
|
|
987
|
-
resolvedStartConfig.routerFilePath,
|
|
988
|
-
resolvedStartConfig.startFilePath,
|
|
989
|
-
].filter((f): f is string => Boolean(f))
|
|
990
|
-
|
|
991
1271
|
const userOpts: ImportProtectionOptions | undefined =
|
|
992
1272
|
startConfig.importProtection
|
|
993
1273
|
|
|
@@ -998,18 +1278,14 @@ export function importProtectionPlugin(
|
|
|
998
1278
|
|
|
999
1279
|
config.enabled = true
|
|
1000
1280
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
} else {
|
|
1005
|
-
config.effectiveBehavior =
|
|
1006
|
-
viteConfig.command === 'serve'
|
|
1007
|
-
? (userOpts.behavior.dev ?? 'mock')
|
|
1008
|
-
: (userOpts.behavior.build ?? 'error')
|
|
1009
|
-
}
|
|
1281
|
+
const behavior = userOpts?.behavior
|
|
1282
|
+
if (typeof behavior === 'string') {
|
|
1283
|
+
config.effectiveBehavior = behavior
|
|
1010
1284
|
} else {
|
|
1011
1285
|
config.effectiveBehavior =
|
|
1012
|
-
viteConfig.command === 'serve'
|
|
1286
|
+
viteConfig.command === 'serve'
|
|
1287
|
+
? (behavior?.dev ?? 'mock')
|
|
1288
|
+
: (behavior?.build ?? 'error')
|
|
1013
1289
|
}
|
|
1014
1290
|
|
|
1015
1291
|
config.logMode = userOpts?.log ?? 'once'
|
|
@@ -1021,6 +1297,9 @@ export function importProtectionPlugin(
|
|
|
1021
1297
|
}
|
|
1022
1298
|
|
|
1023
1299
|
const defaults = getDefaultImportProtectionRules()
|
|
1300
|
+
// Use user-provided patterns when available, otherwise defaults.
|
|
1301
|
+
const pick = <T>(user: Array<T> | undefined, fallback: Array<T>) =>
|
|
1302
|
+
user ? [...user] : [...fallback]
|
|
1024
1303
|
|
|
1025
1304
|
// Client specifier denies always include framework defaults even
|
|
1026
1305
|
// when the user provides a custom list.
|
|
@@ -1029,45 +1308,34 @@ export function importProtectionPlugin(
|
|
|
1029
1308
|
...(userOpts?.client?.specifiers ?? []),
|
|
1030
1309
|
])
|
|
1031
1310
|
|
|
1032
|
-
const clientFiles = userOpts?.client?.files
|
|
1033
|
-
? [...userOpts.client.files]
|
|
1034
|
-
: [...defaults.client.files]
|
|
1035
|
-
const clientExcludeFiles = userOpts?.client?.excludeFiles
|
|
1036
|
-
? [...userOpts.client.excludeFiles]
|
|
1037
|
-
: [...defaults.client.excludeFiles]
|
|
1038
|
-
const serverSpecifiers = userOpts?.server?.specifiers
|
|
1039
|
-
? dedupePatterns([...userOpts.server.specifiers])
|
|
1040
|
-
: dedupePatterns([...defaults.server.specifiers])
|
|
1041
|
-
const serverFiles = userOpts?.server?.files
|
|
1042
|
-
? [...userOpts.server.files]
|
|
1043
|
-
: [...defaults.server.files]
|
|
1044
|
-
const serverExcludeFiles = userOpts?.server?.excludeFiles
|
|
1045
|
-
? [...userOpts.server.excludeFiles]
|
|
1046
|
-
: [...defaults.server.excludeFiles]
|
|
1047
|
-
|
|
1048
1311
|
config.compiledRules.client = {
|
|
1049
1312
|
specifiers: compileMatchers(clientSpecifiers),
|
|
1050
|
-
files: compileMatchers(
|
|
1051
|
-
|
|
1313
|
+
files: compileMatchers(
|
|
1314
|
+
pick(userOpts?.client?.files, defaults.client.files),
|
|
1315
|
+
),
|
|
1316
|
+
excludeFiles: compileMatchers(
|
|
1317
|
+
pick(userOpts?.client?.excludeFiles, defaults.client.excludeFiles),
|
|
1318
|
+
),
|
|
1052
1319
|
}
|
|
1053
1320
|
config.compiledRules.server = {
|
|
1054
|
-
specifiers: compileMatchers(
|
|
1055
|
-
|
|
1056
|
-
|
|
1321
|
+
specifiers: compileMatchers(
|
|
1322
|
+
dedupePatterns(
|
|
1323
|
+
pick(userOpts?.server?.specifiers, defaults.server.specifiers),
|
|
1324
|
+
),
|
|
1325
|
+
),
|
|
1326
|
+
files: compileMatchers(
|
|
1327
|
+
pick(userOpts?.server?.files, defaults.server.files),
|
|
1328
|
+
),
|
|
1329
|
+
excludeFiles: compileMatchers(
|
|
1330
|
+
pick(userOpts?.server?.excludeFiles, defaults.server.excludeFiles),
|
|
1331
|
+
),
|
|
1057
1332
|
}
|
|
1058
1333
|
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
config.excludeMatchers = compileMatchers(userOpts.exclude)
|
|
1065
|
-
}
|
|
1066
|
-
if (userOpts?.ignoreImporters) {
|
|
1067
|
-
config.ignoreImporterMatchers = compileMatchers(
|
|
1068
|
-
userOpts.ignoreImporters,
|
|
1069
|
-
)
|
|
1070
|
-
}
|
|
1334
|
+
config.includeMatchers = compileMatchers(userOpts?.include ?? [])
|
|
1335
|
+
config.excludeMatchers = compileMatchers(userOpts?.exclude ?? [])
|
|
1336
|
+
config.ignoreImporterMatchers = compileMatchers(
|
|
1337
|
+
userOpts?.ignoreImporters ?? [],
|
|
1338
|
+
)
|
|
1071
1339
|
|
|
1072
1340
|
// Marker specifiers
|
|
1073
1341
|
const markers = getMarkerSpecifiers()
|
|
@@ -1085,25 +1353,13 @@ export function importProtectionPlugin(
|
|
|
1085
1353
|
if (!config.enabled) return
|
|
1086
1354
|
// Clear memoization caches that grow unboundedly across builds
|
|
1087
1355
|
clearNormalizeFilePathCache()
|
|
1088
|
-
|
|
1356
|
+
extensionlessIdResolver.clear()
|
|
1357
|
+
importPatternCache.clear()
|
|
1089
1358
|
shouldCheckImporterCache.clear()
|
|
1090
1359
|
|
|
1091
1360
|
// Clear per-env caches
|
|
1092
1361
|
for (const envState of envStates.values()) {
|
|
1093
|
-
envState
|
|
1094
|
-
envState.resolveCacheByFile.clear()
|
|
1095
|
-
envState.importLocCache.clear()
|
|
1096
|
-
envState.seenViolations.clear()
|
|
1097
|
-
envState.transformResultCache.clear()
|
|
1098
|
-
envState.transformResultKeysByFile.clear()
|
|
1099
|
-
envState.postTransformImports.clear()
|
|
1100
|
-
envState.serverFnLookupModules.clear()
|
|
1101
|
-
envState.pendingViolations.clear()
|
|
1102
|
-
envState.deferredBuildViolations.length = 0
|
|
1103
|
-
envState.graph.clear()
|
|
1104
|
-
envState.deniedSources.clear()
|
|
1105
|
-
envState.deniedEdges.clear()
|
|
1106
|
-
envState.mockExportsByImporter.clear()
|
|
1362
|
+
clearEnvState(envState)
|
|
1107
1363
|
}
|
|
1108
1364
|
|
|
1109
1365
|
// Clear shared state
|
|
@@ -1119,42 +1375,14 @@ export function importProtectionPlugin(
|
|
|
1119
1375
|
if (mod.id) {
|
|
1120
1376
|
const id = mod.id
|
|
1121
1377
|
const importerFile = normalizeFilePath(id)
|
|
1378
|
+
|
|
1379
|
+
// Invalidate extensionless-resolution cache entries affected by this file.
|
|
1380
|
+
extensionlessIdResolver.invalidateByFile(importerFile)
|
|
1122
1381
|
shared.fileMarkerKind.delete(importerFile)
|
|
1123
1382
|
|
|
1124
1383
|
// Invalidate per-env caches
|
|
1125
1384
|
for (const envState of envStates.values()) {
|
|
1126
|
-
envState
|
|
1127
|
-
|
|
1128
|
-
// Invalidate resolve cache using reverse index
|
|
1129
|
-
const resolveKeys = envState.resolveCacheByFile.get(importerFile)
|
|
1130
|
-
if (resolveKeys) {
|
|
1131
|
-
for (const key of resolveKeys) {
|
|
1132
|
-
envState.resolveCache.delete(key)
|
|
1133
|
-
}
|
|
1134
|
-
envState.resolveCacheByFile.delete(importerFile)
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
// Invalidate graph edges
|
|
1138
|
-
envState.graph.invalidate(importerFile)
|
|
1139
|
-
envState.deniedEdges.delete(importerFile)
|
|
1140
|
-
envState.mockExportsByImporter.delete(importerFile)
|
|
1141
|
-
envState.serverFnLookupModules.delete(importerFile)
|
|
1142
|
-
envState.pendingViolations.delete(importerFile)
|
|
1143
|
-
|
|
1144
|
-
// Invalidate transform result cache for this file.
|
|
1145
|
-
const transformKeys =
|
|
1146
|
-
envState.transformResultKeysByFile.get(importerFile)
|
|
1147
|
-
if (transformKeys) {
|
|
1148
|
-
for (const key of transformKeys) {
|
|
1149
|
-
envState.transformResultCache.delete(key)
|
|
1150
|
-
envState.postTransformImports.delete(key)
|
|
1151
|
-
}
|
|
1152
|
-
envState.transformResultKeysByFile.delete(importerFile)
|
|
1153
|
-
} else {
|
|
1154
|
-
// Fallback: at least clear the physical-file entry.
|
|
1155
|
-
envState.transformResultCache.delete(importerFile)
|
|
1156
|
-
envState.postTransformImports.delete(importerFile)
|
|
1157
|
-
}
|
|
1385
|
+
invalidateFileFromEnv(envState, importerFile)
|
|
1158
1386
|
}
|
|
1159
1387
|
}
|
|
1160
1388
|
}
|
|
@@ -1173,7 +1401,7 @@ export function importProtectionPlugin(
|
|
|
1173
1401
|
: '(entry)'
|
|
1174
1402
|
const isEntryResolve = !importer
|
|
1175
1403
|
const filtered =
|
|
1176
|
-
|
|
1404
|
+
process.env.TSR_IMPORT_PROTECTION_DEBUG_FILTER === 'entry'
|
|
1177
1405
|
? isEntryResolve
|
|
1178
1406
|
: matchesDebugFilter(source, importerPath)
|
|
1179
1407
|
if (filtered) {
|
|
@@ -1184,12 +1412,11 @@ export function importProtectionPlugin(
|
|
|
1184
1412
|
importer: importerPath,
|
|
1185
1413
|
isEntryResolve,
|
|
1186
1414
|
command: config.command,
|
|
1187
|
-
behavior: config.effectiveBehavior,
|
|
1188
1415
|
})
|
|
1189
1416
|
}
|
|
1190
1417
|
}
|
|
1191
1418
|
|
|
1192
|
-
// Internal virtual modules
|
|
1419
|
+
// Internal virtual modules (mock:build:N, mock-edge, mock-runtime, marker)
|
|
1193
1420
|
const internalVirtualId = resolveInternalVirtualModuleId(source)
|
|
1194
1421
|
if (internalVirtualId) return internalVirtualId
|
|
1195
1422
|
|
|
@@ -1217,15 +1444,30 @@ export function importProtectionPlugin(
|
|
|
1217
1444
|
env.serverFnLookupModules.has(normalizedImporter) ||
|
|
1218
1445
|
isScanResolve
|
|
1219
1446
|
|
|
1220
|
-
// Dev mock mode: defer violations
|
|
1221
|
-
//
|
|
1447
|
+
// Dev mock mode: defer all violations (including pre-transform
|
|
1448
|
+
// resolves) until post-transform data is available, then
|
|
1449
|
+
// confirm/discard via graph reachability.
|
|
1222
1450
|
// Build mode (both mock and error): defer violations until
|
|
1223
1451
|
// generateBundle so tree-shaking can eliminate false positives.
|
|
1224
1452
|
const isDevMock =
|
|
1225
1453
|
config.command === 'serve' && config.effectiveBehavior === 'mock'
|
|
1226
1454
|
const isBuild = config.command === 'build'
|
|
1455
|
+
const shouldDefer = shouldDeferViolation({ isBuild, isDevMock })
|
|
1456
|
+
|
|
1457
|
+
const resolveAgainstImporter = async (): Promise<string | null> => {
|
|
1458
|
+
const primary = await this.resolve(source, importer, {
|
|
1459
|
+
skipSelf: true,
|
|
1460
|
+
})
|
|
1461
|
+
if (primary) {
|
|
1462
|
+
return canonicalizeResolvedId(
|
|
1463
|
+
primary.id,
|
|
1464
|
+
config.root,
|
|
1465
|
+
resolveExtensionlessAbsoluteId,
|
|
1466
|
+
)
|
|
1467
|
+
}
|
|
1227
1468
|
|
|
1228
|
-
|
|
1469
|
+
return null
|
|
1470
|
+
}
|
|
1229
1471
|
|
|
1230
1472
|
// Check if this is a marker import
|
|
1231
1473
|
const markerKind = config.markerSpecifiers.serverOnly.has(source)
|
|
@@ -1258,16 +1500,17 @@ export function importProtectionPlugin(
|
|
|
1258
1500
|
source,
|
|
1259
1501
|
{
|
|
1260
1502
|
type: 'marker',
|
|
1261
|
-
message:
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1503
|
+
message: buildMarkerViolationMessage(
|
|
1504
|
+
getRelativePath(normalizedImporter),
|
|
1505
|
+
markerKind,
|
|
1506
|
+
),
|
|
1265
1507
|
},
|
|
1266
1508
|
)
|
|
1267
1509
|
const markerResult = await reportOrDeferViolation(
|
|
1268
1510
|
this,
|
|
1269
1511
|
env,
|
|
1270
1512
|
normalizedImporter,
|
|
1513
|
+
importer,
|
|
1271
1514
|
info,
|
|
1272
1515
|
shouldDefer,
|
|
1273
1516
|
isPreTransformResolve,
|
|
@@ -1284,6 +1527,60 @@ export function importProtectionPlugin(
|
|
|
1284
1527
|
}
|
|
1285
1528
|
}
|
|
1286
1529
|
|
|
1530
|
+
// Retroactive marker violation detection: on cold starts, module
|
|
1531
|
+
// A may import module B before B's marker is set (because B hasn't
|
|
1532
|
+
// been processed yet). When B's marker is set (here),
|
|
1533
|
+
// retroactively check all known importers of B in the graph and
|
|
1534
|
+
// create deferred marker violations for them. Without this,
|
|
1535
|
+
// cold-start ordering can miss marker violations that warm starts
|
|
1536
|
+
// detect (warm starts see markers early from cached transforms).
|
|
1537
|
+
//
|
|
1538
|
+
// Uses lightweight `deferViolation` to avoid heavy side effects
|
|
1539
|
+
// (mock module creation, export resolution). Immediately calls
|
|
1540
|
+
// `processPendingViolations` to flush the deferred violations,
|
|
1541
|
+
// because the marker resolveId fires during Vite's import
|
|
1542
|
+
// analysis (after our transform hook) — there may be no
|
|
1543
|
+
// subsequent transform invocation to flush them.
|
|
1544
|
+
//
|
|
1545
|
+
// Guarded by `violatesEnv` (per-environment) plus a per-env
|
|
1546
|
+
// seen-set. The marker is shared across environments but each
|
|
1547
|
+
// env's graph has its own edges; this ensures the check runs
|
|
1548
|
+
// at most once per (env, module) pair.
|
|
1549
|
+
const envRetroKey = `retro-marker:${normalizedImporter}`
|
|
1550
|
+
if (violatesEnv && !env.seenViolations.has(envRetroKey)) {
|
|
1551
|
+
env.seenViolations.add(envRetroKey)
|
|
1552
|
+
let retroDeferred = false
|
|
1553
|
+
const importersMap = env.graph.reverseEdges.get(normalizedImporter)
|
|
1554
|
+
if (importersMap && importersMap.size > 0) {
|
|
1555
|
+
for (const [importerFile, specifier] of importersMap) {
|
|
1556
|
+
if (!specifier) continue
|
|
1557
|
+
if (!shouldCheckImporter(importerFile)) continue
|
|
1558
|
+
const markerInfo = await buildMarkerViolationFromResolvedImport(
|
|
1559
|
+
provider,
|
|
1560
|
+
env,
|
|
1561
|
+
envName,
|
|
1562
|
+
envType,
|
|
1563
|
+
importerFile,
|
|
1564
|
+
specifier,
|
|
1565
|
+
normalizedImporter,
|
|
1566
|
+
getRelativePath(normalizedImporter),
|
|
1567
|
+
)
|
|
1568
|
+
if (markerInfo) {
|
|
1569
|
+
deferViolation(
|
|
1570
|
+
env,
|
|
1571
|
+
importerFile,
|
|
1572
|
+
markerInfo,
|
|
1573
|
+
isPreTransformResolve,
|
|
1574
|
+
)
|
|
1575
|
+
retroDeferred = true
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
if (retroDeferred) {
|
|
1580
|
+
await processPendingViolations(env, this.warn.bind(this))
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1287
1584
|
return markerKind === 'server'
|
|
1288
1585
|
? resolvedMarkerVirtualModuleId('server')
|
|
1289
1586
|
: resolvedMarkerVirtualModuleId('client')
|
|
@@ -1299,7 +1596,9 @@ export function importProtectionPlugin(
|
|
|
1299
1596
|
// 1. Specifier-based denial
|
|
1300
1597
|
const specifierMatch = matchesAny(source, matchers.specifiers)
|
|
1301
1598
|
if (specifierMatch) {
|
|
1302
|
-
|
|
1599
|
+
if (!isPreTransformResolve) {
|
|
1600
|
+
env.graph.addEdge(source, normalizedImporter, source)
|
|
1601
|
+
}
|
|
1303
1602
|
const info = await buildViolationInfo(
|
|
1304
1603
|
provider,
|
|
1305
1604
|
env,
|
|
@@ -1314,10 +1613,24 @@ export function importProtectionPlugin(
|
|
|
1314
1613
|
message: `Import "${source}" is denied in the ${envType} environment`,
|
|
1315
1614
|
},
|
|
1316
1615
|
)
|
|
1616
|
+
|
|
1617
|
+
// Resolve the specifier so edge-survival can verify whether
|
|
1618
|
+
// the import survives the Start compiler transform (e.g.
|
|
1619
|
+
// factory-safe pattern strips imports inside .server() callbacks).
|
|
1620
|
+
if (shouldDefer && !info.resolved) {
|
|
1621
|
+
try {
|
|
1622
|
+
const resolvedForInfo = await resolveAgainstImporter()
|
|
1623
|
+
if (resolvedForInfo) info.resolved = resolvedForInfo
|
|
1624
|
+
} catch {
|
|
1625
|
+
// Non-fatal: edge-survival will skip unresolved specifiers
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1317
1629
|
return reportOrDeferViolation(
|
|
1318
1630
|
this,
|
|
1319
1631
|
env,
|
|
1320
1632
|
normalizedImporter,
|
|
1633
|
+
importer,
|
|
1321
1634
|
info,
|
|
1322
1635
|
shouldDefer,
|
|
1323
1636
|
isPreTransformResolve,
|
|
@@ -1331,16 +1644,19 @@ export function importProtectionPlugin(
|
|
|
1331
1644
|
if (env.resolveCache.has(cacheKey)) {
|
|
1332
1645
|
resolved = env.resolveCache.get(cacheKey) ?? null
|
|
1333
1646
|
} else {
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
env.
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1647
|
+
resolved = await resolveAgainstImporter()
|
|
1648
|
+
|
|
1649
|
+
// Only cache successful resolves. Null resolves can be
|
|
1650
|
+
// order-dependent across importer variants (e.g. code-split
|
|
1651
|
+
// `?tsr-split=...` ids) and may poison later lookups.
|
|
1652
|
+
if (resolved !== null) {
|
|
1653
|
+
env.resolveCache.set(cacheKey, resolved)
|
|
1654
|
+
getOrCreate(
|
|
1655
|
+
env.resolveCacheByFile,
|
|
1656
|
+
normalizedImporter,
|
|
1657
|
+
() => new Set(),
|
|
1658
|
+
).add(cacheKey)
|
|
1659
|
+
}
|
|
1344
1660
|
}
|
|
1345
1661
|
|
|
1346
1662
|
if (resolved) {
|
|
@@ -1351,7 +1667,9 @@ export function importProtectionPlugin(
|
|
|
1351
1667
|
env.serverFnLookupModules.add(resolved)
|
|
1352
1668
|
}
|
|
1353
1669
|
|
|
1354
|
-
|
|
1670
|
+
if (!isPreTransformResolve) {
|
|
1671
|
+
env.graph.addEdge(resolved, normalizedImporter, source)
|
|
1672
|
+
}
|
|
1355
1673
|
|
|
1356
1674
|
// Skip file-based and marker-based denial for resolved paths that
|
|
1357
1675
|
// match the per-environment `excludeFiles` patterns. By default
|
|
@@ -1371,7 +1689,7 @@ export function importProtectionPlugin(
|
|
|
1371
1689
|
: undefined
|
|
1372
1690
|
|
|
1373
1691
|
if (fileMatch) {
|
|
1374
|
-
const info = await
|
|
1692
|
+
const info = await buildFileViolationInfo(
|
|
1375
1693
|
provider,
|
|
1376
1694
|
env,
|
|
1377
1695
|
envName,
|
|
@@ -1379,17 +1697,14 @@ export function importProtectionPlugin(
|
|
|
1379
1697
|
importer,
|
|
1380
1698
|
normalizedImporter,
|
|
1381
1699
|
source,
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
pattern: fileMatch.pattern,
|
|
1385
|
-
resolved,
|
|
1386
|
-
message: `Import "${source}" (resolved to "${relativePath}") is denied in the ${envType} environment`,
|
|
1387
|
-
},
|
|
1700
|
+
resolved,
|
|
1701
|
+
fileMatch.pattern,
|
|
1388
1702
|
)
|
|
1389
1703
|
return reportOrDeferViolation(
|
|
1390
1704
|
this,
|
|
1391
1705
|
env,
|
|
1392
1706
|
normalizedImporter,
|
|
1707
|
+
importer,
|
|
1393
1708
|
info,
|
|
1394
1709
|
shouldDefer,
|
|
1395
1710
|
isPreTransformResolve,
|
|
@@ -1411,6 +1726,7 @@ export function importProtectionPlugin(
|
|
|
1411
1726
|
this,
|
|
1412
1727
|
env,
|
|
1413
1728
|
normalizedImporter,
|
|
1729
|
+
importer,
|
|
1414
1730
|
markerInfo,
|
|
1415
1731
|
shouldDefer,
|
|
1416
1732
|
isPreTransformResolve,
|
|
@@ -1447,16 +1763,51 @@ export function importProtectionPlugin(
|
|
|
1447
1763
|
const env = envStates.get(envName)
|
|
1448
1764
|
if (!env || env.deferredBuildViolations.length === 0) return
|
|
1449
1765
|
|
|
1766
|
+
const candidateCache = new Map<string, Array<string>>()
|
|
1767
|
+
const toModuleIdCandidates = (id: string): Array<string> => {
|
|
1768
|
+
let cached = candidateCache.get(id)
|
|
1769
|
+
if (cached) return cached
|
|
1770
|
+
|
|
1771
|
+
const out = new Set<string>()
|
|
1772
|
+
const normalized = normalizeFilePath(id)
|
|
1773
|
+
out.add(id)
|
|
1774
|
+
out.add(normalized)
|
|
1775
|
+
out.add(relativizePath(normalized, config.root))
|
|
1776
|
+
|
|
1777
|
+
if (normalized.startsWith(VITE_BROWSER_VIRTUAL_PREFIX)) {
|
|
1778
|
+
const internal = `\0${normalized.slice(VITE_BROWSER_VIRTUAL_PREFIX.length)}`
|
|
1779
|
+
out.add(internal)
|
|
1780
|
+
out.add(relativizePath(normalizeFilePath(internal), config.root))
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
if (normalized.startsWith('\0')) {
|
|
1784
|
+
const browser = `${VITE_BROWSER_VIRTUAL_PREFIX}${normalized.slice(1)}`
|
|
1785
|
+
out.add(browser)
|
|
1786
|
+
out.add(relativizePath(normalizeFilePath(browser), config.root))
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
cached = Array.from(out)
|
|
1790
|
+
candidateCache.set(id, cached)
|
|
1791
|
+
return cached
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1450
1794
|
// Collect all module IDs that survived tree-shaking in this bundle.
|
|
1451
1795
|
const survivingModules = new Set<string>()
|
|
1452
1796
|
for (const chunk of Object.values(bundle)) {
|
|
1453
1797
|
if (chunk.type === 'chunk') {
|
|
1454
1798
|
for (const moduleId of Object.keys(chunk.modules)) {
|
|
1455
|
-
|
|
1799
|
+
for (const candidate of toModuleIdCandidates(moduleId)) {
|
|
1800
|
+
survivingModules.add(candidate)
|
|
1801
|
+
}
|
|
1456
1802
|
}
|
|
1457
1803
|
}
|
|
1458
1804
|
}
|
|
1459
1805
|
|
|
1806
|
+
const didModuleSurvive = (moduleId: string): boolean =>
|
|
1807
|
+
toModuleIdCandidates(moduleId).some((candidate) =>
|
|
1808
|
+
survivingModules.has(candidate),
|
|
1809
|
+
)
|
|
1810
|
+
|
|
1460
1811
|
// Check each deferred violation: if its check module survived
|
|
1461
1812
|
// in the bundle, the import was NOT tree-shaken — real leak.
|
|
1462
1813
|
const realViolations: Array<ViolationInfo> = []
|
|
@@ -1465,8 +1816,34 @@ export function importProtectionPlugin(
|
|
|
1465
1816
|
mockModuleId,
|
|
1466
1817
|
checkModuleId,
|
|
1467
1818
|
} of env.deferredBuildViolations) {
|
|
1468
|
-
|
|
1469
|
-
if (
|
|
1819
|
+
let survived: boolean
|
|
1820
|
+
if (checkModuleId != null) {
|
|
1821
|
+
// Marker violation: check if the importer survived
|
|
1822
|
+
// (marker is about the file's directive, not a binding).
|
|
1823
|
+
// Include transform-result keys (e.g. code-split variants)
|
|
1824
|
+
// to cover all bundle representations of the importer.
|
|
1825
|
+
const importerVariantIds = new Set<string>([info.importer])
|
|
1826
|
+
const importerKeys = env.transformResultKeysByFile.get(
|
|
1827
|
+
normalizeFilePath(info.importer),
|
|
1828
|
+
)
|
|
1829
|
+
if (importerKeys) {
|
|
1830
|
+
for (const key of importerKeys) {
|
|
1831
|
+
importerVariantIds.add(key)
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
survived = false
|
|
1835
|
+
for (const importerId of importerVariantIds) {
|
|
1836
|
+
if (didModuleSurvive(importerId)) {
|
|
1837
|
+
survived = true
|
|
1838
|
+
break
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
} else {
|
|
1842
|
+
// File/specifier violation: check if the mock module survived.
|
|
1843
|
+
survived = didModuleSurvive(mockModuleId)
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
if (!survived) continue
|
|
1470
1847
|
|
|
1471
1848
|
if (config.onViolation) {
|
|
1472
1849
|
const result = await config.onViolation(info)
|
|
@@ -1485,12 +1862,7 @@ export function importProtectionPlugin(
|
|
|
1485
1862
|
// Mock mode: warn for each surviving violation.
|
|
1486
1863
|
const seen = new Set<string>()
|
|
1487
1864
|
for (const info of realViolations) {
|
|
1488
|
-
const key = dedupeKey(
|
|
1489
|
-
info.type,
|
|
1490
|
-
info.importer,
|
|
1491
|
-
info.specifier,
|
|
1492
|
-
info.resolved,
|
|
1493
|
-
)
|
|
1865
|
+
const key = dedupeKey(info)
|
|
1494
1866
|
if (!seen.has(key)) {
|
|
1495
1867
|
seen.add(key)
|
|
1496
1868
|
this.warn(formatViolation(info, config.root))
|
|
@@ -1519,6 +1891,9 @@ export function importProtectionPlugin(
|
|
|
1519
1891
|
async handler(code, id) {
|
|
1520
1892
|
const envName = this.environment.name
|
|
1521
1893
|
const file = normalizeFilePath(id)
|
|
1894
|
+
const envType = getEnvType(envName)
|
|
1895
|
+
const matchers = getRulesForEnvironment(envName)
|
|
1896
|
+
const isBuild = config.command === 'build'
|
|
1522
1897
|
|
|
1523
1898
|
if (IMPORT_PROTECTION_DEBUG) {
|
|
1524
1899
|
if (matchesDebugFilter(file)) {
|
|
@@ -1534,6 +1909,58 @@ export function importProtectionPlugin(
|
|
|
1534
1909
|
return undefined
|
|
1535
1910
|
}
|
|
1536
1911
|
|
|
1912
|
+
// Self-denial: if this file is denied in the current environment
|
|
1913
|
+
// (e.g. a `.server` file transformed in the client environment),
|
|
1914
|
+
// replace its entire content with a mock module.
|
|
1915
|
+
//
|
|
1916
|
+
// This is the core mechanism for preventing cross-environment
|
|
1917
|
+
// cache contamination: resolveId never returns virtual module
|
|
1918
|
+
// IDs for file-based violations, so there is nothing for
|
|
1919
|
+
// third-party resolver caches (e.g. vite-tsconfig-paths) to
|
|
1920
|
+
// leak across environments. Each environment's transform
|
|
1921
|
+
// independently decides whether the file is denied.
|
|
1922
|
+
//
|
|
1923
|
+
// In dev mode, this also solves the cold-start problem where
|
|
1924
|
+
// the importer's AST is unavailable for export resolution:
|
|
1925
|
+
// the denied file's own source code is always available here,
|
|
1926
|
+
// so we parse its exports directly.
|
|
1927
|
+
const selfFileMatch = checkFileDenial(getRelativePath(file), matchers)
|
|
1928
|
+
if (selfFileMatch) {
|
|
1929
|
+
// Parse exports once — shared by build and dev paths.
|
|
1930
|
+
// Falls back to empty list on non-standard syntax.
|
|
1931
|
+
let exportNames: Array<string> = []
|
|
1932
|
+
try {
|
|
1933
|
+
exportNames = collectNamedExports(code)
|
|
1934
|
+
} catch {
|
|
1935
|
+
// Parsing may fail on non-standard syntax
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
if (isBuild) {
|
|
1939
|
+
return generateSelfContainedMockModule(exportNames)
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
// Dev mode: generate a mock that imports mock-runtime for
|
|
1943
|
+
// runtime diagnostics (error/warn on property access).
|
|
1944
|
+
const runtimeId = mockRuntimeModuleIdFromViolation(
|
|
1945
|
+
{
|
|
1946
|
+
type: 'file',
|
|
1947
|
+
env: envType,
|
|
1948
|
+
envType,
|
|
1949
|
+
behavior:
|
|
1950
|
+
config.effectiveBehavior === 'error' ? 'error' : 'mock',
|
|
1951
|
+
importer: file,
|
|
1952
|
+
specifier: relativizePath(file, config.root),
|
|
1953
|
+
resolved: file,
|
|
1954
|
+
pattern: selfFileMatch.pattern,
|
|
1955
|
+
message: `File "${relativizePath(file, config.root)}" is denied in the ${envType} environment`,
|
|
1956
|
+
trace: [],
|
|
1957
|
+
},
|
|
1958
|
+
config.mockAccess,
|
|
1959
|
+
config.root,
|
|
1960
|
+
)
|
|
1961
|
+
return generateDevSelfDenialModule(exportNames, runtimeId)
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1537
1964
|
// getCombinedSourcemap() returns the composed sourcemap
|
|
1538
1965
|
let map: SourceMapLike | undefined
|
|
1539
1966
|
try {
|
|
@@ -1555,110 +1982,166 @@ export function importProtectionPlugin(
|
|
|
1555
1982
|
const cacheKey = normalizePath(id)
|
|
1556
1983
|
|
|
1557
1984
|
const envState = getEnv(envName)
|
|
1985
|
+
const isServerFnLookup = id.includes(SERVER_FN_LOOKUP_QUERY)
|
|
1558
1986
|
|
|
1559
1987
|
// Propagate SERVER_FN_LOOKUP status before import-analysis
|
|
1560
|
-
if (
|
|
1988
|
+
if (isServerFnLookup) {
|
|
1561
1989
|
envState.serverFnLookupModules.add(file)
|
|
1562
1990
|
}
|
|
1563
1991
|
|
|
1564
|
-
|
|
1992
|
+
const result: TransformResult = {
|
|
1565
1993
|
code,
|
|
1566
1994
|
map,
|
|
1567
1995
|
originalCode,
|
|
1568
1996
|
lineIndex,
|
|
1569
|
-
})
|
|
1570
|
-
|
|
1571
|
-
const keySet = getOrCreate(
|
|
1572
|
-
envState.transformResultKeysByFile,
|
|
1573
|
-
file,
|
|
1574
|
-
() => new Set<string>(),
|
|
1575
|
-
)
|
|
1576
|
-
keySet.add(cacheKey)
|
|
1577
|
-
|
|
1578
|
-
// Also store stripped-path entry for physical-file lookups.
|
|
1579
|
-
if (cacheKey !== file) {
|
|
1580
|
-
envState.transformResultCache.set(file, {
|
|
1581
|
-
code,
|
|
1582
|
-
map,
|
|
1583
|
-
originalCode,
|
|
1584
|
-
lineIndex,
|
|
1585
|
-
})
|
|
1586
|
-
keySet.add(file)
|
|
1587
1997
|
}
|
|
1588
|
-
|
|
1589
|
-
|
|
1998
|
+
cacheTransformResult(envState, file, cacheKey, result)
|
|
1999
|
+
|
|
2000
|
+
// Build mode: only self-denial (above) and transform caching are
|
|
2001
|
+
// needed. All violations are detected and deferred in resolveId;
|
|
2002
|
+
// self-denial replaces denied file content; generateBundle checks
|
|
2003
|
+
// tree-shaking survival. The import resolution loop below is
|
|
2004
|
+
// dev-mode only — it resolves imports for graph reachability,
|
|
2005
|
+
// catches violations missed on warm starts (where Vite caches
|
|
2006
|
+
// resolveId), and rewrites denied imports to mock modules.
|
|
2007
|
+
if (isBuild) return undefined
|
|
2008
|
+
|
|
2009
|
+
// Dev mode: resolve imports, populate graph, detect violations,
|
|
2010
|
+
// and rewrite denied imports.
|
|
2011
|
+
const isDevMock = config.effectiveBehavior === 'mock'
|
|
1590
2012
|
const importSources = extractImportSources(code)
|
|
1591
2013
|
const resolvedChildren = new Set<string>()
|
|
2014
|
+
const deniedSourceReplacements = new Map<string, string>()
|
|
1592
2015
|
for (const src of importSources) {
|
|
1593
2016
|
try {
|
|
1594
2017
|
const resolved = await this.resolve(src, id, { skipSelf: true })
|
|
1595
2018
|
if (resolved && !resolved.external) {
|
|
1596
|
-
const resolvedPath =
|
|
2019
|
+
const resolvedPath = canonicalizeResolvedId(
|
|
2020
|
+
resolved.id,
|
|
2021
|
+
config.root,
|
|
2022
|
+
resolveExtensionlessAbsoluteId,
|
|
2023
|
+
)
|
|
2024
|
+
|
|
1597
2025
|
resolvedChildren.add(resolvedPath)
|
|
2026
|
+
|
|
2027
|
+
// When the resolved ID is a mock-module (from our
|
|
2028
|
+
// resolveId returning a mock-edge ID), postTransformImports
|
|
2029
|
+
// would only contain the mock ID. Edge-survival needs the
|
|
2030
|
+
// real physical path so pending violations can be matched.
|
|
2031
|
+
//
|
|
2032
|
+
// For relative specifiers we can compute the physical path
|
|
2033
|
+
// directly. For bare/alias specifiers, look up the real
|
|
2034
|
+
// resolved path from the pending violations that were
|
|
2035
|
+
// already stored by resolveId before this transform ran.
|
|
2036
|
+
if (resolved.id.includes('tanstack-start-import-protection:')) {
|
|
2037
|
+
let physicalPath: string | undefined
|
|
2038
|
+
// Look up real resolved path from pending violations
|
|
2039
|
+
const pending = envState.pendingViolations.get(file)
|
|
2040
|
+
if (pending) {
|
|
2041
|
+
const match = pending.find(
|
|
2042
|
+
(pv) => pv.info.specifier === src && pv.info.resolved,
|
|
2043
|
+
)
|
|
2044
|
+
if (match) physicalPath = match.info.resolved
|
|
2045
|
+
}
|
|
2046
|
+
if (physicalPath && physicalPath !== resolvedPath) {
|
|
2047
|
+
resolvedChildren.add(physicalPath)
|
|
2048
|
+
envState.graph.addEdge(physicalPath, file, src)
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
|
|
1598
2052
|
// Populate import graph edges for warm-start trace accuracy
|
|
1599
2053
|
envState.graph.addEdge(resolvedPath, file, src)
|
|
2054
|
+
|
|
2055
|
+
if (isDevMock) {
|
|
2056
|
+
const relativePath = getRelativePath(resolvedPath)
|
|
2057
|
+
const fileMatch = checkFileDenial(relativePath, matchers)
|
|
2058
|
+
|
|
2059
|
+
if (fileMatch) {
|
|
2060
|
+
const info = await buildFileViolationInfo(
|
|
2061
|
+
envState.transformResultProvider,
|
|
2062
|
+
envState,
|
|
2063
|
+
envName,
|
|
2064
|
+
envType,
|
|
2065
|
+
id,
|
|
2066
|
+
file,
|
|
2067
|
+
src,
|
|
2068
|
+
resolvedPath,
|
|
2069
|
+
fileMatch.pattern,
|
|
2070
|
+
)
|
|
2071
|
+
|
|
2072
|
+
const replacement = await reportOrDeferViolation(
|
|
2073
|
+
this,
|
|
2074
|
+
envState,
|
|
2075
|
+
file,
|
|
2076
|
+
id,
|
|
2077
|
+
info,
|
|
2078
|
+
isDevMock,
|
|
2079
|
+
isServerFnLookup,
|
|
2080
|
+
)
|
|
2081
|
+
|
|
2082
|
+
if (replacement) {
|
|
2083
|
+
deniedSourceReplacements.set(
|
|
2084
|
+
src,
|
|
2085
|
+
replacement.startsWith('\0')
|
|
2086
|
+
? VITE_BROWSER_VIRTUAL_PREFIX + replacement.slice(1)
|
|
2087
|
+
: replacement,
|
|
2088
|
+
)
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
1600
2092
|
}
|
|
1601
2093
|
} catch {
|
|
1602
2094
|
// Non-fatal
|
|
1603
2095
|
}
|
|
1604
2096
|
}
|
|
1605
2097
|
envState.postTransformImports.set(cacheKey, resolvedChildren)
|
|
1606
|
-
if (cacheKey !== file) {
|
|
2098
|
+
if (cacheKey !== file && !isServerFnLookup) {
|
|
1607
2099
|
envState.postTransformImports.set(file, resolvedChildren)
|
|
1608
2100
|
}
|
|
1609
2101
|
|
|
1610
2102
|
await processPendingViolations(envState, this.warn.bind(this))
|
|
1611
2103
|
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
applyToEnvironment(env) {
|
|
1622
|
-
if (!config.enabled) return false
|
|
1623
|
-
// We record expected named exports per importer so mock-edge modules
|
|
1624
|
-
// can provide explicit ESM named exports. This is needed in both dev
|
|
1625
|
-
// and build: native ESM (dev) requires real named exports, and
|
|
1626
|
-
// Rolldown (used in Vite 6+) doesn't support Rollup's
|
|
1627
|
-
// syntheticNamedExports flag which was previously relied upon in build.
|
|
1628
|
-
//
|
|
1629
|
-
// In build+error mode we still emit mock modules for deferred
|
|
1630
|
-
// violations (checked at generateBundle time), so we always need the
|
|
1631
|
-
// export name data when import protection is active.
|
|
1632
|
-
return environmentNames.has(env.name)
|
|
1633
|
-
},
|
|
2104
|
+
if (deniedSourceReplacements.size > 0) {
|
|
2105
|
+
try {
|
|
2106
|
+
const rewritten = rewriteDeniedImports(
|
|
2107
|
+
code,
|
|
2108
|
+
id,
|
|
2109
|
+
new Set(deniedSourceReplacements.keys()),
|
|
2110
|
+
(source: string) =>
|
|
2111
|
+
deniedSourceReplacements.get(source) ?? source,
|
|
2112
|
+
)
|
|
1634
2113
|
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
include: [/\.[cm]?[tj]sx?($|\?)/],
|
|
1639
|
-
},
|
|
1640
|
-
},
|
|
1641
|
-
handler(code, id) {
|
|
1642
|
-
const envName = this.environment.name
|
|
1643
|
-
const envState = envStates.get(envName)
|
|
1644
|
-
if (!envState) return undefined
|
|
2114
|
+
if (!rewritten) {
|
|
2115
|
+
return undefined
|
|
2116
|
+
}
|
|
1645
2117
|
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
2118
|
+
const normalizedMap = rewritten.map
|
|
2119
|
+
? {
|
|
2120
|
+
...rewritten.map,
|
|
2121
|
+
version: Number(rewritten.map.version),
|
|
2122
|
+
sourcesContent:
|
|
2123
|
+
rewritten.map.sourcesContent?.map(
|
|
2124
|
+
(s: string | null) => s ?? '',
|
|
2125
|
+
) ?? [],
|
|
2126
|
+
}
|
|
2127
|
+
: {
|
|
2128
|
+
version: 3,
|
|
2129
|
+
file: id,
|
|
2130
|
+
names: [],
|
|
2131
|
+
sources: [id],
|
|
2132
|
+
sourcesContent: [code],
|
|
2133
|
+
mappings: '',
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
return {
|
|
2137
|
+
code: rewritten.code,
|
|
2138
|
+
map: normalizedMap,
|
|
2139
|
+
}
|
|
2140
|
+
} catch {
|
|
2141
|
+
// Non-fatal: keep original code when rewrite fails.
|
|
2142
|
+
}
|
|
1656
2143
|
}
|
|
1657
2144
|
|
|
1658
|
-
// Note: we no longer rewrite imports here.
|
|
1659
|
-
// Dev uses per-importer mock-edge modules in resolveId so native ESM
|
|
1660
|
-
// has explicit named exports, and runtime diagnostics are handled by
|
|
1661
|
-
// the mock runtime proxy when those mocks are actually invoked.
|
|
1662
2145
|
return undefined
|
|
1663
2146
|
},
|
|
1664
2147
|
},
|