@tanstack/start-plugin-core 1.161.3 → 1.162.0
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/import-protection-plugin/defaults.d.ts +6 -4
- package/dist/esm/import-protection-plugin/defaults.js +3 -12
- package/dist/esm/import-protection-plugin/defaults.js.map +1 -1
- package/dist/esm/import-protection-plugin/plugin.d.ts +1 -1
- package/dist/esm/import-protection-plugin/plugin.js +488 -257
- package/dist/esm/import-protection-plugin/plugin.js.map +1 -1
- package/dist/esm/import-protection-plugin/postCompileUsage.d.ts +4 -2
- package/dist/esm/import-protection-plugin/postCompileUsage.js +31 -150
- package/dist/esm/import-protection-plugin/postCompileUsage.js.map +1 -1
- package/dist/esm/import-protection-plugin/rewriteDeniedImports.js +13 -9
- package/dist/esm/import-protection-plugin/rewriteDeniedImports.js.map +1 -1
- package/dist/esm/import-protection-plugin/sourceLocation.d.ts +32 -66
- package/dist/esm/import-protection-plugin/sourceLocation.js +129 -56
- package/dist/esm/import-protection-plugin/sourceLocation.js.map +1 -1
- package/dist/esm/import-protection-plugin/trace.d.ts +10 -0
- package/dist/esm/import-protection-plugin/trace.js +30 -44
- package/dist/esm/import-protection-plugin/trace.js.map +1 -1
- package/dist/esm/import-protection-plugin/utils.d.ts +8 -4
- package/dist/esm/import-protection-plugin/utils.js +43 -1
- package/dist/esm/import-protection-plugin/utils.js.map +1 -1
- package/dist/esm/import-protection-plugin/virtualModules.d.ts +7 -1
- package/dist/esm/import-protection-plugin/virtualModules.js +104 -135
- package/dist/esm/import-protection-plugin/virtualModules.js.map +1 -1
- package/package.json +8 -8
- package/src/import-protection-plugin/defaults.ts +8 -19
- package/src/import-protection-plugin/plugin.ts +776 -433
- package/src/import-protection-plugin/postCompileUsage.ts +57 -229
- package/src/import-protection-plugin/rewriteDeniedImports.ts +34 -42
- package/src/import-protection-plugin/sourceLocation.ts +184 -185
- package/src/import-protection-plugin/trace.ts +38 -49
- package/src/import-protection-plugin/utils.ts +62 -1
- package/src/import-protection-plugin/virtualModules.ts +163 -177
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as path from 'pathe'
|
|
2
1
|
import { normalizePath } from 'vite'
|
|
3
2
|
|
|
4
3
|
import { resolveViteId } from '../utils'
|
|
@@ -11,7 +10,15 @@ import {
|
|
|
11
10
|
} from './defaults'
|
|
12
11
|
import { findPostCompileUsagePos } from './postCompileUsage'
|
|
13
12
|
import { compileMatchers, matchesAny } from './matchers'
|
|
14
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
clearNormalizeFilePathCache,
|
|
15
|
+
dedupePatterns,
|
|
16
|
+
escapeRegExp,
|
|
17
|
+
extractImportSources,
|
|
18
|
+
getOrCreate,
|
|
19
|
+
normalizeFilePath,
|
|
20
|
+
relativizePath,
|
|
21
|
+
} from './utils'
|
|
15
22
|
import { collectMockExportNamesBySource } from './rewriteDeniedImports'
|
|
16
23
|
import {
|
|
17
24
|
MARKER_PREFIX,
|
|
@@ -30,16 +37,18 @@ import {
|
|
|
30
37
|
mockRuntimeModuleIdFromViolation,
|
|
31
38
|
} from './virtualModules'
|
|
32
39
|
import {
|
|
40
|
+
ImportLocCache,
|
|
33
41
|
addTraceImportLocations,
|
|
34
42
|
buildCodeSnippet,
|
|
35
43
|
buildLineIndex,
|
|
44
|
+
clearImportPatternCache,
|
|
36
45
|
findImportStatementLocationFromTransformed,
|
|
37
46
|
findPostCompileUsageLocation,
|
|
38
47
|
pickOriginalCodeFromSourcesContent,
|
|
39
48
|
} from './sourceLocation'
|
|
40
|
-
import type { PluginOption } from 'vite'
|
|
49
|
+
import type { PluginOption, ViteDevServer } from 'vite'
|
|
41
50
|
import type { CompiledMatcher } from './matchers'
|
|
42
|
-
import type { ViolationInfo } from './trace'
|
|
51
|
+
import type { Loc, TraceStep, ViolationInfo } from './trace'
|
|
43
52
|
import type {
|
|
44
53
|
SourceMapLike,
|
|
45
54
|
TransformResult,
|
|
@@ -51,10 +60,30 @@ import type {
|
|
|
51
60
|
} from '../schema'
|
|
52
61
|
import type { CompileStartFrameworkOptions, GetConfigFn } from '../types'
|
|
53
62
|
|
|
54
|
-
|
|
63
|
+
const SERVER_FN_LOOKUP_QUERY = '?' + SERVER_FN_LOOKUP
|
|
64
|
+
const RESOLVED_MARKER_SERVER_ONLY = resolveViteId(`${MARKER_PREFIX}server-only`)
|
|
65
|
+
const RESOLVED_MARKER_CLIENT_ONLY = resolveViteId(`${MARKER_PREFIX}client-only`)
|
|
66
|
+
|
|
67
|
+
const IMPORT_PROTECTION_DEBUG =
|
|
68
|
+
process.env.TSR_IMPORT_PROTECTION_DEBUG === '1' ||
|
|
69
|
+
process.env.TSR_IMPORT_PROTECTION_DEBUG === 'true'
|
|
70
|
+
const IMPORT_PROTECTION_DEBUG_FILTER =
|
|
71
|
+
process.env.TSR_IMPORT_PROTECTION_DEBUG_FILTER
|
|
72
|
+
|
|
73
|
+
function debugLog(...args: Array<unknown>) {
|
|
74
|
+
if (!IMPORT_PROTECTION_DEBUG) return
|
|
75
|
+
console.warn('[import-protection:debug]', ...args)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Check if a value matches the debug filter (when set). */
|
|
79
|
+
function matchesDebugFilter(...values: Array<string>): boolean {
|
|
80
|
+
if (!IMPORT_PROTECTION_DEBUG_FILTER) return true
|
|
81
|
+
return values.some((v) => v.includes(IMPORT_PROTECTION_DEBUG_FILTER))
|
|
82
|
+
}
|
|
83
|
+
|
|
55
84
|
export { RESOLVED_MOCK_MODULE_ID } from './virtualModules'
|
|
56
85
|
export { rewriteDeniedImports } from './rewriteDeniedImports'
|
|
57
|
-
export { dedupePatterns } from './utils'
|
|
86
|
+
export { dedupePatterns, extractImportSources } from './utils'
|
|
58
87
|
export type { Pattern } from './utils'
|
|
59
88
|
|
|
60
89
|
/**
|
|
@@ -68,6 +97,9 @@ interface PluginConfig {
|
|
|
68
97
|
srcDirectory: string
|
|
69
98
|
framework: CompileStartFrameworkOptions
|
|
70
99
|
|
|
100
|
+
/** Absolute, query-free entry file ids used for trace roots. */
|
|
101
|
+
entryFiles: Array<string>
|
|
102
|
+
|
|
71
103
|
effectiveBehavior: ImportProtectionBehavior
|
|
72
104
|
mockAccess: 'error' | 'warn' | 'off'
|
|
73
105
|
logMode: 'once' | 'always'
|
|
@@ -119,20 +151,65 @@ interface EnvState {
|
|
|
119
151
|
resolveCacheByFile: Map<string, Set<string>>
|
|
120
152
|
|
|
121
153
|
/** Import location cache. Key: `${importerFile}::${source}`. */
|
|
122
|
-
importLocCache:
|
|
123
|
-
string,
|
|
124
|
-
{ file?: string; line: number; column: number } | null
|
|
125
|
-
>
|
|
126
|
-
/** Reverse index: file path → Set of importLocCache keys for that file. */
|
|
127
|
-
importLocByFile: Map<string, Set<string>>
|
|
154
|
+
importLocCache: ImportLocCache
|
|
128
155
|
|
|
129
156
|
/** Deduplication of logged violations (no env prefix in key). */
|
|
130
157
|
seenViolations: Set<string>
|
|
131
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Modules transitively loaded during a `fetchModule(?SERVER_FN_LOOKUP)` call.
|
|
161
|
+
* In dev mode the compiler calls `fetchModule(id + '?' + SERVER_FN_LOOKUP)` to
|
|
162
|
+
* analyse a module's exports. The direct target carries the query parameter so
|
|
163
|
+
* `isPreTransformResolve` is `true`. But Vite also resolves the target's own
|
|
164
|
+
* imports (and their imports, etc.) with the plain file path as the importer —
|
|
165
|
+
* those would otherwise fire false-positive violations.
|
|
166
|
+
*
|
|
167
|
+
* We record every module resolved while walking a SERVER_FN_LOOKUP chain so
|
|
168
|
+
* that their child imports are also treated as pre-transform resolves.
|
|
169
|
+
*/
|
|
170
|
+
serverFnLookupModules: Set<string>
|
|
171
|
+
|
|
132
172
|
/** Transform result cache (code + composed sourcemap + original source). */
|
|
133
173
|
transformResultCache: Map<string, TransformResult>
|
|
134
174
|
/** Reverse index: physical file path → Set of transformResultCache keys. */
|
|
135
175
|
transformResultKeysByFile: Map<string, Set<string>>
|
|
176
|
+
|
|
177
|
+
/** Cached provider that reads from {@link transformResultCache}. */
|
|
178
|
+
transformResultProvider: TransformResultProvider
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Post-transform resolved imports. Populated by the transform-cache hook
|
|
182
|
+
* after resolving every import source found in the transformed code.
|
|
183
|
+
* Key: transform cache key (normalised module ID incl. query params).
|
|
184
|
+
* Value: set of resolved child file paths.
|
|
185
|
+
*/
|
|
186
|
+
postTransformImports: Map<string, Set<string>>
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Whether a `resolveId` call without an importer has been observed for this
|
|
190
|
+
* environment since `buildStart`. Vite calls `resolveId(source, undefined)`
|
|
191
|
+
* for true entry modules during a cold start. On warm start (`.vite` cache
|
|
192
|
+
* exists), Vite reuses its module graph and does NOT call `resolveId` for
|
|
193
|
+
* entries, so this stays `false`.
|
|
194
|
+
*
|
|
195
|
+
* When `false`, the import graph is considered unreliable (edges may be
|
|
196
|
+
* missing) and violations are reported immediately instead of deferred.
|
|
197
|
+
*/
|
|
198
|
+
hasSeenEntry: boolean
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Violations deferred in dev mock mode. Keyed by the violating importer's
|
|
202
|
+
* normalized file path. Violations are confirmed or discarded by the
|
|
203
|
+
* transform-cache hook once enough post-transform data is available to
|
|
204
|
+
* determine whether the importer is still reachable from an entry point.
|
|
205
|
+
*/
|
|
206
|
+
pendingViolations: Map<string, Array<PendingViolation>>
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
interface PendingViolation {
|
|
210
|
+
info: ViolationInfo
|
|
211
|
+
/** The mock module ID that resolveId already returned for this violation. */
|
|
212
|
+
mockReturnValue: string
|
|
136
213
|
}
|
|
137
214
|
|
|
138
215
|
/**
|
|
@@ -155,12 +232,115 @@ export interface ImportProtectionPluginOptions {
|
|
|
155
232
|
export function importProtectionPlugin(
|
|
156
233
|
opts: ImportProtectionPluginOptions,
|
|
157
234
|
): PluginOption {
|
|
235
|
+
let devServer: ViteDevServer | null = null
|
|
236
|
+
|
|
237
|
+
type ModuleGraphNode = {
|
|
238
|
+
id?: string | null
|
|
239
|
+
url?: string
|
|
240
|
+
importers: Set<ModuleGraphNode>
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Build an import trace using Vite's per-environment module graph, which
|
|
245
|
+
* is authoritative even on warm starts when the plugin's own ImportGraph
|
|
246
|
+
* may be incomplete (Vite skips resolveId for cached modules).
|
|
247
|
+
*/
|
|
248
|
+
function buildTraceFromModuleGraph(
|
|
249
|
+
envName: string,
|
|
250
|
+
env: EnvState,
|
|
251
|
+
targetFile: string,
|
|
252
|
+
): Array<TraceStep> | null {
|
|
253
|
+
if (!devServer) return null
|
|
254
|
+
const environment = devServer.environments[envName]
|
|
255
|
+
if (!environment) return null
|
|
256
|
+
|
|
257
|
+
const file = normalizeFilePath(targetFile)
|
|
258
|
+
const start = environment.moduleGraph.getModuleById(file)
|
|
259
|
+
if (!start) return null
|
|
260
|
+
|
|
261
|
+
// Resolve a module graph node to its normalized file path once and
|
|
262
|
+
// cache the result so BFS + reconstruction don't recompute.
|
|
263
|
+
const nodeIds = new Map<ModuleGraphNode, string>()
|
|
264
|
+
function nodeId(n: ModuleGraphNode): string {
|
|
265
|
+
let cached = nodeIds.get(n)
|
|
266
|
+
if (cached === undefined) {
|
|
267
|
+
cached = n.id
|
|
268
|
+
? normalizeFilePath(n.id)
|
|
269
|
+
: n.url
|
|
270
|
+
? normalizeFilePath(n.url)
|
|
271
|
+
: ''
|
|
272
|
+
nodeIds.set(n, cached)
|
|
273
|
+
}
|
|
274
|
+
return cached
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const queue: Array<ModuleGraphNode> = [start]
|
|
278
|
+
const visited = new Set<ModuleGraphNode>([start])
|
|
279
|
+
const parent = new Map<ModuleGraphNode, ModuleGraphNode>()
|
|
280
|
+
|
|
281
|
+
let entryRoot: ModuleGraphNode | null = null
|
|
282
|
+
let fallbackRoot: ModuleGraphNode | null = null
|
|
283
|
+
let qi = 0
|
|
284
|
+
while (qi < queue.length) {
|
|
285
|
+
const node = queue[qi++]!
|
|
286
|
+
const id = nodeId(node)
|
|
287
|
+
|
|
288
|
+
if (id && env.graph.entries.has(id)) {
|
|
289
|
+
entryRoot = node
|
|
290
|
+
break
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const importers = node.importers
|
|
294
|
+
if (importers.size === 0) {
|
|
295
|
+
if (!fallbackRoot) fallbackRoot = node
|
|
296
|
+
continue
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
for (const imp of importers) {
|
|
300
|
+
if (visited.has(imp)) continue
|
|
301
|
+
visited.add(imp)
|
|
302
|
+
parent.set(imp, node)
|
|
303
|
+
queue.push(imp)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const root = entryRoot ?? fallbackRoot
|
|
308
|
+
|
|
309
|
+
if (!root) return null
|
|
310
|
+
|
|
311
|
+
// Reconstruct: root -> ... -> start
|
|
312
|
+
const chain: Array<ModuleGraphNode> = []
|
|
313
|
+
let cur: ModuleGraphNode | undefined = root
|
|
314
|
+
for (let i = 0; i < config.maxTraceDepth + 2 && cur; i++) {
|
|
315
|
+
chain.push(cur)
|
|
316
|
+
if (cur === start) break
|
|
317
|
+
cur = parent.get(cur)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const steps: Array<TraceStep> = []
|
|
321
|
+
for (let i = 0; i < chain.length; i++) {
|
|
322
|
+
const id = nodeId(chain[i]!)
|
|
323
|
+
if (!id) continue
|
|
324
|
+
let specifier: string | undefined
|
|
325
|
+
if (i + 1 < chain.length) {
|
|
326
|
+
const nextId = nodeId(chain[i + 1]!)
|
|
327
|
+
if (nextId) {
|
|
328
|
+
specifier = env.graph.reverseEdges.get(nextId)?.get(id)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
steps.push(specifier ? { file: id, specifier } : { file: id })
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return steps.length ? steps : null
|
|
335
|
+
}
|
|
336
|
+
|
|
158
337
|
const config: PluginConfig = {
|
|
159
338
|
enabled: true,
|
|
160
339
|
root: '',
|
|
161
340
|
command: 'build',
|
|
162
341
|
srcDirectory: '',
|
|
163
342
|
framework: opts.framework,
|
|
343
|
+
entryFiles: [],
|
|
164
344
|
effectiveBehavior: 'error',
|
|
165
345
|
mockAccess: 'error',
|
|
166
346
|
logMode: 'once',
|
|
@@ -180,51 +360,6 @@ export function importProtectionPlugin(
|
|
|
180
360
|
const envStates = new Map<string, EnvState>()
|
|
181
361
|
const shared: SharedState = { fileMarkerKind: new Map() }
|
|
182
362
|
|
|
183
|
-
// ---------------------------------------------------------------------------
|
|
184
|
-
// Internal helpers
|
|
185
|
-
// ---------------------------------------------------------------------------
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Create a per-env `importLocCache` whose `.set` method automatically
|
|
189
|
-
* maintains the reverse index (`importLocByFile`) for O(1) invalidation
|
|
190
|
-
* in `hotUpdate`.
|
|
191
|
-
*
|
|
192
|
-
* Cache keys have the format `${importerFile}::${source}`.
|
|
193
|
-
*/
|
|
194
|
-
function createImportLocCache(
|
|
195
|
-
env: EnvState,
|
|
196
|
-
): Map<string, { file?: string; line: number; column: number } | null> {
|
|
197
|
-
const cache = new Map<
|
|
198
|
-
string,
|
|
199
|
-
{ file?: string; line: number; column: number } | null
|
|
200
|
-
>()
|
|
201
|
-
const originalSet = cache.set.bind(cache)
|
|
202
|
-
cache.set = function (key, value) {
|
|
203
|
-
originalSet(key, value)
|
|
204
|
-
const sepIdx = key.indexOf('::')
|
|
205
|
-
if (sepIdx !== -1) {
|
|
206
|
-
const file = key.slice(0, sepIdx)
|
|
207
|
-
let fileKeys = env.importLocByFile.get(file)
|
|
208
|
-
if (!fileKeys) {
|
|
209
|
-
fileKeys = new Set()
|
|
210
|
-
env.importLocByFile.set(file, fileKeys)
|
|
211
|
-
}
|
|
212
|
-
fileKeys.add(key)
|
|
213
|
-
}
|
|
214
|
-
return this
|
|
215
|
-
}
|
|
216
|
-
return cache
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function getMockEdgeExports(
|
|
220
|
-
env: EnvState,
|
|
221
|
-
importerId: string,
|
|
222
|
-
source: string,
|
|
223
|
-
): Array<string> {
|
|
224
|
-
const importerFile = normalizeFilePath(importerId)
|
|
225
|
-
return env.mockExportsByImporter.get(importerFile)?.get(source) ?? []
|
|
226
|
-
}
|
|
227
|
-
|
|
228
363
|
function getMarkerKindForFile(
|
|
229
364
|
fileId: string,
|
|
230
365
|
): 'server' | 'client' | undefined {
|
|
@@ -232,45 +367,61 @@ export function importProtectionPlugin(
|
|
|
232
367
|
return shared.fileMarkerKind.get(file)
|
|
233
368
|
}
|
|
234
369
|
|
|
370
|
+
type ViolationReporter = {
|
|
371
|
+
warn: (msg: string) => void
|
|
372
|
+
error: (msg: string) => never
|
|
373
|
+
}
|
|
374
|
+
|
|
235
375
|
/**
|
|
236
|
-
* Build
|
|
376
|
+
* Build the best available trace for a module and enrich each step with
|
|
377
|
+
* line/column locations. Tries the plugin's own ImportGraph first, then
|
|
378
|
+
* Vite's moduleGraph (authoritative on warm start), keeping whichever is
|
|
379
|
+
* longer. Annotates the last step with the denied specifier + location.
|
|
237
380
|
*
|
|
238
|
-
*
|
|
239
|
-
* the `tanstack-start-core:import-protection-transform-cache` plugin's
|
|
240
|
-
* transform hook.
|
|
381
|
+
* Shared by {@link buildViolationInfo} and {@link processPendingViolations}.
|
|
241
382
|
*/
|
|
242
|
-
function
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
383
|
+
async function rebuildAndAnnotateTrace(
|
|
384
|
+
provider: TransformResultProvider,
|
|
385
|
+
env: EnvState,
|
|
386
|
+
envName: string,
|
|
387
|
+
normalizedImporter: string,
|
|
388
|
+
specifier: string,
|
|
389
|
+
importerLoc: Loc | undefined,
|
|
390
|
+
traceOverride?: Array<TraceStep>,
|
|
391
|
+
): Promise<Array<TraceStep>> {
|
|
392
|
+
let trace =
|
|
393
|
+
traceOverride ??
|
|
394
|
+
buildTrace(env.graph, normalizedImporter, config.maxTraceDepth)
|
|
395
|
+
|
|
396
|
+
if (config.command === 'serve') {
|
|
397
|
+
const mgTrace = buildTraceFromModuleGraph(
|
|
398
|
+
envName,
|
|
399
|
+
env,
|
|
400
|
+
normalizedImporter,
|
|
401
|
+
)
|
|
402
|
+
if (mgTrace && mgTrace.length > trace.length) {
|
|
403
|
+
trace = mgTrace
|
|
404
|
+
}
|
|
259
405
|
}
|
|
260
|
-
|
|
406
|
+
await addTraceImportLocations(provider, trace, env.importLocCache)
|
|
261
407
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
408
|
+
if (trace.length > 0) {
|
|
409
|
+
const last = trace[trace.length - 1]!
|
|
410
|
+
if (!last.specifier) last.specifier = specifier
|
|
411
|
+
if (importerLoc && last.line == null) {
|
|
412
|
+
last.line = importerLoc.line
|
|
413
|
+
last.column = importerLoc.column
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return trace
|
|
265
418
|
}
|
|
266
419
|
|
|
267
420
|
/**
|
|
268
421
|
* Build a complete {@link ViolationInfo} with trace, location, and snippet.
|
|
269
422
|
*
|
|
270
423
|
* This is the single path that all violation types go through: specifier,
|
|
271
|
-
* file, and marker.
|
|
272
|
-
* `buildTrace` → `addTraceImportLocations` → location lookup → annotate →
|
|
273
|
-
* snippet that previously appeared 5 times in the codebase.
|
|
424
|
+
* file, and marker.
|
|
274
425
|
*/
|
|
275
426
|
async function buildViolationInfo(
|
|
276
427
|
provider: TransformResultProvider,
|
|
@@ -291,14 +442,8 @@ export function importProtectionPlugin(
|
|
|
291
442
|
| 'snippet'
|
|
292
443
|
| 'importerLoc'
|
|
293
444
|
>,
|
|
445
|
+
traceOverride?: Array<TraceStep>,
|
|
294
446
|
): Promise<ViolationInfo> {
|
|
295
|
-
const trace = buildTrace(
|
|
296
|
-
env.graph,
|
|
297
|
-
normalizedImporter,
|
|
298
|
-
config.maxTraceDepth,
|
|
299
|
-
)
|
|
300
|
-
await addTraceImportLocations(provider, trace, env.importLocCache)
|
|
301
|
-
|
|
302
447
|
const loc =
|
|
303
448
|
(await findPostCompileUsageLocation(
|
|
304
449
|
provider,
|
|
@@ -313,16 +458,15 @@ export function importProtectionPlugin(
|
|
|
313
458
|
env.importLocCache,
|
|
314
459
|
))
|
|
315
460
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
461
|
+
const trace = await rebuildAndAnnotateTrace(
|
|
462
|
+
provider,
|
|
463
|
+
env,
|
|
464
|
+
envName,
|
|
465
|
+
normalizedImporter,
|
|
466
|
+
source,
|
|
467
|
+
loc,
|
|
468
|
+
traceOverride,
|
|
469
|
+
)
|
|
326
470
|
|
|
327
471
|
const snippet = loc ? buildCodeSnippet(provider, importer, loc) : undefined
|
|
328
472
|
|
|
@@ -339,8 +483,14 @@ export function importProtectionPlugin(
|
|
|
339
483
|
}
|
|
340
484
|
}
|
|
341
485
|
|
|
342
|
-
|
|
343
|
-
|
|
486
|
+
/**
|
|
487
|
+
* Check if a resolved import violates marker restrictions (e.g. importing
|
|
488
|
+
* a server-only module in the client env). If so, build and return the
|
|
489
|
+
* {@link ViolationInfo} — the caller is responsible for reporting/deferring.
|
|
490
|
+
*
|
|
491
|
+
* Returns `undefined` when the resolved import has no marker conflict.
|
|
492
|
+
*/
|
|
493
|
+
async function buildMarkerViolationFromResolvedImport(
|
|
344
494
|
provider: TransformResultProvider,
|
|
345
495
|
env: EnvState,
|
|
346
496
|
envName: string,
|
|
@@ -349,8 +499,8 @@ export function importProtectionPlugin(
|
|
|
349
499
|
source: string,
|
|
350
500
|
resolvedId: string,
|
|
351
501
|
relativePath: string,
|
|
352
|
-
|
|
353
|
-
): Promise<
|
|
502
|
+
traceOverride?: Array<TraceStep>,
|
|
503
|
+
): Promise<ViolationInfo | undefined> {
|
|
354
504
|
const markerKind = getMarkerKindForFile(resolvedId)
|
|
355
505
|
const violates =
|
|
356
506
|
(envType === 'client' && markerKind === 'server') ||
|
|
@@ -359,7 +509,7 @@ export function importProtectionPlugin(
|
|
|
359
509
|
|
|
360
510
|
const normalizedImporter = normalizeFilePath(importer)
|
|
361
511
|
|
|
362
|
-
|
|
512
|
+
return buildViolationInfo(
|
|
363
513
|
provider,
|
|
364
514
|
env,
|
|
365
515
|
envName,
|
|
@@ -375,19 +525,8 @@ export function importProtectionPlugin(
|
|
|
375
525
|
? `Module "${relativePath}" is marked server-only but is imported in the client environment`
|
|
376
526
|
: `Module "${relativePath}" is marked client-only but is imported in the server environment`,
|
|
377
527
|
},
|
|
528
|
+
traceOverride,
|
|
378
529
|
)
|
|
379
|
-
|
|
380
|
-
return handleViolation.call(ctx, env, info, opts)
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function buildMockEdgeModuleId(
|
|
384
|
-
env: EnvState,
|
|
385
|
-
importerId: string,
|
|
386
|
-
source: string,
|
|
387
|
-
runtimeId: string,
|
|
388
|
-
): string {
|
|
389
|
-
const exports = getMockEdgeExports(env, importerId, source)
|
|
390
|
-
return makeMockEdgeModuleId(exports, source, runtimeId)
|
|
391
530
|
}
|
|
392
531
|
|
|
393
532
|
function getEnvType(envName: string): 'client' | 'server' {
|
|
@@ -416,7 +555,7 @@ export function importProtectionPlugin(
|
|
|
416
555
|
function getEnv(envName: string): EnvState {
|
|
417
556
|
let envState = envStates.get(envName)
|
|
418
557
|
if (!envState) {
|
|
419
|
-
const
|
|
558
|
+
const transformResultCache = new Map<string, TransformResult>()
|
|
420
559
|
envState = {
|
|
421
560
|
graph: new ImportGraph(),
|
|
422
561
|
deniedSources: new Set(),
|
|
@@ -424,50 +563,58 @@ export function importProtectionPlugin(
|
|
|
424
563
|
mockExportsByImporter: new Map(),
|
|
425
564
|
resolveCache: new Map(),
|
|
426
565
|
resolveCacheByFile: new Map(),
|
|
427
|
-
importLocCache: new
|
|
428
|
-
importLocByFile,
|
|
566
|
+
importLocCache: new ImportLocCache(),
|
|
429
567
|
seenViolations: new Set(),
|
|
430
|
-
transformResultCache
|
|
568
|
+
transformResultCache,
|
|
431
569
|
transformResultKeysByFile: new Map(),
|
|
570
|
+
transformResultProvider: {
|
|
571
|
+
getTransformResult(id: string) {
|
|
572
|
+
const fullKey = normalizePath(id)
|
|
573
|
+
const exact = transformResultCache.get(fullKey)
|
|
574
|
+
if (exact) return exact
|
|
575
|
+
const strippedKey = normalizeFilePath(id)
|
|
576
|
+
return strippedKey !== fullKey
|
|
577
|
+
? transformResultCache.get(strippedKey)
|
|
578
|
+
: undefined
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
postTransformImports: new Map(),
|
|
582
|
+
hasSeenEntry: false,
|
|
583
|
+
serverFnLookupModules: new Set(),
|
|
584
|
+
pendingViolations: new Map(),
|
|
432
585
|
}
|
|
433
|
-
// Install reverse-index-maintaining importLocCache
|
|
434
|
-
envState.importLocCache = createImportLocCache(envState)
|
|
435
586
|
envStates.set(envName, envState)
|
|
436
587
|
}
|
|
437
588
|
return envState
|
|
438
589
|
}
|
|
439
590
|
|
|
591
|
+
const shouldCheckImporterCache = new Map<string, boolean>()
|
|
440
592
|
function shouldCheckImporter(importer: string): boolean {
|
|
441
|
-
|
|
442
|
-
|
|
593
|
+
let result = shouldCheckImporterCache.get(importer)
|
|
594
|
+
if (result !== undefined) return result
|
|
595
|
+
|
|
596
|
+
const relativePath = relativizePath(importer, config.root)
|
|
443
597
|
|
|
444
|
-
// Check exclude first
|
|
445
598
|
if (
|
|
446
599
|
config.excludeMatchers.length > 0 &&
|
|
447
600
|
matchesAny(relativePath, config.excludeMatchers)
|
|
448
601
|
) {
|
|
449
|
-
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// Check ignore importers
|
|
453
|
-
if (
|
|
602
|
+
result = false
|
|
603
|
+
} else if (
|
|
454
604
|
config.ignoreImporterMatchers.length > 0 &&
|
|
455
605
|
matchesAny(relativePath, config.ignoreImporterMatchers)
|
|
456
606
|
) {
|
|
457
|
-
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
// Default: check if within srcDirectory
|
|
466
|
-
if (config.srcDirectory) {
|
|
467
|
-
return importer.startsWith(config.srcDirectory)
|
|
607
|
+
result = false
|
|
608
|
+
} else if (config.includeMatchers.length > 0) {
|
|
609
|
+
result = !!matchesAny(relativePath, config.includeMatchers)
|
|
610
|
+
} else if (config.srcDirectory) {
|
|
611
|
+
result = importer.startsWith(config.srcDirectory)
|
|
612
|
+
} else {
|
|
613
|
+
result = true
|
|
468
614
|
}
|
|
469
615
|
|
|
470
|
-
|
|
616
|
+
shouldCheckImporterCache.set(importer, result)
|
|
617
|
+
return result
|
|
471
618
|
}
|
|
472
619
|
|
|
473
620
|
function dedupeKey(
|
|
@@ -487,12 +634,279 @@ export function importProtectionPlugin(
|
|
|
487
634
|
}
|
|
488
635
|
|
|
489
636
|
function getRelativePath(absolutePath: string): string {
|
|
490
|
-
return normalizePath(
|
|
637
|
+
return relativizePath(normalizePath(absolutePath), config.root)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/** Register known Start entrypoints as trace roots for all environments. */
|
|
641
|
+
function registerEntries(): void {
|
|
642
|
+
const { resolvedStartConfig } = opts.getConfig()
|
|
643
|
+
for (const envDef of opts.environments) {
|
|
644
|
+
const envState = getEnv(envDef.name)
|
|
645
|
+
if (resolvedStartConfig.routerFilePath) {
|
|
646
|
+
envState.graph.addEntry(
|
|
647
|
+
normalizePath(resolvedStartConfig.routerFilePath),
|
|
648
|
+
)
|
|
649
|
+
}
|
|
650
|
+
if (resolvedStartConfig.startFilePath) {
|
|
651
|
+
envState.graph.addEntry(
|
|
652
|
+
normalizePath(resolvedStartConfig.startFilePath),
|
|
653
|
+
)
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function checkPostTransformReachability(
|
|
659
|
+
env: EnvState,
|
|
660
|
+
file: string,
|
|
661
|
+
): 'reachable' | 'unreachable' | 'unknown' {
|
|
662
|
+
const visited = new Set<string>()
|
|
663
|
+
const queue: Array<string> = [file]
|
|
664
|
+
let hasUnknownEdge = false
|
|
665
|
+
let qi = 0
|
|
666
|
+
|
|
667
|
+
while (qi < queue.length) {
|
|
668
|
+
const current = queue[qi++]!
|
|
669
|
+
if (visited.has(current)) continue
|
|
670
|
+
visited.add(current)
|
|
671
|
+
|
|
672
|
+
if (env.graph.entries.has(current)) {
|
|
673
|
+
return 'reachable'
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Walk reverse edges
|
|
677
|
+
const importers = env.graph.reverseEdges.get(current)
|
|
678
|
+
if (!importers) continue
|
|
679
|
+
|
|
680
|
+
for (const [parent] of importers) {
|
|
681
|
+
if (visited.has(parent)) continue
|
|
682
|
+
|
|
683
|
+
// Check all code-split variants for this parent. The edge is
|
|
684
|
+
// live if ANY variant's resolved imports include `current`.
|
|
685
|
+
const keySet = env.transformResultKeysByFile.get(parent)
|
|
686
|
+
let anyVariantCached = false
|
|
687
|
+
let edgeLive = false
|
|
688
|
+
|
|
689
|
+
if (keySet) {
|
|
690
|
+
for (const k of keySet) {
|
|
691
|
+
const resolvedImports = env.postTransformImports.get(k)
|
|
692
|
+
if (resolvedImports) {
|
|
693
|
+
anyVariantCached = true
|
|
694
|
+
if (resolvedImports.has(current)) {
|
|
695
|
+
edgeLive = true
|
|
696
|
+
break
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Fallback: direct file-path key
|
|
703
|
+
if (!anyVariantCached) {
|
|
704
|
+
const resolvedImports = env.postTransformImports.get(parent)
|
|
705
|
+
if (resolvedImports) {
|
|
706
|
+
anyVariantCached = true
|
|
707
|
+
if (resolvedImports.has(current)) {
|
|
708
|
+
edgeLive = true
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (!anyVariantCached) {
|
|
714
|
+
const hasTransformResult =
|
|
715
|
+
env.transformResultCache.has(parent) ||
|
|
716
|
+
(keySet ? keySet.size > 0 : false)
|
|
717
|
+
|
|
718
|
+
if (hasTransformResult) {
|
|
719
|
+
// Transform ran but postTransformImports not yet populated
|
|
720
|
+
hasUnknownEdge = true
|
|
721
|
+
continue
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Transform never ran — Vite served from cache (warm start).
|
|
725
|
+
// Conservatively treat edge as live.
|
|
726
|
+
queue.push(parent)
|
|
727
|
+
continue
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (edgeLive) {
|
|
731
|
+
queue.push(parent)
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return hasUnknownEdge ? 'unknown' : 'unreachable'
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Process pending violations for the given environment. Called from the
|
|
741
|
+
* transform-cache hook after each module transform is cached, because new
|
|
742
|
+
* transform data may allow us to confirm or discard pending violations.
|
|
743
|
+
*
|
|
744
|
+
* @param warnFn - `this.warn` from the transform hook context
|
|
745
|
+
*/
|
|
746
|
+
async function processPendingViolations(
|
|
747
|
+
env: EnvState,
|
|
748
|
+
warnFn: (msg: string) => void,
|
|
749
|
+
): Promise<void> {
|
|
750
|
+
if (env.pendingViolations.size === 0) return
|
|
751
|
+
|
|
752
|
+
const toDelete: Array<string> = []
|
|
753
|
+
|
|
754
|
+
for (const [file, violations] of env.pendingViolations) {
|
|
755
|
+
// On warm start, skip graph reachability — confirm immediately.
|
|
756
|
+
const status = env.hasSeenEntry
|
|
757
|
+
? checkPostTransformReachability(env, file)
|
|
758
|
+
: 'reachable'
|
|
759
|
+
|
|
760
|
+
if (status === 'reachable') {
|
|
761
|
+
for (const pv of violations) {
|
|
762
|
+
const key = dedupeKey(
|
|
763
|
+
pv.info.type,
|
|
764
|
+
pv.info.importer,
|
|
765
|
+
pv.info.specifier,
|
|
766
|
+
pv.info.resolved,
|
|
767
|
+
)
|
|
768
|
+
if (!hasSeen(env, key)) {
|
|
769
|
+
const freshTrace = await rebuildAndAnnotateTrace(
|
|
770
|
+
env.transformResultProvider,
|
|
771
|
+
env,
|
|
772
|
+
pv.info.env,
|
|
773
|
+
pv.info.importer,
|
|
774
|
+
pv.info.specifier,
|
|
775
|
+
pv.info.importerLoc,
|
|
776
|
+
)
|
|
777
|
+
if (freshTrace.length > pv.info.trace.length) {
|
|
778
|
+
pv.info.trace = freshTrace
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (config.onViolation) {
|
|
782
|
+
const result = config.onViolation(pv.info)
|
|
783
|
+
if (result === false) continue
|
|
784
|
+
}
|
|
785
|
+
warnFn(formatViolation(pv.info, config.root))
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
toDelete.push(file)
|
|
789
|
+
} else if (status === 'unreachable') {
|
|
790
|
+
toDelete.push(file)
|
|
791
|
+
}
|
|
792
|
+
// 'unknown' — keep pending for next transform-cache invocation.
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
for (const file of toDelete) {
|
|
796
|
+
env.pendingViolations.delete(file)
|
|
797
|
+
}
|
|
491
798
|
}
|
|
492
799
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
800
|
+
/**
|
|
801
|
+
* Record a violation as pending for later confirmation via graph
|
|
802
|
+
* reachability. Called from `resolveId` when `shouldDefer` is true.
|
|
803
|
+
*/
|
|
804
|
+
function deferViolation(
|
|
805
|
+
env: EnvState,
|
|
806
|
+
importerFile: string,
|
|
807
|
+
info: ViolationInfo,
|
|
808
|
+
mockReturnValue:
|
|
809
|
+
| { id: string; syntheticNamedExports: boolean }
|
|
810
|
+
| string
|
|
811
|
+
| undefined,
|
|
812
|
+
): void {
|
|
813
|
+
getOrCreate(env.pendingViolations, importerFile, () => []).push({
|
|
814
|
+
info,
|
|
815
|
+
mockReturnValue:
|
|
816
|
+
typeof mockReturnValue === 'string'
|
|
817
|
+
? mockReturnValue
|
|
818
|
+
: (mockReturnValue?.id ?? ''),
|
|
819
|
+
})
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function handleViolation(
|
|
823
|
+
ctx: ViolationReporter,
|
|
824
|
+
env: EnvState,
|
|
825
|
+
info: ViolationInfo,
|
|
826
|
+
violationOpts?: { silent?: boolean },
|
|
827
|
+
): { id: string; syntheticNamedExports: boolean } | string | undefined {
|
|
828
|
+
const key = dedupeKey(
|
|
829
|
+
info.type,
|
|
830
|
+
info.importer,
|
|
831
|
+
info.specifier,
|
|
832
|
+
info.resolved,
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
if (!violationOpts?.silent) {
|
|
836
|
+
if (config.onViolation) {
|
|
837
|
+
const result = config.onViolation(info)
|
|
838
|
+
if (result === false) {
|
|
839
|
+
return undefined
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const seen = hasSeen(env, key)
|
|
844
|
+
|
|
845
|
+
if (config.effectiveBehavior === 'error') {
|
|
846
|
+
if (!seen) ctx.error(formatViolation(info, config.root))
|
|
847
|
+
return undefined
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if (!seen) {
|
|
851
|
+
ctx.warn(formatViolation(info, config.root))
|
|
852
|
+
}
|
|
853
|
+
} else {
|
|
854
|
+
if (config.effectiveBehavior === 'error') {
|
|
855
|
+
return undefined
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
env.deniedSources.add(info.specifier)
|
|
860
|
+
getOrCreate(env.deniedEdges, info.importer, () => new Set<string>()).add(
|
|
861
|
+
info.specifier,
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
if (config.command === 'serve') {
|
|
865
|
+
const runtimeId = mockRuntimeModuleIdFromViolation(
|
|
866
|
+
info,
|
|
867
|
+
config.mockAccess,
|
|
868
|
+
config.root,
|
|
869
|
+
)
|
|
870
|
+
const importerFile = normalizeFilePath(info.importer)
|
|
871
|
+
const exports =
|
|
872
|
+
env.mockExportsByImporter.get(importerFile)?.get(info.specifier) ?? []
|
|
873
|
+
return resolveViteId(
|
|
874
|
+
makeMockEdgeModuleId(exports, info.specifier, runtimeId),
|
|
875
|
+
)
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Build: Rollup uses syntheticNamedExports
|
|
879
|
+
return { id: RESOLVED_MOCK_MODULE_ID, syntheticNamedExports: true }
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Unified violation dispatch: either defers or reports immediately.
|
|
884
|
+
*
|
|
885
|
+
* When `shouldDefer` is true, calls `handleViolation` silently to obtain
|
|
886
|
+
* the mock module ID, stores the violation as pending, and triggers
|
|
887
|
+
* `processPendingViolations`. Otherwise reports (or silences for
|
|
888
|
+
* pre-transform resolves) immediately.
|
|
889
|
+
*
|
|
890
|
+
* Returns the mock module ID / resolve result from `handleViolation`.
|
|
891
|
+
*/
|
|
892
|
+
async function reportOrDeferViolation(
|
|
893
|
+
ctx: ViolationReporter,
|
|
894
|
+
env: EnvState,
|
|
895
|
+
importerFile: string,
|
|
896
|
+
info: ViolationInfo,
|
|
897
|
+
shouldDefer: boolean,
|
|
898
|
+
isPreTransformResolve: boolean,
|
|
899
|
+
): Promise<ReturnType<typeof handleViolation>> {
|
|
900
|
+
if (shouldDefer) {
|
|
901
|
+
const result = handleViolation(ctx, env, info, { silent: true })
|
|
902
|
+
deferViolation(env, importerFile, info, result)
|
|
903
|
+
await processPendingViolations(env, ctx.warn.bind(ctx))
|
|
904
|
+
return result
|
|
905
|
+
}
|
|
906
|
+
return handleViolation(ctx, env, info, {
|
|
907
|
+
silent: isPreTransformResolve,
|
|
908
|
+
})
|
|
909
|
+
}
|
|
496
910
|
|
|
497
911
|
return [
|
|
498
912
|
{
|
|
@@ -513,10 +927,14 @@ export function importProtectionPlugin(
|
|
|
513
927
|
const { startConfig, resolvedStartConfig } = opts.getConfig()
|
|
514
928
|
config.srcDirectory = resolvedStartConfig.srcDirectory
|
|
515
929
|
|
|
930
|
+
config.entryFiles = [
|
|
931
|
+
resolvedStartConfig.routerFilePath,
|
|
932
|
+
resolvedStartConfig.startFilePath,
|
|
933
|
+
].filter((f): f is string => Boolean(f))
|
|
934
|
+
|
|
516
935
|
const userOpts: ImportProtectionOptions | undefined =
|
|
517
936
|
startConfig.importProtection
|
|
518
937
|
|
|
519
|
-
// Determine if plugin is enabled
|
|
520
938
|
if (userOpts?.enabled === false) {
|
|
521
939
|
config.enabled = false
|
|
522
940
|
return
|
|
@@ -524,7 +942,6 @@ export function importProtectionPlugin(
|
|
|
524
942
|
|
|
525
943
|
config.enabled = true
|
|
526
944
|
|
|
527
|
-
// Determine effective behavior
|
|
528
945
|
if (userOpts?.behavior) {
|
|
529
946
|
if (typeof userOpts.behavior === 'string') {
|
|
530
947
|
config.effectiveBehavior = userOpts.behavior
|
|
@@ -535,38 +952,27 @@ export function importProtectionPlugin(
|
|
|
535
952
|
: (userOpts.behavior.build ?? 'error')
|
|
536
953
|
}
|
|
537
954
|
} else {
|
|
538
|
-
// Defaults: dev='mock', build='error'
|
|
539
955
|
config.effectiveBehavior =
|
|
540
956
|
viteConfig.command === 'serve' ? 'mock' : 'error'
|
|
541
957
|
}
|
|
542
958
|
|
|
543
|
-
// Log mode
|
|
544
959
|
config.logMode = userOpts?.log ?? 'once'
|
|
545
|
-
|
|
546
|
-
// Mock runtime access diagnostics
|
|
547
960
|
config.mockAccess = userOpts?.mockAccess ?? 'error'
|
|
548
|
-
|
|
549
|
-
// Max trace depth
|
|
550
961
|
config.maxTraceDepth = userOpts?.maxTraceDepth ?? 20
|
|
962
|
+
if (userOpts?.onViolation) {
|
|
963
|
+
const fn = userOpts.onViolation
|
|
964
|
+
config.onViolation = (info) => fn(info)
|
|
965
|
+
}
|
|
551
966
|
|
|
552
|
-
|
|
553
|
-
config.onViolation = userOpts?.onViolation as
|
|
554
|
-
| ((info: ViolationInfo) => boolean | void)
|
|
555
|
-
| undefined
|
|
556
|
-
|
|
557
|
-
// Get default rules
|
|
558
|
-
const defaults = getDefaultImportProtectionRules(opts.framework)
|
|
967
|
+
const defaults = getDefaultImportProtectionRules()
|
|
559
968
|
|
|
560
|
-
//
|
|
561
|
-
//
|
|
562
|
-
// always include the framework defaults even when the user provides a
|
|
563
|
-
// custom list.
|
|
969
|
+
// Client specifier denies always include framework defaults even
|
|
970
|
+
// when the user provides a custom list.
|
|
564
971
|
const clientSpecifiers = dedupePatterns([
|
|
565
972
|
...defaults.client.specifiers,
|
|
566
973
|
...(userOpts?.client?.specifiers ?? []),
|
|
567
974
|
])
|
|
568
975
|
|
|
569
|
-
// For file patterns, user config overrides defaults.
|
|
570
976
|
const clientFiles = userOpts?.client?.files
|
|
571
977
|
? [...userOpts.client.files]
|
|
572
978
|
: [...defaults.client.files]
|
|
@@ -600,41 +1006,35 @@ export function importProtectionPlugin(
|
|
|
600
1006
|
}
|
|
601
1007
|
|
|
602
1008
|
// Marker specifiers
|
|
603
|
-
const markers = getMarkerSpecifiers(
|
|
1009
|
+
const markers = getMarkerSpecifiers()
|
|
604
1010
|
config.markerSpecifiers = {
|
|
605
1011
|
serverOnly: new Set(markers.serverOnly),
|
|
606
1012
|
clientOnly: new Set(markers.clientOnly),
|
|
607
1013
|
}
|
|
1014
|
+
},
|
|
608
1015
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
for (const envDef of opts.environments) {
|
|
612
|
-
const envState = getEnv(envDef.name)
|
|
613
|
-
|
|
614
|
-
if (resolvedStartConfig.routerFilePath) {
|
|
615
|
-
envState.graph.addEntry(
|
|
616
|
-
normalizePath(resolvedStartConfig.routerFilePath),
|
|
617
|
-
)
|
|
618
|
-
}
|
|
619
|
-
if (resolvedStartConfig.startFilePath) {
|
|
620
|
-
envState.graph.addEntry(
|
|
621
|
-
normalizePath(resolvedStartConfig.startFilePath),
|
|
622
|
-
)
|
|
623
|
-
}
|
|
624
|
-
}
|
|
1016
|
+
configureServer(server) {
|
|
1017
|
+
devServer = server
|
|
625
1018
|
},
|
|
626
1019
|
|
|
627
1020
|
buildStart() {
|
|
628
1021
|
if (!config.enabled) return
|
|
1022
|
+
// Clear memoization caches that grow unboundedly across builds
|
|
1023
|
+
clearNormalizeFilePathCache()
|
|
1024
|
+
clearImportPatternCache()
|
|
1025
|
+
shouldCheckImporterCache.clear()
|
|
1026
|
+
|
|
629
1027
|
// Clear per-env caches
|
|
630
1028
|
for (const envState of envStates.values()) {
|
|
631
1029
|
envState.resolveCache.clear()
|
|
632
1030
|
envState.resolveCacheByFile.clear()
|
|
633
1031
|
envState.importLocCache.clear()
|
|
634
|
-
envState.importLocByFile.clear()
|
|
635
1032
|
envState.seenViolations.clear()
|
|
636
1033
|
envState.transformResultCache.clear()
|
|
637
1034
|
envState.transformResultKeysByFile.clear()
|
|
1035
|
+
envState.postTransformImports.clear()
|
|
1036
|
+
envState.hasSeenEntry = false
|
|
1037
|
+
envState.serverFnLookupModules.clear()
|
|
638
1038
|
envState.graph.clear()
|
|
639
1039
|
envState.deniedSources.clear()
|
|
640
1040
|
envState.deniedEdges.clear()
|
|
@@ -644,21 +1044,7 @@ export function importProtectionPlugin(
|
|
|
644
1044
|
// Clear shared state
|
|
645
1045
|
shared.fileMarkerKind.clear()
|
|
646
1046
|
|
|
647
|
-
|
|
648
|
-
for (const envDef of opts.environments) {
|
|
649
|
-
const envState = getEnv(envDef.name)
|
|
650
|
-
const { resolvedStartConfig } = opts.getConfig()
|
|
651
|
-
if (resolvedStartConfig.routerFilePath) {
|
|
652
|
-
envState.graph.addEntry(
|
|
653
|
-
normalizePath(resolvedStartConfig.routerFilePath),
|
|
654
|
-
)
|
|
655
|
-
}
|
|
656
|
-
if (resolvedStartConfig.startFilePath) {
|
|
657
|
-
envState.graph.addEntry(
|
|
658
|
-
normalizePath(resolvedStartConfig.startFilePath),
|
|
659
|
-
)
|
|
660
|
-
}
|
|
661
|
-
}
|
|
1047
|
+
registerEntries()
|
|
662
1048
|
},
|
|
663
1049
|
|
|
664
1050
|
hotUpdate(ctx) {
|
|
@@ -672,14 +1058,7 @@ export function importProtectionPlugin(
|
|
|
672
1058
|
|
|
673
1059
|
// Invalidate per-env caches
|
|
674
1060
|
for (const envState of envStates.values()) {
|
|
675
|
-
|
|
676
|
-
const locKeys = envState.importLocByFile.get(importerFile)
|
|
677
|
-
if (locKeys) {
|
|
678
|
-
for (const key of locKeys) {
|
|
679
|
-
envState.importLocCache.delete(key)
|
|
680
|
-
}
|
|
681
|
-
envState.importLocByFile.delete(importerFile)
|
|
682
|
-
}
|
|
1061
|
+
envState.importLocCache.deleteByFile(importerFile)
|
|
683
1062
|
|
|
684
1063
|
// Invalidate resolve cache using reverse index
|
|
685
1064
|
const resolveKeys = envState.resolveCacheByFile.get(importerFile)
|
|
@@ -694,6 +1073,8 @@ export function importProtectionPlugin(
|
|
|
694
1073
|
envState.graph.invalidate(importerFile)
|
|
695
1074
|
envState.deniedEdges.delete(importerFile)
|
|
696
1075
|
envState.mockExportsByImporter.delete(importerFile)
|
|
1076
|
+
envState.serverFnLookupModules.delete(importerFile)
|
|
1077
|
+
envState.pendingViolations.delete(importerFile)
|
|
697
1078
|
|
|
698
1079
|
// Invalidate transform result cache for this file.
|
|
699
1080
|
const transformKeys =
|
|
@@ -701,11 +1082,13 @@ export function importProtectionPlugin(
|
|
|
701
1082
|
if (transformKeys) {
|
|
702
1083
|
for (const key of transformKeys) {
|
|
703
1084
|
envState.transformResultCache.delete(key)
|
|
1085
|
+
envState.postTransformImports.delete(key)
|
|
704
1086
|
}
|
|
705
1087
|
envState.transformResultKeysByFile.delete(importerFile)
|
|
706
1088
|
} else {
|
|
707
1089
|
// Fallback: at least clear the physical-file entry.
|
|
708
1090
|
envState.transformResultCache.delete(importerFile)
|
|
1091
|
+
envState.postTransformImports.delete(importerFile)
|
|
709
1092
|
}
|
|
710
1093
|
}
|
|
711
1094
|
}
|
|
@@ -713,13 +1096,36 @@ export function importProtectionPlugin(
|
|
|
713
1096
|
},
|
|
714
1097
|
|
|
715
1098
|
async resolveId(source, importer, _options) {
|
|
716
|
-
if (!config.enabled) return undefined
|
|
717
1099
|
const envName = this.environment.name
|
|
718
1100
|
const env = getEnv(envName)
|
|
719
1101
|
const envType = getEnvType(envName)
|
|
720
|
-
const provider =
|
|
1102
|
+
const provider = env.transformResultProvider
|
|
1103
|
+
const isScanResolve = !!(_options as Record<string, unknown>).scan
|
|
1104
|
+
|
|
1105
|
+
if (IMPORT_PROTECTION_DEBUG) {
|
|
1106
|
+
const importerPath = importer
|
|
1107
|
+
? normalizeFilePath(importer)
|
|
1108
|
+
: '(entry)'
|
|
1109
|
+
const isEntryResolve = !importer
|
|
1110
|
+
const filtered =
|
|
1111
|
+
IMPORT_PROTECTION_DEBUG_FILTER === 'entry'
|
|
1112
|
+
? isEntryResolve
|
|
1113
|
+
: matchesDebugFilter(source, importerPath)
|
|
1114
|
+
if (filtered) {
|
|
1115
|
+
debugLog('resolveId', {
|
|
1116
|
+
env: envName,
|
|
1117
|
+
envType,
|
|
1118
|
+
source,
|
|
1119
|
+
importer: importerPath,
|
|
1120
|
+
isEntryResolve,
|
|
1121
|
+
hasSeenEntry: env.hasSeenEntry,
|
|
1122
|
+
command: config.command,
|
|
1123
|
+
behavior: config.effectiveBehavior,
|
|
1124
|
+
})
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
721
1127
|
|
|
722
|
-
// Internal virtual modules
|
|
1128
|
+
// Internal virtual modules
|
|
723
1129
|
if (source === MOCK_MODULE_ID) {
|
|
724
1130
|
return RESOLVED_MOCK_MODULE_ID
|
|
725
1131
|
}
|
|
@@ -733,115 +1139,95 @@ export function importProtectionPlugin(
|
|
|
733
1139
|
return resolveViteId(source)
|
|
734
1140
|
}
|
|
735
1141
|
|
|
736
|
-
// Skip if no importer (entry points)
|
|
737
1142
|
if (!importer) {
|
|
738
|
-
// Track entry-ish modules so traces can terminate.
|
|
739
|
-
// Vite may pass virtual ids here; normalize but keep them.
|
|
740
1143
|
env.graph.addEntry(source)
|
|
1144
|
+
env.hasSeenEntry = true
|
|
741
1145
|
return undefined
|
|
742
1146
|
}
|
|
743
1147
|
|
|
744
|
-
// Skip virtual modules
|
|
745
1148
|
if (source.startsWith('\0') || source.startsWith('virtual:')) {
|
|
746
1149
|
return undefined
|
|
747
1150
|
}
|
|
748
1151
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
// 2. Vite's dep-optimizer scanner (`options.scan === true`) uses esbuild
|
|
757
|
-
// to discover bare imports for pre-bundling. esbuild reads raw source
|
|
758
|
-
// without running Vite transform hooks, so it also sees imports that
|
|
759
|
-
// the compiler would normally strip.
|
|
760
|
-
//
|
|
761
|
-
// In both cases the imports are NOT real client-side imports. We must
|
|
762
|
-
// suppress violation *reporting* (no warnings / errors) but still return
|
|
763
|
-
// mock module IDs so that transitive resolution doesn't blow up.
|
|
1152
|
+
const normalizedImporter = normalizeFilePath(importer)
|
|
1153
|
+
const isDirectLookup = importer.includes(SERVER_FN_LOOKUP_QUERY)
|
|
1154
|
+
|
|
1155
|
+
if (isDirectLookup) {
|
|
1156
|
+
env.serverFnLookupModules.add(normalizedImporter)
|
|
1157
|
+
}
|
|
1158
|
+
|
|
764
1159
|
const isPreTransformResolve =
|
|
765
|
-
|
|
766
|
-
|
|
1160
|
+
isDirectLookup ||
|
|
1161
|
+
env.serverFnLookupModules.has(normalizedImporter) ||
|
|
1162
|
+
isScanResolve
|
|
1163
|
+
|
|
1164
|
+
// Dev mock mode: defer violations until post-transform data is
|
|
1165
|
+
// available, then confirm/discard via graph reachability.
|
|
1166
|
+
const isDevMock =
|
|
1167
|
+
config.command === 'serve' && config.effectiveBehavior === 'mock'
|
|
1168
|
+
|
|
1169
|
+
const shouldDefer = isDevMock && !isPreTransformResolve
|
|
767
1170
|
|
|
768
1171
|
// Check if this is a marker import
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1172
|
+
const markerKind = config.markerSpecifiers.serverOnly.has(source)
|
|
1173
|
+
? ('server' as const)
|
|
1174
|
+
: config.markerSpecifiers.clientOnly.has(source)
|
|
1175
|
+
? ('client' as const)
|
|
1176
|
+
: undefined
|
|
1177
|
+
|
|
1178
|
+
if (markerKind) {
|
|
1179
|
+
const existing = shared.fileMarkerKind.get(normalizedImporter)
|
|
1180
|
+
if (existing && existing !== markerKind) {
|
|
774
1181
|
this.error(
|
|
775
|
-
`[import-protection] File "${getRelativePath(
|
|
1182
|
+
`[import-protection] File "${getRelativePath(normalizedImporter)}" has both server-only and client-only markers. This is not allowed.`,
|
|
776
1183
|
)
|
|
777
1184
|
}
|
|
778
|
-
shared.fileMarkerKind.set(
|
|
1185
|
+
shared.fileMarkerKind.set(normalizedImporter, markerKind)
|
|
1186
|
+
|
|
1187
|
+
const violatesEnv =
|
|
1188
|
+
(envType === 'client' && markerKind === 'server') ||
|
|
1189
|
+
(envType === 'server' && markerKind === 'client')
|
|
779
1190
|
|
|
780
|
-
|
|
781
|
-
if (envType === 'client') {
|
|
1191
|
+
if (violatesEnv) {
|
|
782
1192
|
const info = await buildViolationInfo(
|
|
783
1193
|
provider,
|
|
784
1194
|
env,
|
|
785
1195
|
envName,
|
|
786
1196
|
envType,
|
|
787
1197
|
importer,
|
|
788
|
-
|
|
1198
|
+
normalizedImporter,
|
|
789
1199
|
source,
|
|
790
1200
|
{
|
|
791
1201
|
type: 'marker',
|
|
792
|
-
message:
|
|
1202
|
+
message:
|
|
1203
|
+
markerKind === 'server'
|
|
1204
|
+
? `Module "${getRelativePath(normalizedImporter)}" is marked server-only but is imported in the client environment`
|
|
1205
|
+
: `Module "${getRelativePath(normalizedImporter)}" is marked client-only but is imported in the server environment`,
|
|
793
1206
|
},
|
|
794
1207
|
)
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
})
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// Return virtual empty module
|
|
801
|
-
return resolveViteId(`${MARKER_PREFIX}server-only`)
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
if (config.markerSpecifiers.clientOnly.has(source)) {
|
|
805
|
-
const resolvedImporter = normalizeFilePath(importer)
|
|
806
|
-
const existing = shared.fileMarkerKind.get(resolvedImporter)
|
|
807
|
-
if (existing && existing !== 'client') {
|
|
808
|
-
this.error(
|
|
809
|
-
`[import-protection] File "${getRelativePath(resolvedImporter)}" has both server-only and client-only markers. This is not allowed.`,
|
|
810
|
-
)
|
|
811
|
-
}
|
|
812
|
-
shared.fileMarkerKind.set(resolvedImporter, 'client')
|
|
813
|
-
|
|
814
|
-
if (envType === 'server') {
|
|
815
|
-
const info = await buildViolationInfo(
|
|
816
|
-
provider,
|
|
1208
|
+
await reportOrDeferViolation(
|
|
1209
|
+
this,
|
|
817
1210
|
env,
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
source,
|
|
823
|
-
{
|
|
824
|
-
type: 'marker',
|
|
825
|
-
message: `Module "${getRelativePath(resolvedImporter)}" is marked client-only but is imported in the server environment`,
|
|
826
|
-
},
|
|
1211
|
+
normalizedImporter,
|
|
1212
|
+
info,
|
|
1213
|
+
shouldDefer,
|
|
1214
|
+
isPreTransformResolve,
|
|
827
1215
|
)
|
|
828
|
-
handleViolation.call(this, env, info, {
|
|
829
|
-
silent: isPreTransformResolve,
|
|
830
|
-
})
|
|
831
1216
|
}
|
|
832
1217
|
|
|
833
|
-
return
|
|
1218
|
+
return markerKind === 'server'
|
|
1219
|
+
? RESOLVED_MARKER_SERVER_ONLY
|
|
1220
|
+
: RESOLVED_MARKER_CLIENT_ONLY
|
|
834
1221
|
}
|
|
835
1222
|
|
|
836
1223
|
// Check if the importer is within our scope
|
|
837
|
-
const normalizedImporter = normalizeFilePath(importer)
|
|
838
1224
|
if (!shouldCheckImporter(normalizedImporter)) {
|
|
839
1225
|
return undefined
|
|
840
1226
|
}
|
|
841
1227
|
|
|
842
1228
|
const matchers = getRulesForEnvironment(envName)
|
|
843
1229
|
|
|
844
|
-
// 1. Specifier-based denial
|
|
1230
|
+
// 1. Specifier-based denial
|
|
845
1231
|
const specifierMatch = matchesAny(source, matchers.specifiers)
|
|
846
1232
|
if (specifierMatch) {
|
|
847
1233
|
env.graph.addEdge(source, normalizedImporter, source)
|
|
@@ -856,46 +1242,48 @@ export function importProtectionPlugin(
|
|
|
856
1242
|
{
|
|
857
1243
|
type: 'specifier',
|
|
858
1244
|
pattern: specifierMatch.pattern,
|
|
859
|
-
message: `Import "${source}" is denied in the
|
|
1245
|
+
message: `Import "${source}" is denied in the ${envType} environment`,
|
|
860
1246
|
},
|
|
861
1247
|
)
|
|
862
|
-
return
|
|
863
|
-
|
|
864
|
-
|
|
1248
|
+
return reportOrDeferViolation(
|
|
1249
|
+
this,
|
|
1250
|
+
env,
|
|
1251
|
+
normalizedImporter,
|
|
1252
|
+
info,
|
|
1253
|
+
shouldDefer,
|
|
1254
|
+
isPreTransformResolve,
|
|
1255
|
+
)
|
|
865
1256
|
}
|
|
866
1257
|
|
|
867
|
-
// 2. Resolve the import (cached)
|
|
868
|
-
// marker checks, and graph edge tracking.
|
|
1258
|
+
// 2. Resolve the import (cached)
|
|
869
1259
|
const cacheKey = `${normalizedImporter}:${source}`
|
|
870
1260
|
let resolved: string | null
|
|
871
1261
|
|
|
872
1262
|
if (env.resolveCache.has(cacheKey)) {
|
|
873
|
-
resolved = env.resolveCache.get(cacheKey)
|
|
1263
|
+
resolved = env.resolveCache.get(cacheKey) ?? null
|
|
874
1264
|
} else {
|
|
875
1265
|
const result = await this.resolve(source, importer, {
|
|
876
1266
|
skipSelf: true,
|
|
877
1267
|
})
|
|
878
1268
|
resolved = result ? normalizeFilePath(result.id) : null
|
|
879
1269
|
env.resolveCache.set(cacheKey, resolved)
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
if (!fileKeys) {
|
|
886
|
-
fileKeys = new Set()
|
|
887
|
-
env.resolveCacheByFile.set(normalizedImporter, fileKeys)
|
|
888
|
-
}
|
|
889
|
-
fileKeys.add(cacheKey)
|
|
1270
|
+
getOrCreate(
|
|
1271
|
+
env.resolveCacheByFile,
|
|
1272
|
+
normalizedImporter,
|
|
1273
|
+
() => new Set(),
|
|
1274
|
+
).add(cacheKey)
|
|
890
1275
|
}
|
|
891
1276
|
|
|
892
1277
|
if (resolved) {
|
|
893
1278
|
const relativePath = getRelativePath(resolved)
|
|
894
1279
|
|
|
895
|
-
//
|
|
1280
|
+
// Propagate pre-transform status transitively
|
|
1281
|
+
if (isPreTransformResolve && !isScanResolve) {
|
|
1282
|
+
env.serverFnLookupModules.add(resolved)
|
|
1283
|
+
}
|
|
1284
|
+
|
|
896
1285
|
env.graph.addEdge(resolved, normalizedImporter, source)
|
|
897
1286
|
|
|
898
|
-
// File-based denial check
|
|
899
1287
|
const fileMatch =
|
|
900
1288
|
matchers.files.length > 0
|
|
901
1289
|
? matchesAny(relativePath, matchers.files)
|
|
@@ -914,17 +1302,20 @@ export function importProtectionPlugin(
|
|
|
914
1302
|
type: 'file',
|
|
915
1303
|
pattern: fileMatch.pattern,
|
|
916
1304
|
resolved,
|
|
917
|
-
message: `Import "${source}" (resolved to "${relativePath}") is denied in the
|
|
1305
|
+
message: `Import "${source}" (resolved to "${relativePath}") is denied in the ${envType} environment`,
|
|
918
1306
|
},
|
|
919
1307
|
)
|
|
920
|
-
return
|
|
921
|
-
|
|
922
|
-
|
|
1308
|
+
return reportOrDeferViolation(
|
|
1309
|
+
this,
|
|
1310
|
+
env,
|
|
1311
|
+
normalizedImporter,
|
|
1312
|
+
info,
|
|
1313
|
+
shouldDefer,
|
|
1314
|
+
isPreTransformResolve,
|
|
1315
|
+
)
|
|
923
1316
|
}
|
|
924
1317
|
|
|
925
|
-
|
|
926
|
-
const markerRes = await maybeReportMarkerViolationFromResolvedImport(
|
|
927
|
-
this,
|
|
1318
|
+
const markerInfo = await buildMarkerViolationFromResolvedImport(
|
|
928
1319
|
provider,
|
|
929
1320
|
env,
|
|
930
1321
|
envName,
|
|
@@ -933,10 +1324,16 @@ export function importProtectionPlugin(
|
|
|
933
1324
|
source,
|
|
934
1325
|
resolved,
|
|
935
1326
|
relativePath,
|
|
936
|
-
{ silent: isPreTransformResolve },
|
|
937
1327
|
)
|
|
938
|
-
if (
|
|
939
|
-
return
|
|
1328
|
+
if (markerInfo) {
|
|
1329
|
+
return reportOrDeferViolation(
|
|
1330
|
+
this,
|
|
1331
|
+
env,
|
|
1332
|
+
normalizedImporter,
|
|
1333
|
+
markerInfo,
|
|
1334
|
+
shouldDefer,
|
|
1335
|
+
isPreTransformResolve,
|
|
1336
|
+
)
|
|
940
1337
|
}
|
|
941
1338
|
}
|
|
942
1339
|
|
|
@@ -946,11 +1343,26 @@ export function importProtectionPlugin(
|
|
|
946
1343
|
load: {
|
|
947
1344
|
filter: {
|
|
948
1345
|
id: new RegExp(
|
|
949
|
-
|
|
1346
|
+
[
|
|
1347
|
+
RESOLVED_MOCK_MODULE_ID,
|
|
1348
|
+
RESOLVED_MARKER_PREFIX,
|
|
1349
|
+
RESOLVED_MOCK_EDGE_PREFIX,
|
|
1350
|
+
RESOLVED_MOCK_RUNTIME_PREFIX,
|
|
1351
|
+
]
|
|
1352
|
+
.map(escapeRegExp)
|
|
1353
|
+
.join('|'),
|
|
950
1354
|
),
|
|
951
1355
|
},
|
|
952
1356
|
handler(id) {
|
|
953
|
-
if (
|
|
1357
|
+
if (IMPORT_PROTECTION_DEBUG) {
|
|
1358
|
+
if (matchesDebugFilter(id)) {
|
|
1359
|
+
debugLog('load:handler', {
|
|
1360
|
+
env: this.environment.name,
|
|
1361
|
+
id: normalizePath(id),
|
|
1362
|
+
})
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
954
1366
|
if (id === RESOLVED_MOCK_MODULE_ID) {
|
|
955
1367
|
return loadSilentMockModule()
|
|
956
1368
|
}
|
|
@@ -976,25 +1388,9 @@ export function importProtectionPlugin(
|
|
|
976
1388
|
},
|
|
977
1389
|
},
|
|
978
1390
|
{
|
|
979
|
-
//
|
|
980
|
-
// `enforce: 'pre'`
|
|
981
|
-
//
|
|
982
|
-
// so that the `resolveId` hook (in the main plugin above) can look up
|
|
983
|
-
// the importer's transform result and map violation locations back to
|
|
984
|
-
// original source.
|
|
985
|
-
//
|
|
986
|
-
// Why not use `ctx.load()` in `resolveId`?
|
|
987
|
-
// - Vite dev: `this.load()` returns a ModuleInfo proxy that throws on
|
|
988
|
-
// `.code` access — code is not exposed.
|
|
989
|
-
// - Rollup build: `ModuleInfo` has `.code` but NOT `.map`, so we
|
|
990
|
-
// can't map generated positions back to original source.
|
|
991
|
-
//
|
|
992
|
-
// By caching in the transform hook we get both code and the composed
|
|
993
|
-
// sourcemap that chains all the way back to the original file.
|
|
994
|
-
//
|
|
995
|
-
// Performance: only files under `srcDirectory` are cached because only
|
|
996
|
-
// those can be importers in a violation. Third-party code in
|
|
997
|
-
// node_modules is never checked.
|
|
1391
|
+
// Captures transformed code + composed sourcemap for location mapping.
|
|
1392
|
+
// Runs after all `enforce: 'pre'` hooks (including the Start compiler).
|
|
1393
|
+
// Only files under `srcDirectory` are cached.
|
|
998
1394
|
name: 'tanstack-start-core:import-protection-transform-cache',
|
|
999
1395
|
|
|
1000
1396
|
applyToEnvironment(env) {
|
|
@@ -1008,33 +1404,32 @@ export function importProtectionPlugin(
|
|
|
1008
1404
|
include: [/\.[cm]?[tj]sx?($|\?)/],
|
|
1009
1405
|
},
|
|
1010
1406
|
},
|
|
1011
|
-
handler(code, id) {
|
|
1012
|
-
if (!config.enabled) return undefined
|
|
1407
|
+
async handler(code, id) {
|
|
1013
1408
|
const envName = this.environment.name
|
|
1014
1409
|
const file = normalizeFilePath(id)
|
|
1015
1410
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1411
|
+
if (IMPORT_PROTECTION_DEBUG) {
|
|
1412
|
+
if (matchesDebugFilter(file)) {
|
|
1413
|
+
debugLog('transform-cache', {
|
|
1414
|
+
env: envName,
|
|
1415
|
+
id: normalizePath(id),
|
|
1416
|
+
file,
|
|
1417
|
+
})
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1019
1421
|
if (!shouldCheckImporter(file)) {
|
|
1020
1422
|
return undefined
|
|
1021
1423
|
}
|
|
1022
1424
|
|
|
1023
|
-
// getCombinedSourcemap() returns the composed sourcemap
|
|
1024
|
-
// transform hooks that ran before this one. It includes
|
|
1025
|
-
// sourcesContent so we can extract original source later.
|
|
1425
|
+
// getCombinedSourcemap() returns the composed sourcemap
|
|
1026
1426
|
let map: SourceMapLike | undefined
|
|
1027
1427
|
try {
|
|
1028
1428
|
map = this.getCombinedSourcemap()
|
|
1029
1429
|
} catch {
|
|
1030
|
-
// No sourcemap available (e.g. virtual modules or modules
|
|
1031
|
-
// that no prior plugin produced a map for).
|
|
1032
1430
|
map = undefined
|
|
1033
1431
|
}
|
|
1034
1432
|
|
|
1035
|
-
// Extract the original source from sourcesContent right here.
|
|
1036
|
-
// Composed sourcemaps can contain multiple sources; try to pick the
|
|
1037
|
-
// entry that best matches this importer.
|
|
1038
1433
|
let originalCode: string | undefined
|
|
1039
1434
|
if (map?.sourcesContent) {
|
|
1040
1435
|
originalCode = pickOriginalCodeFromSourcesContent(
|
|
@@ -1044,15 +1439,16 @@ export function importProtectionPlugin(
|
|
|
1044
1439
|
)
|
|
1045
1440
|
}
|
|
1046
1441
|
|
|
1047
|
-
// Precompute a line index for fast index->line/col conversions.
|
|
1048
1442
|
const lineIndex = buildLineIndex(code)
|
|
1049
|
-
|
|
1050
|
-
// Key by the full normalized module ID including query params
|
|
1051
|
-
// (e.g. "src/routes/index.tsx?tsr-split=component") so that
|
|
1052
|
-
// virtual modules derived from the same physical file each get
|
|
1053
|
-
// their own cache entry.
|
|
1054
1443
|
const cacheKey = normalizePath(id)
|
|
1444
|
+
|
|
1055
1445
|
const envState = getEnv(envName)
|
|
1446
|
+
|
|
1447
|
+
// Propagate SERVER_FN_LOOKUP status before import-analysis
|
|
1448
|
+
if (id.includes(SERVER_FN_LOOKUP_QUERY)) {
|
|
1449
|
+
envState.serverFnLookupModules.add(file)
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1056
1452
|
envState.transformResultCache.set(cacheKey, {
|
|
1057
1453
|
code,
|
|
1058
1454
|
map,
|
|
@@ -1060,19 +1456,14 @@ export function importProtectionPlugin(
|
|
|
1060
1456
|
lineIndex,
|
|
1061
1457
|
})
|
|
1062
1458
|
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
}
|
|
1459
|
+
const keySet = getOrCreate(
|
|
1460
|
+
envState.transformResultKeysByFile,
|
|
1461
|
+
file,
|
|
1462
|
+
() => new Set<string>(),
|
|
1463
|
+
)
|
|
1069
1464
|
keySet.add(cacheKey)
|
|
1070
1465
|
|
|
1071
|
-
// Also store
|
|
1072
|
-
// physical file path (e.g. from trace steps in the import graph,
|
|
1073
|
-
// which normalize away query params) still find a result.
|
|
1074
|
-
// The last variant transformed wins, which is acceptable — trace
|
|
1075
|
-
// lookups are best-effort for line numbers.
|
|
1466
|
+
// Also store stripped-path entry for physical-file lookups.
|
|
1076
1467
|
if (cacheKey !== file) {
|
|
1077
1468
|
envState.transformResultCache.set(file, {
|
|
1078
1469
|
code,
|
|
@@ -1083,7 +1474,29 @@ export function importProtectionPlugin(
|
|
|
1083
1474
|
keySet.add(file)
|
|
1084
1475
|
}
|
|
1085
1476
|
|
|
1086
|
-
//
|
|
1477
|
+
// Resolve import sources to canonical paths for reachability checks.
|
|
1478
|
+
const importSources = extractImportSources(code)
|
|
1479
|
+
const resolvedChildren = new Set<string>()
|
|
1480
|
+
for (const src of importSources) {
|
|
1481
|
+
try {
|
|
1482
|
+
const resolved = await this.resolve(src, id, { skipSelf: true })
|
|
1483
|
+
if (resolved && !resolved.external) {
|
|
1484
|
+
const resolvedPath = normalizeFilePath(resolved.id)
|
|
1485
|
+
resolvedChildren.add(resolvedPath)
|
|
1486
|
+
// Populate import graph edges for warm-start trace accuracy
|
|
1487
|
+
envState.graph.addEdge(resolvedPath, file, src)
|
|
1488
|
+
}
|
|
1489
|
+
} catch {
|
|
1490
|
+
// Non-fatal
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
envState.postTransformImports.set(cacheKey, resolvedChildren)
|
|
1494
|
+
if (cacheKey !== file) {
|
|
1495
|
+
envState.postTransformImports.set(file, resolvedChildren)
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
await processPendingViolations(envState, this.warn.bind(this))
|
|
1499
|
+
|
|
1087
1500
|
return undefined
|
|
1088
1501
|
},
|
|
1089
1502
|
},
|
|
@@ -1115,7 +1528,6 @@ export function importProtectionPlugin(
|
|
|
1115
1528
|
},
|
|
1116
1529
|
},
|
|
1117
1530
|
handler(code, id) {
|
|
1118
|
-
if (!config.enabled) return undefined
|
|
1119
1531
|
const envName = this.environment.name
|
|
1120
1532
|
const envState = envStates.get(envName)
|
|
1121
1533
|
if (!envState) return undefined
|
|
@@ -1141,73 +1553,4 @@ export function importProtectionPlugin(
|
|
|
1141
1553
|
},
|
|
1142
1554
|
},
|
|
1143
1555
|
] satisfies Array<PluginOption>
|
|
1144
|
-
|
|
1145
|
-
// ---------------------------------------------------------------------------
|
|
1146
|
-
// Violation handling
|
|
1147
|
-
// ---------------------------------------------------------------------------
|
|
1148
|
-
|
|
1149
|
-
function handleViolation(
|
|
1150
|
-
this: { warn: (msg: string) => void; error: (msg: string) => never },
|
|
1151
|
-
env: EnvState,
|
|
1152
|
-
info: ViolationInfo,
|
|
1153
|
-
opts?: { silent?: boolean },
|
|
1154
|
-
): { id: string; syntheticNamedExports: boolean } | string | undefined {
|
|
1155
|
-
const key = dedupeKey(
|
|
1156
|
-
info.type,
|
|
1157
|
-
info.importer,
|
|
1158
|
-
info.specifier,
|
|
1159
|
-
info.resolved,
|
|
1160
|
-
)
|
|
1161
|
-
|
|
1162
|
-
if (!opts?.silent) {
|
|
1163
|
-
// Call user callback
|
|
1164
|
-
if (config.onViolation) {
|
|
1165
|
-
const result = config.onViolation(info)
|
|
1166
|
-
if (result === false) {
|
|
1167
|
-
return undefined
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
const seen = hasSeen(env, key)
|
|
1172
|
-
|
|
1173
|
-
if (config.effectiveBehavior === 'error') {
|
|
1174
|
-
if (!seen) this.error(formatViolation(info, config.root))
|
|
1175
|
-
return undefined
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
// Mock mode: log once, but always return the mock module.
|
|
1179
|
-
if (!seen) {
|
|
1180
|
-
this.warn(formatViolation(info, config.root))
|
|
1181
|
-
}
|
|
1182
|
-
} else {
|
|
1183
|
-
// Silent mode: in error behavior, skip entirely (no mock needed
|
|
1184
|
-
// for compiler-internal lookups); in mock mode, fall through to
|
|
1185
|
-
// return the mock module ID without logging.
|
|
1186
|
-
if (config.effectiveBehavior === 'error') {
|
|
1187
|
-
return undefined
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
env.deniedSources.add(info.specifier)
|
|
1192
|
-
let edgeSet = env.deniedEdges.get(info.importer)
|
|
1193
|
-
if (!edgeSet) {
|
|
1194
|
-
edgeSet = new Set<string>()
|
|
1195
|
-
env.deniedEdges.set(info.importer, edgeSet)
|
|
1196
|
-
}
|
|
1197
|
-
edgeSet.add(info.specifier)
|
|
1198
|
-
|
|
1199
|
-
if (config.command === 'serve') {
|
|
1200
|
-
const runtimeId = mockRuntimeModuleIdFromViolation(
|
|
1201
|
-
info,
|
|
1202
|
-
config.mockAccess,
|
|
1203
|
-
config.root,
|
|
1204
|
-
)
|
|
1205
|
-
return resolveViteId(
|
|
1206
|
-
buildMockEdgeModuleId(env, info.importer, info.specifier, runtimeId),
|
|
1207
|
-
)
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
// Build: Rollup can synthesize named exports.
|
|
1211
|
-
return { id: RESOLVED_MOCK_MODULE_ID, syntheticNamedExports: true }
|
|
1212
|
-
}
|
|
1213
1556
|
}
|