@tanstack/start-plugin-core 1.162.9 → 1.163.1

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.
@@ -21,21 +21,13 @@ import {
21
21
  } from './utils'
22
22
  import { collectMockExportNamesBySource } from './rewriteDeniedImports'
23
23
  import {
24
- MARKER_PREFIX,
25
- MOCK_EDGE_PREFIX,
26
- MOCK_MODULE_ID,
27
- MOCK_RUNTIME_PREFIX,
28
- RESOLVED_MARKER_PREFIX,
29
- RESOLVED_MOCK_BUILD_PREFIX,
30
- RESOLVED_MOCK_EDGE_PREFIX,
31
- RESOLVED_MOCK_MODULE_ID,
32
- RESOLVED_MOCK_RUNTIME_PREFIX,
33
- loadMarkerModule,
34
- loadMockEdgeModule,
35
- loadMockRuntimeModule,
36
- loadSilentMockModule,
24
+ MOCK_BUILD_PREFIX,
25
+ getResolvedVirtualModuleMatchers,
26
+ loadResolvedVirtualModule,
37
27
  makeMockEdgeModuleId,
38
28
  mockRuntimeModuleIdFromViolation,
29
+ resolveInternalVirtualModuleId,
30
+ resolvedMarkerVirtualModuleId,
39
31
  } from './virtualModules'
40
32
  import {
41
33
  ImportLocCache,
@@ -62,9 +54,6 @@ import type {
62
54
  import type { CompileStartFrameworkOptions, GetConfigFn } from '../types'
63
55
 
64
56
  const SERVER_FN_LOOKUP_QUERY = '?' + SERVER_FN_LOOKUP
65
- const RESOLVED_MARKER_SERVER_ONLY = resolveViteId(`${MARKER_PREFIX}server-only`)
66
- const RESOLVED_MARKER_CLIENT_ONLY = resolveViteId(`${MARKER_PREFIX}client-only`)
67
-
68
57
  const IMPORT_PROTECTION_DEBUG =
69
58
  process.env.TSR_IMPORT_PROTECTION_DEBUG === '1' ||
70
59
  process.env.TSR_IMPORT_PROTECTION_DEBUG === 'true'
@@ -82,7 +71,6 @@ function matchesDebugFilter(...values: Array<string>): boolean {
82
71
  return values.some((v) => v.includes(IMPORT_PROTECTION_DEBUG_FILTER))
83
72
  }
84
73
 
85
- export { RESOLVED_MOCK_MODULE_ID } from './virtualModules'
86
74
  export { rewriteDeniedImports } from './rewriteDeniedImports'
87
75
  export { dedupePatterns, extractImportSources } from './utils'
88
76
  export type { Pattern } from './utils'
@@ -110,10 +98,12 @@ interface PluginConfig {
110
98
  client: {
111
99
  specifiers: Array<CompiledMatcher>
112
100
  files: Array<CompiledMatcher>
101
+ excludeFiles: Array<CompiledMatcher>
113
102
  }
114
103
  server: {
115
104
  specifiers: Array<CompiledMatcher>
116
105
  files: Array<CompiledMatcher>
106
+ excludeFiles: Array<CompiledMatcher>
117
107
  }
118
108
  }
119
109
  includeMatchers: Array<CompiledMatcher>
@@ -214,6 +204,16 @@ interface DeferredBuildViolation {
214
204
  info: ViolationInfo
215
205
  /** Unique mock module ID assigned to this violation. */
216
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
217
  }
218
218
 
219
219
  /**
@@ -350,8 +350,8 @@ export function importProtectionPlugin(
350
350
  logMode: 'once',
351
351
  maxTraceDepth: 20,
352
352
  compiledRules: {
353
- client: { specifiers: [], files: [] },
354
- server: { specifiers: [], files: [] },
353
+ client: { specifiers: [], files: [], excludeFiles: [] },
354
+ server: { specifiers: [], files: [], excludeFiles: [] },
355
355
  },
356
356
  includeMatchers: [],
357
357
  excludeMatchers: [],
@@ -540,6 +540,7 @@ export function importProtectionPlugin(
540
540
  function getRulesForEnvironment(envName: string): {
541
541
  specifiers: Array<CompiledMatcher>
542
542
  files: Array<CompiledMatcher>
543
+ excludeFiles: Array<CompiledMatcher>
543
544
  } {
544
545
  const type = getEnvType(envName)
545
546
  return type === 'client'
@@ -815,27 +816,18 @@ export function importProtectionPlugin(
815
816
  env: EnvState,
816
817
  importerFile: string,
817
818
  info: ViolationInfo,
818
- mockReturnValue:
819
- | { id: string; syntheticNamedExports: boolean }
820
- | string
821
- | undefined,
819
+ mockReturnValue: string | undefined,
822
820
  ): void {
823
821
  getOrCreate(env.pendingViolations, importerFile, () => []).push({
824
822
  info,
825
- mockReturnValue:
826
- typeof mockReturnValue === 'string'
827
- ? mockReturnValue
828
- : (mockReturnValue?.id ?? ''),
823
+ mockReturnValue: mockReturnValue ?? '',
829
824
  })
830
825
  }
831
826
 
832
827
  /** Counter for generating unique per-violation mock module IDs in build mode. */
833
828
  let buildViolationCounter = 0
834
829
 
835
- type HandleViolationResult =
836
- | { id: string; syntheticNamedExports: boolean }
837
- | string
838
- | undefined
830
+ type HandleViolationResult = string | undefined
839
831
 
840
832
  async function handleViolation(
841
833
  ctx: ViolationReporter,
@@ -906,8 +898,22 @@ export function importProtectionPlugin(
906
898
 
907
899
  // Build: unique per-violation mock IDs so generateBundle can check
908
900
  // which violations survived tree-shaking (both mock and error mode).
909
- const mockId = `${RESOLVED_MOCK_BUILD_PREFIX}${buildViolationCounter++}`
910
- return { id: mockId, syntheticNamedExports: true }
901
+ // We wrap the base mock in a mock-edge module that provides explicit
902
+ // named exports Rolldown doesn't support Rollup's
903
+ // syntheticNamedExports, so without this named imports like
904
+ // `import { Foo } from "denied-pkg"` would fail with MISSING_EXPORT.
905
+ //
906
+ // Use the unresolved MOCK_BUILD_PREFIX (without \0) as the runtimeId
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),
916
+ )
911
917
  }
912
918
 
913
919
  /**
@@ -937,9 +943,14 @@ export function importProtectionPlugin(
937
943
 
938
944
  if (config.command === 'build') {
939
945
  // Build mode: store for generateBundle tree-shaking check.
940
- // The unique mock ID is inside `result`.
941
- const mockId = typeof result === 'string' ? result : (result?.id ?? '')
942
- env.deferredBuildViolations.push({ info, mockModuleId: mockId })
946
+ // The mock-edge module ID is returned as a plain string.
947
+ const mockId = result ?? ''
948
+ env.deferredBuildViolations.push({
949
+ info,
950
+ mockModuleId: mockId,
951
+ // For marker violations, check importer survival instead of mock.
952
+ checkModuleId: info.type === 'marker' ? info.importer : undefined,
953
+ })
943
954
  } else {
944
955
  // Dev mock: store for graph-reachability check.
945
956
  deferViolation(env, importerFile, info, result)
@@ -1021,20 +1032,28 @@ export function importProtectionPlugin(
1021
1032
  const clientFiles = userOpts?.client?.files
1022
1033
  ? [...userOpts.client.files]
1023
1034
  : [...defaults.client.files]
1035
+ const clientExcludeFiles = userOpts?.client?.excludeFiles
1036
+ ? [...userOpts.client.excludeFiles]
1037
+ : [...defaults.client.excludeFiles]
1024
1038
  const serverSpecifiers = userOpts?.server?.specifiers
1025
1039
  ? dedupePatterns([...userOpts.server.specifiers])
1026
1040
  : dedupePatterns([...defaults.server.specifiers])
1027
1041
  const serverFiles = userOpts?.server?.files
1028
1042
  ? [...userOpts.server.files]
1029
1043
  : [...defaults.server.files]
1044
+ const serverExcludeFiles = userOpts?.server?.excludeFiles
1045
+ ? [...userOpts.server.excludeFiles]
1046
+ : [...defaults.server.excludeFiles]
1030
1047
 
1031
1048
  config.compiledRules.client = {
1032
1049
  specifiers: compileMatchers(clientSpecifiers),
1033
1050
  files: compileMatchers(clientFiles),
1051
+ excludeFiles: compileMatchers(clientExcludeFiles),
1034
1052
  }
1035
1053
  config.compiledRules.server = {
1036
1054
  specifiers: compileMatchers(serverSpecifiers),
1037
1055
  files: compileMatchers(serverFiles),
1056
+ excludeFiles: compileMatchers(serverExcludeFiles),
1038
1057
  }
1039
1058
 
1040
1059
  // Include/exclude
@@ -1171,18 +1190,8 @@ export function importProtectionPlugin(
1171
1190
  }
1172
1191
 
1173
1192
  // Internal virtual modules
1174
- if (source === MOCK_MODULE_ID) {
1175
- return RESOLVED_MOCK_MODULE_ID
1176
- }
1177
- if (source.startsWith(MOCK_EDGE_PREFIX)) {
1178
- return resolveViteId(source)
1179
- }
1180
- if (source.startsWith(MOCK_RUNTIME_PREFIX)) {
1181
- return resolveViteId(source)
1182
- }
1183
- if (source.startsWith(MARKER_PREFIX)) {
1184
- return resolveViteId(source)
1185
- }
1193
+ const internalVirtualId = resolveInternalVirtualModuleId(source)
1194
+ if (internalVirtualId) return internalVirtualId
1186
1195
 
1187
1196
  if (!importer) {
1188
1197
  env.graph.addEntry(source)
@@ -1276,8 +1285,8 @@ export function importProtectionPlugin(
1276
1285
  }
1277
1286
 
1278
1287
  return markerKind === 'server'
1279
- ? RESOLVED_MARKER_SERVER_ONLY
1280
- : RESOLVED_MARKER_CLIENT_ONLY
1288
+ ? resolvedMarkerVirtualModuleId('server')
1289
+ : resolvedMarkerVirtualModuleId('client')
1281
1290
  }
1282
1291
 
1283
1292
  // Check if the importer is within our scope
@@ -1344,56 +1353,69 @@ export function importProtectionPlugin(
1344
1353
 
1345
1354
  env.graph.addEdge(resolved, normalizedImporter, source)
1346
1355
 
1347
- const fileMatch =
1348
- matchers.files.length > 0
1349
- ? matchesAny(relativePath, matchers.files)
1350
- : undefined
1356
+ // Skip file-based and marker-based denial for resolved paths that
1357
+ // match the per-environment `excludeFiles` patterns. By default
1358
+ // this includes `**/node_modules/**` so that third-party packages
1359
+ // using `.client.` / `.server.` in their filenames (e.g. react-tweet
1360
+ // exports `index.client.js`) are not treated as user-authored
1361
+ // environment boundaries. Users can override `excludeFiles` per
1362
+ // environment to narrow or widen this exclusion.
1363
+ const isExcludedFile =
1364
+ matchers.excludeFiles.length > 0 &&
1365
+ matchesAny(relativePath, matchers.excludeFiles)
1366
+
1367
+ if (!isExcludedFile) {
1368
+ const fileMatch =
1369
+ matchers.files.length > 0
1370
+ ? matchesAny(relativePath, matchers.files)
1371
+ : undefined
1372
+
1373
+ if (fileMatch) {
1374
+ const info = await buildViolationInfo(
1375
+ provider,
1376
+ env,
1377
+ envName,
1378
+ envType,
1379
+ importer,
1380
+ normalizedImporter,
1381
+ source,
1382
+ {
1383
+ type: 'file',
1384
+ pattern: fileMatch.pattern,
1385
+ resolved,
1386
+ message: `Import "${source}" (resolved to "${relativePath}") is denied in the ${envType} environment`,
1387
+ },
1388
+ )
1389
+ return reportOrDeferViolation(
1390
+ this,
1391
+ env,
1392
+ normalizedImporter,
1393
+ info,
1394
+ shouldDefer,
1395
+ isPreTransformResolve,
1396
+ )
1397
+ }
1351
1398
 
1352
- if (fileMatch) {
1353
- const info = await buildViolationInfo(
1399
+ const markerInfo = await buildMarkerViolationFromResolvedImport(
1354
1400
  provider,
1355
1401
  env,
1356
1402
  envName,
1357
1403
  envType,
1358
1404
  importer,
1359
- normalizedImporter,
1360
1405
  source,
1361
- {
1362
- type: 'file',
1363
- pattern: fileMatch.pattern,
1364
- resolved,
1365
- message: `Import "${source}" (resolved to "${relativePath}") is denied in the ${envType} environment`,
1366
- },
1367
- )
1368
- return reportOrDeferViolation(
1369
- this,
1370
- env,
1371
- normalizedImporter,
1372
- info,
1373
- shouldDefer,
1374
- isPreTransformResolve,
1375
- )
1376
- }
1377
-
1378
- const markerInfo = await buildMarkerViolationFromResolvedImport(
1379
- provider,
1380
- env,
1381
- envName,
1382
- envType,
1383
- importer,
1384
- source,
1385
- resolved,
1386
- relativePath,
1387
- )
1388
- if (markerInfo) {
1389
- return reportOrDeferViolation(
1390
- this,
1391
- env,
1392
- normalizedImporter,
1393
- markerInfo,
1394
- shouldDefer,
1395
- isPreTransformResolve,
1406
+ resolved,
1407
+ relativePath,
1396
1408
  )
1409
+ if (markerInfo) {
1410
+ return reportOrDeferViolation(
1411
+ this,
1412
+ env,
1413
+ normalizedImporter,
1414
+ markerInfo,
1415
+ shouldDefer,
1416
+ isPreTransformResolve,
1417
+ )
1418
+ }
1397
1419
  }
1398
1420
  }
1399
1421
 
@@ -1403,15 +1425,7 @@ export function importProtectionPlugin(
1403
1425
  load: {
1404
1426
  filter: {
1405
1427
  id: new RegExp(
1406
- [
1407
- RESOLVED_MOCK_MODULE_ID,
1408
- RESOLVED_MOCK_BUILD_PREFIX,
1409
- RESOLVED_MARKER_PREFIX,
1410
- RESOLVED_MOCK_EDGE_PREFIX,
1411
- RESOLVED_MOCK_RUNTIME_PREFIX,
1412
- ]
1413
- .map(escapeRegExp)
1414
- .join('|'),
1428
+ getResolvedVirtualModuleMatchers().map(escapeRegExp).join('|'),
1415
1429
  ),
1416
1430
  },
1417
1431
  handler(id) {
@@ -1424,32 +1438,7 @@ export function importProtectionPlugin(
1424
1438
  }
1425
1439
  }
1426
1440
 
1427
- if (id === RESOLVED_MOCK_MODULE_ID) {
1428
- return loadSilentMockModule()
1429
- }
1430
-
1431
- // Per-violation build mock modules — same silent mock code
1432
- if (id.startsWith(RESOLVED_MOCK_BUILD_PREFIX)) {
1433
- return loadSilentMockModule()
1434
- }
1435
-
1436
- if (id.startsWith(RESOLVED_MOCK_EDGE_PREFIX)) {
1437
- return loadMockEdgeModule(
1438
- id.slice(RESOLVED_MOCK_EDGE_PREFIX.length),
1439
- )
1440
- }
1441
-
1442
- if (id.startsWith(RESOLVED_MOCK_RUNTIME_PREFIX)) {
1443
- return loadMockRuntimeModule(
1444
- id.slice(RESOLVED_MOCK_RUNTIME_PREFIX.length),
1445
- )
1446
- }
1447
-
1448
- if (id.startsWith(RESOLVED_MARKER_PREFIX)) {
1449
- return loadMarkerModule()
1450
- }
1451
-
1452
- return undefined
1441
+ return loadResolvedVirtualModule(id)
1453
1442
  },
1454
1443
  },
1455
1444
 
@@ -1468,11 +1457,16 @@ export function importProtectionPlugin(
1468
1457
  }
1469
1458
  }
1470
1459
 
1471
- // Check each deferred violation: if its unique mock module survived
1460
+ // Check each deferred violation: if its check module survived
1472
1461
  // in the bundle, the import was NOT tree-shaken — real leak.
1473
1462
  const realViolations: Array<ViolationInfo> = []
1474
- for (const { info, mockModuleId } of env.deferredBuildViolations) {
1475
- if (!survivingModules.has(mockModuleId)) continue
1463
+ for (const {
1464
+ info,
1465
+ mockModuleId,
1466
+ checkModuleId,
1467
+ } of env.deferredBuildViolations) {
1468
+ const checkId = checkModuleId ?? mockModuleId
1469
+ if (!survivingModules.has(checkId)) continue
1476
1470
 
1477
1471
  if (config.onViolation) {
1478
1472
  const result = await config.onViolation(info)
@@ -1624,18 +1618,17 @@ export function importProtectionPlugin(
1624
1618
  name: 'tanstack-start-core:import-protection-mock-rewrite',
1625
1619
  enforce: 'pre',
1626
1620
 
1627
- // Only needed during dev. In build, we rely on Rollup's syntheticNamedExports.
1628
- apply: 'serve',
1629
-
1630
1621
  applyToEnvironment(env) {
1631
1622
  if (!config.enabled) return false
1632
- // Only needed in mock mode when not mocking, there is nothing to
1633
- // record. applyToEnvironment runs after configResolved, so
1634
- // config.effectiveBehavior is already set.
1635
- if (config.effectiveBehavior !== 'mock') return false
1636
- // We record expected named exports per importer in all Start Vite
1637
- // environments during dev so mock-edge modules can provide explicit
1638
- // ESM named exports.
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.
1639
1632
  return environmentNames.has(env.name)
1640
1633
  },
1641
1634
 
@@ -6,7 +6,7 @@ import { relativizePath } from './utils'
6
6
  import type { ViolationInfo } from './trace'
7
7
 
8
8
  export const MOCK_MODULE_ID = 'tanstack-start-import-protection:mock'
9
- export const RESOLVED_MOCK_MODULE_ID = resolveViteId(MOCK_MODULE_ID)
9
+ const RESOLVED_MOCK_MODULE_ID = resolveViteId(MOCK_MODULE_ID)
10
10
 
11
11
  /**
12
12
  * Per-violation mock prefix used in build+error mode.
@@ -14,17 +14,63 @@ export const RESOLVED_MOCK_MODULE_ID = resolveViteId(MOCK_MODULE_ID)
14
14
  * survived tree-shaking in `generateBundle`.
15
15
  */
16
16
  export const MOCK_BUILD_PREFIX = 'tanstack-start-import-protection:mock:build:'
17
- export const RESOLVED_MOCK_BUILD_PREFIX = resolveViteId(MOCK_BUILD_PREFIX)
17
+ const RESOLVED_MOCK_BUILD_PREFIX = resolveViteId(MOCK_BUILD_PREFIX)
18
18
 
19
19
  export const MOCK_EDGE_PREFIX = 'tanstack-start-import-protection:mock-edge:'
20
- export const RESOLVED_MOCK_EDGE_PREFIX = resolveViteId(MOCK_EDGE_PREFIX)
20
+ const RESOLVED_MOCK_EDGE_PREFIX = resolveViteId(MOCK_EDGE_PREFIX)
21
21
 
22
22
  export const MOCK_RUNTIME_PREFIX =
23
23
  'tanstack-start-import-protection:mock-runtime:'
24
- export const RESOLVED_MOCK_RUNTIME_PREFIX = resolveViteId(MOCK_RUNTIME_PREFIX)
24
+ const RESOLVED_MOCK_RUNTIME_PREFIX = resolveViteId(MOCK_RUNTIME_PREFIX)
25
25
 
26
26
  export const MARKER_PREFIX = 'tanstack-start-import-protection:marker:'
27
- export const RESOLVED_MARKER_PREFIX = resolveViteId(MARKER_PREFIX)
27
+ const RESOLVED_MARKER_PREFIX = resolveViteId(MARKER_PREFIX)
28
+
29
+ const RESOLVED_MARKER_SERVER_ONLY = resolveViteId(`${MARKER_PREFIX}server-only`)
30
+ const RESOLVED_MARKER_CLIENT_ONLY = resolveViteId(`${MARKER_PREFIX}client-only`)
31
+
32
+ export function resolvedMarkerVirtualModuleId(
33
+ kind: 'server' | 'client',
34
+ ): string {
35
+ return kind === 'server'
36
+ ? RESOLVED_MARKER_SERVER_ONLY
37
+ : RESOLVED_MARKER_CLIENT_ONLY
38
+ }
39
+
40
+ /**
41
+ * Convenience list for plugin `load` filters/handlers.
42
+ *
43
+ * Vite/Rollup call `load(id)` with the *resolved* virtual id (prefixed by `\0`).
44
+ * `resolveId(source)` sees the *unresolved* id/prefix (without `\0`).
45
+ */
46
+ export function getResolvedVirtualModuleMatchers(): ReadonlyArray<string> {
47
+ return RESOLVED_VIRTUAL_MODULE_MATCHERS
48
+ }
49
+
50
+ const RESOLVED_VIRTUAL_MODULE_MATCHERS = [
51
+ RESOLVED_MOCK_MODULE_ID,
52
+ RESOLVED_MOCK_BUILD_PREFIX,
53
+ RESOLVED_MOCK_EDGE_PREFIX,
54
+ RESOLVED_MOCK_RUNTIME_PREFIX,
55
+ RESOLVED_MARKER_PREFIX,
56
+ ] as const
57
+
58
+ /**
59
+ * Resolve import-protection's internal virtual module IDs.
60
+ *
61
+ * `resolveId(source)` sees *unresolved* ids/prefixes (no `\0`).
62
+ * Returning a resolved id (with `\0`) ensures Vite/Rollup route it to `load`.
63
+ */
64
+ export function resolveInternalVirtualModuleId(
65
+ source: string,
66
+ ): string | undefined {
67
+ if (source === MOCK_MODULE_ID) return RESOLVED_MOCK_MODULE_ID
68
+ if (source.startsWith(MOCK_EDGE_PREFIX)) return resolveViteId(source)
69
+ if (source.startsWith(MOCK_RUNTIME_PREFIX)) return resolveViteId(source)
70
+ if (source.startsWith(MOCK_BUILD_PREFIX)) return resolveViteId(source)
71
+ if (source.startsWith(MARKER_PREFIX)) return resolveViteId(source)
72
+ return undefined
73
+ }
28
74
 
29
75
  function toBase64Url(input: string): string {
30
76
  return Buffer.from(input, 'utf8').toString('base64url')
@@ -87,7 +133,8 @@ export function makeMockEdgeModuleId(
87
133
  * (property access for primitive coercion, calls, construction, sets).
88
134
  *
89
135
  * When `diagnostics` is omitted, the mock is completely silent — suitable
90
- * for the shared `MOCK_MODULE_ID` that uses `syntheticNamedExports`.
136
+ * for base mock modules (e.g. `MOCK_MODULE_ID` or per-violation build mocks)
137
+ * that are consumed by mock-edge modules providing explicit named exports.
91
138
  */
92
139
  function generateMockCode(diagnostics?: {
93
140
  meta: {
@@ -170,7 +217,8 @@ function __report(action, accessPath) {
170
217
  : ''
171
218
 
172
219
  return `
173
- ${preamble}function ${fnName}(name) {
220
+ ${preamble}/* @__NO_SIDE_EFFECTS__ */
221
+ function ${fnName}(name) {
174
222
  const fn = function () {};
175
223
  fn.prototype.name = name;
176
224
  const children = Object.create(null);
@@ -197,16 +245,13 @@ ${preamble}function ${fnName}(name) {
197
245
  });
198
246
  return proxy;
199
247
  }
200
- const mock = ${fnName}('mock');
248
+ const mock = /* @__PURE__ */ ${fnName}('mock');
201
249
  export default mock;
202
250
  `
203
251
  }
204
252
 
205
- export function loadSilentMockModule(): {
206
- syntheticNamedExports: boolean
207
- code: string
208
- } {
209
- return { syntheticNamedExports: true, code: generateMockCode() }
253
+ export function loadSilentMockModule(): { code: string } {
254
+ return { code: generateMockCode() }
210
255
  }
211
256
 
212
257
  export function loadMockEdgeModule(encodedPayload: string): { code: string } {
@@ -292,3 +337,30 @@ const MARKER_MODULE_RESULT = { code: 'export {}' } as const
292
337
  export function loadMarkerModule(): { code: string } {
293
338
  return MARKER_MODULE_RESULT
294
339
  }
340
+
341
+ export function loadResolvedVirtualModule(
342
+ id: string,
343
+ ): { code: string } | undefined {
344
+ if (id === RESOLVED_MOCK_MODULE_ID) {
345
+ return loadSilentMockModule()
346
+ }
347
+
348
+ // Per-violation build mock modules — same silent mock code
349
+ if (id.startsWith(RESOLVED_MOCK_BUILD_PREFIX)) {
350
+ return loadSilentMockModule()
351
+ }
352
+
353
+ if (id.startsWith(RESOLVED_MOCK_EDGE_PREFIX)) {
354
+ return loadMockEdgeModule(id.slice(RESOLVED_MOCK_EDGE_PREFIX.length))
355
+ }
356
+
357
+ if (id.startsWith(RESOLVED_MOCK_RUNTIME_PREFIX)) {
358
+ return loadMockRuntimeModule(id.slice(RESOLVED_MOCK_RUNTIME_PREFIX.length))
359
+ }
360
+
361
+ if (id.startsWith(RESOLVED_MARKER_PREFIX)) {
362
+ return loadMarkerModule()
363
+ }
364
+
365
+ return undefined
366
+ }
package/src/schema.ts CHANGED
@@ -16,6 +16,7 @@ const importProtectionBehaviorSchema = z.enum(['error', 'mock'])
16
16
  const importProtectionEnvRulesSchema = z.object({
17
17
  specifiers: z.array(patternSchema).optional(),
18
18
  files: z.array(patternSchema).optional(),
19
+ excludeFiles: z.array(patternSchema).optional(),
19
20
  })
20
21
 
21
22
  const importProtectionOptionsSchema = z