@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.
Files changed (47) hide show
  1. package/dist/esm/constants.d.ts +1 -0
  2. package/dist/esm/constants.js +2 -0
  3. package/dist/esm/constants.js.map +1 -1
  4. package/dist/esm/import-protection-plugin/ast.d.ts +3 -0
  5. package/dist/esm/import-protection-plugin/ast.js +8 -0
  6. package/dist/esm/import-protection-plugin/ast.js.map +1 -0
  7. package/dist/esm/import-protection-plugin/constants.d.ts +6 -0
  8. package/dist/esm/import-protection-plugin/constants.js +24 -0
  9. package/dist/esm/import-protection-plugin/constants.js.map +1 -0
  10. package/dist/esm/import-protection-plugin/extensionlessAbsoluteIdResolver.d.ts +22 -0
  11. package/dist/esm/import-protection-plugin/extensionlessAbsoluteIdResolver.js +95 -0
  12. package/dist/esm/import-protection-plugin/extensionlessAbsoluteIdResolver.js.map +1 -0
  13. package/dist/esm/import-protection-plugin/plugin.d.ts +2 -13
  14. package/dist/esm/import-protection-plugin/plugin.js +684 -299
  15. package/dist/esm/import-protection-plugin/plugin.js.map +1 -1
  16. package/dist/esm/import-protection-plugin/postCompileUsage.js +4 -2
  17. package/dist/esm/import-protection-plugin/postCompileUsage.js.map +1 -1
  18. package/dist/esm/import-protection-plugin/rewriteDeniedImports.d.ts +4 -5
  19. package/dist/esm/import-protection-plugin/rewriteDeniedImports.js +225 -3
  20. package/dist/esm/import-protection-plugin/rewriteDeniedImports.js.map +1 -1
  21. package/dist/esm/import-protection-plugin/sourceLocation.d.ts +4 -7
  22. package/dist/esm/import-protection-plugin/sourceLocation.js +18 -73
  23. package/dist/esm/import-protection-plugin/sourceLocation.js.map +1 -1
  24. package/dist/esm/import-protection-plugin/types.d.ts +94 -0
  25. package/dist/esm/import-protection-plugin/utils.d.ts +33 -1
  26. package/dist/esm/import-protection-plugin/utils.js +69 -3
  27. package/dist/esm/import-protection-plugin/utils.js.map +1 -1
  28. package/dist/esm/import-protection-plugin/virtualModules.d.ts +30 -2
  29. package/dist/esm/import-protection-plugin/virtualModules.js +66 -23
  30. package/dist/esm/import-protection-plugin/virtualModules.js.map +1 -1
  31. package/dist/esm/start-compiler-plugin/plugin.d.ts +2 -1
  32. package/dist/esm/start-compiler-plugin/plugin.js +1 -2
  33. package/dist/esm/start-compiler-plugin/plugin.js.map +1 -1
  34. package/package.json +4 -4
  35. package/src/constants.ts +2 -0
  36. package/src/import-protection-plugin/INTERNALS.md +462 -60
  37. package/src/import-protection-plugin/ast.ts +7 -0
  38. package/src/import-protection-plugin/constants.ts +25 -0
  39. package/src/import-protection-plugin/extensionlessAbsoluteIdResolver.ts +121 -0
  40. package/src/import-protection-plugin/plugin.ts +1080 -597
  41. package/src/import-protection-plugin/postCompileUsage.ts +8 -2
  42. package/src/import-protection-plugin/rewriteDeniedImports.ts +141 -9
  43. package/src/import-protection-plugin/sourceLocation.ts +19 -89
  44. package/src/import-protection-plugin/types.ts +103 -0
  45. package/src/import-protection-plugin/utils.ts +123 -4
  46. package/src/import-protection-plugin/virtualModules.ts +117 -31
  47. package/src/start-compiler-plugin/plugin.ts +7 -2
@@ -1,28 +1,41 @@
1
1
  import { normalizePath } from "vite";
2
2
  import { resolveViteId } from "../utils.js";
3
3
  import { VITE_ENVIRONMENT_NAMES } from "../constants.js";
4
- import { SERVER_FN_LOOKUP } from "../start-compiler-plugin/plugin.js";
5
4
  import { formatViolation, ImportGraph, buildTrace } from "./trace.js";
6
5
  import { getDefaultImportProtectionRules, getMarkerSpecifiers } from "./defaults.js";
7
- import { findPostCompileUsagePos } from "./postCompileUsage.js";
8
6
  import { matchesAny, compileMatchers } from "./matchers.js";
9
- import { escapeRegExp, normalizeFilePath, getOrCreate, clearNormalizeFilePathCache, dedupePatterns, extractImportSources, relativizePath } from "./utils.js";
10
- import { collectMockExportNamesBySource } from "./rewriteDeniedImports.js";
11
- import { loadResolvedVirtualModule, getResolvedVirtualModuleMatchers, resolveInternalVirtualModuleId, resolvedMarkerVirtualModuleId, mockRuntimeModuleIdFromViolation, makeMockEdgeModuleId, MOCK_BUILD_PREFIX } from "./virtualModules.js";
12
- import { clearImportPatternCache, pickOriginalCodeFromSourcesContent, buildLineIndex, ImportLocCache, findPostCompileUsageLocation, findImportStatementLocationFromTransformed, buildCodeSnippet, addTraceImportLocations } from "./sourceLocation.js";
13
- const SERVER_FN_LOOKUP_QUERY = "?" + SERVER_FN_LOOKUP;
14
- const IMPORT_PROTECTION_DEBUG = process.env.TSR_IMPORT_PROTECTION_DEBUG === "1" || process.env.TSR_IMPORT_PROTECTION_DEBUG === "true";
15
- const IMPORT_PROTECTION_DEBUG_FILTER = process.env.TSR_IMPORT_PROTECTION_DEBUG_FILTER;
16
- function debugLog(...args) {
17
- if (!IMPORT_PROTECTION_DEBUG) return;
18
- console.warn("[import-protection:debug]", ...args);
19
- }
20
- function matchesDebugFilter(...values) {
21
- if (!IMPORT_PROTECTION_DEBUG_FILTER) return true;
22
- return values.some((v) => v.includes(IMPORT_PROTECTION_DEBUG_FILTER));
23
- }
7
+ import { normalizeFilePath, matchesDebugFilter, debugLog, escapeRegExp, getOrCreate, clearNormalizeFilePathCache, shouldDeferViolation, dedupePatterns, relativizePath, extractImportSources, canonicalizeResolvedId, isInsideDirectory, buildSourceCandidates, buildResolutionCandidates } from "./utils.js";
8
+ import { collectNamedExports, rewriteDeniedImports, collectMockExportNamesBySource } from "./rewriteDeniedImports.js";
9
+ import { loadResolvedVirtualModule, getResolvedVirtualModuleMatchers, resolveInternalVirtualModuleId, resolvedMarkerVirtualModuleId, generateSelfContainedMockModule, mockRuntimeModuleIdFromViolation, generateDevSelfDenialModule, MOCK_BUILD_PREFIX, makeMockEdgeModuleId } from "./virtualModules.js";
10
+ import { ExtensionlessAbsoluteIdResolver } from "./extensionlessAbsoluteIdResolver.js";
11
+ import { IMPORT_PROTECTION_DEBUG, SERVER_FN_LOOKUP_QUERY, VITE_BROWSER_VIRTUAL_PREFIX } from "./constants.js";
12
+ import { pickOriginalCodeFromSourcesContent, buildLineIndex, ImportLocCache, buildCodeSnippet, findPostCompileUsageLocation, findImportStatementLocationFromTransformed, addTraceImportLocations } from "./sourceLocation.js";
24
13
  function importProtectionPlugin(opts) {
25
14
  let devServer = null;
15
+ const extensionlessIdResolver = new ExtensionlessAbsoluteIdResolver();
16
+ const resolveExtensionlessAbsoluteId = (id) => extensionlessIdResolver.resolve(id);
17
+ const importPatternCache = /* @__PURE__ */ new Map();
18
+ function findFirstImportSpecifierIndex(code, source) {
19
+ let patterns = importPatternCache.get(source);
20
+ if (!patterns) {
21
+ const escaped = escapeRegExp(source);
22
+ patterns = [
23
+ new RegExp(`\\bimport\\s+(['"])${escaped}\\1`),
24
+ new RegExp(`\\bfrom\\s+(['"])${escaped}\\1`),
25
+ new RegExp(`\\bimport\\s*\\(\\s*(['"])${escaped}\\1\\s*\\)`)
26
+ ];
27
+ importPatternCache.set(source, patterns);
28
+ }
29
+ let best = -1;
30
+ for (const re of patterns) {
31
+ const m = re.exec(code);
32
+ if (!m) continue;
33
+ const idx = m.index + m[0].indexOf(source);
34
+ if (idx === -1) continue;
35
+ if (best === -1 || idx < best) best = idx;
36
+ }
37
+ return best;
38
+ }
26
39
  function buildTraceFromModuleGraph(envName, env, targetFile) {
27
40
  if (!devServer) return null;
28
41
  const environment = devServer.environments[envName];
@@ -94,7 +107,6 @@ function importProtectionPlugin(opts) {
94
107
  command: "build",
95
108
  srcDirectory: "",
96
109
  framework: opts.framework,
97
- entryFiles: [],
98
110
  effectiveBehavior: "error",
99
111
  mockAccess: "error",
100
112
  logMode: "once",
@@ -112,10 +124,6 @@ function importProtectionPlugin(opts) {
112
124
  };
113
125
  const envStates = /* @__PURE__ */ new Map();
114
126
  const shared = { fileMarkerKind: /* @__PURE__ */ new Map() };
115
- function getMarkerKindForFile(fileId) {
116
- const file = normalizeFilePath(fileId);
117
- return shared.fileMarkerKind.get(file);
118
- }
119
127
  async function rebuildAndAnnotateTrace(provider, env, envName, normalizedImporter, specifier, importerLoc, traceOverride) {
120
128
  let trace = traceOverride ?? buildTrace(env.graph, normalizedImporter, config.maxTraceDepth);
121
129
  if (config.command === "serve") {
@@ -128,7 +136,12 @@ function importProtectionPlugin(opts) {
128
136
  trace = mgTrace;
129
137
  }
130
138
  }
131
- await addTraceImportLocations(provider, trace, env.importLocCache);
139
+ await addTraceImportLocations(
140
+ provider,
141
+ trace,
142
+ env.importLocCache,
143
+ findFirstImportSpecifierIndex
144
+ );
132
145
  if (trace.length > 0) {
133
146
  const last = trace[trace.length - 1];
134
147
  if (!last.specifier) last.specifier = specifier;
@@ -140,16 +153,16 @@ function importProtectionPlugin(opts) {
140
153
  return trace;
141
154
  }
142
155
  async function buildViolationInfo(provider, env, envName, envType, importer, normalizedImporter, source, overrides, traceOverride) {
143
- const loc = await findPostCompileUsageLocation(
144
- provider,
145
- importer,
156
+ const sourceCandidates = buildSourceCandidates(
146
157
  source,
147
- findPostCompileUsagePos
148
- ) || await findImportStatementLocationFromTransformed(
158
+ "resolved" in overrides && typeof overrides.resolved === "string" ? overrides.resolved : void 0,
159
+ config.root
160
+ );
161
+ const loc = await resolveImporterLocation(
149
162
  provider,
163
+ env,
150
164
  importer,
151
- source,
152
- env.importLocCache
165
+ sourceCandidates
153
166
  );
154
167
  const trace = await rebuildAndAnnotateTrace(
155
168
  provider,
@@ -173,8 +186,22 @@ function importProtectionPlugin(opts) {
173
186
  ...overrides
174
187
  };
175
188
  }
189
+ async function resolveImporterLocation(provider, env, importer, sourceCandidates) {
190
+ for (const candidate of sourceCandidates) {
191
+ const loc = await findPostCompileUsageLocation(provider, importer, candidate) || await findImportStatementLocationFromTransformed(
192
+ provider,
193
+ importer,
194
+ candidate,
195
+ env.importLocCache,
196
+ findFirstImportSpecifierIndex
197
+ );
198
+ if (loc) return loc;
199
+ }
200
+ return void 0;
201
+ }
176
202
  async function buildMarkerViolationFromResolvedImport(provider, env, envName, envType, importer, source, resolvedId, relativePath, traceOverride) {
177
- const markerKind = getMarkerKindForFile(resolvedId);
203
+ const normalizedResolvedId = normalizeFilePath(resolvedId);
204
+ const markerKind = shared.fileMarkerKind.get(normalizedResolvedId);
178
205
  const violates = envType === "client" && markerKind === "server" || envType === "server" && markerKind === "client";
179
206
  if (!violates) return void 0;
180
207
  const normalizedImporter = normalizeFilePath(importer);
@@ -188,8 +215,30 @@ function importProtectionPlugin(opts) {
188
215
  source,
189
216
  {
190
217
  type: "marker",
191
- resolved: normalizeFilePath(resolvedId),
192
- message: markerKind === "server" ? `Module "${relativePath}" is marked server-only but is imported in the client environment` : `Module "${relativePath}" is marked client-only but is imported in the server environment`
218
+ resolved: normalizedResolvedId,
219
+ message: buildMarkerViolationMessage(relativePath, markerKind)
220
+ },
221
+ traceOverride
222
+ );
223
+ }
224
+ function buildMarkerViolationMessage(relativePath, markerKind) {
225
+ return markerKind === "server" ? `Module "${relativePath}" is marked server-only but is imported in the client environment` : `Module "${relativePath}" is marked client-only but is imported in the server environment`;
226
+ }
227
+ async function buildFileViolationInfo(provider, env, envName, envType, importer, normalizedImporter, source, resolvedPath, pattern, traceOverride) {
228
+ const relativePath = getRelativePath(resolvedPath);
229
+ return buildViolationInfo(
230
+ provider,
231
+ env,
232
+ envName,
233
+ envType,
234
+ importer,
235
+ normalizedImporter,
236
+ source,
237
+ {
238
+ type: "file",
239
+ pattern,
240
+ resolved: resolvedPath,
241
+ message: `Import "${source}" (resolved to "${relativePath}") is denied in the ${envType} environment`
193
242
  },
194
243
  traceOverride
195
244
  );
@@ -201,6 +250,12 @@ function importProtectionPlugin(opts) {
201
250
  const type = getEnvType(envName);
202
251
  return type === "client" ? config.compiledRules.client : config.compiledRules.server;
203
252
  }
253
+ function checkFileDenial(relativePath, matchers) {
254
+ if (matchers.excludeFiles.length > 0 && matchesAny(relativePath, matchers.excludeFiles)) {
255
+ return void 0;
256
+ }
257
+ return matchers.files.length > 0 ? matchesAny(relativePath, matchers.files) : void 0;
258
+ }
204
259
  const environmentNames = /* @__PURE__ */ new Set([
205
260
  VITE_ENVIRONMENT_NAMES.client,
206
261
  VITE_ENVIRONMENT_NAMES.server
@@ -214,8 +269,6 @@ function importProtectionPlugin(opts) {
214
269
  const transformResultCache = /* @__PURE__ */ new Map();
215
270
  envState = {
216
271
  graph: new ImportGraph(),
217
- deniedSources: /* @__PURE__ */ new Set(),
218
- deniedEdges: /* @__PURE__ */ new Map(),
219
272
  mockExportsByImporter: /* @__PURE__ */ new Map(),
220
273
  resolveCache: /* @__PURE__ */ new Map(),
221
274
  resolveCacheByFile: /* @__PURE__ */ new Map(),
@@ -241,27 +294,137 @@ function importProtectionPlugin(opts) {
241
294
  }
242
295
  return envState;
243
296
  }
297
+ function findExportsInMap(exportMap, candidates) {
298
+ for (const candidate of candidates) {
299
+ const hit = exportMap.get(candidate);
300
+ if (hit && hit.length > 0) return hit;
301
+ }
302
+ return [];
303
+ }
304
+ function buildIdCandidates(id, extra) {
305
+ const set = new Set(buildResolutionCandidates(id));
306
+ if (extra) {
307
+ for (const c of buildResolutionCandidates(extra)) set.add(c);
308
+ set.add(resolveExtensionlessAbsoluteId(extra));
309
+ }
310
+ return Array.from(set);
311
+ }
312
+ async function resolveExportsForDeniedSpecifier(env, ctx, info, importerIdHint) {
313
+ const importerFile = normalizeFilePath(info.importer);
314
+ const specifierCandidates = buildIdCandidates(info.specifier, info.resolved);
315
+ let parsedBySource = env.mockExportsByImporter.get(importerFile);
316
+ if (!parsedBySource) {
317
+ const importerCode = env.transformResultProvider.getTransformResult(importerFile)?.code ?? (importerIdHint && ctx.getModuleInfo ? ctx.getModuleInfo(importerIdHint)?.code ?? void 0 : void 0);
318
+ if (typeof importerCode !== "string" || importerCode.length === 0)
319
+ return [];
320
+ try {
321
+ parsedBySource = collectMockExportNamesBySource(importerCode);
322
+ await recordMockExportsForImporter(
323
+ env,
324
+ importerFile,
325
+ parsedBySource,
326
+ async (src) => {
327
+ const cacheKey = `${importerFile}:${src}`;
328
+ if (env.resolveCache.has(cacheKey)) {
329
+ return env.resolveCache.get(cacheKey) ?? void 0;
330
+ }
331
+ if (!ctx.resolve) return void 0;
332
+ const resolved = await ctx.resolve(src, info.importer, {
333
+ skipSelf: true
334
+ });
335
+ if (!resolved || resolved.external) return void 0;
336
+ return resolved.id;
337
+ }
338
+ );
339
+ parsedBySource = env.mockExportsByImporter.get(importerFile) ?? parsedBySource;
340
+ } catch {
341
+ return [];
342
+ }
343
+ }
344
+ const direct = findExportsInMap(parsedBySource, specifierCandidates);
345
+ if (direct.length > 0) return direct;
346
+ const candidateSet = new Set(specifierCandidates);
347
+ for (const [sourceKey, names] of parsedBySource) {
348
+ if (!names.length) continue;
349
+ const resolvedId = await resolveSourceKey(
350
+ env,
351
+ ctx,
352
+ importerFile,
353
+ sourceKey,
354
+ info.importer
355
+ );
356
+ if (!resolvedId) continue;
357
+ const resolvedCandidates = buildIdCandidates(resolvedId);
358
+ resolvedCandidates.push(resolveExtensionlessAbsoluteId(resolvedId));
359
+ if (resolvedCandidates.some((v) => candidateSet.has(v))) {
360
+ return names;
361
+ }
362
+ }
363
+ return [];
364
+ }
365
+ async function resolveSourceKey(env, ctx, importerFile, sourceKey, importerId) {
366
+ const cacheKey = `${importerFile}:${sourceKey}`;
367
+ if (env.resolveCache.has(cacheKey)) {
368
+ return env.resolveCache.get(cacheKey) ?? void 0;
369
+ }
370
+ if (!ctx.resolve) return void 0;
371
+ try {
372
+ const resolved = await ctx.resolve(sourceKey, importerId, {
373
+ skipSelf: true
374
+ });
375
+ if (!resolved || resolved.external) return void 0;
376
+ return resolved.id;
377
+ } catch {
378
+ return void 0;
379
+ }
380
+ }
381
+ async function recordMockExportsForImporter(env, importerId, namesBySource, resolveSource) {
382
+ const importerFile = normalizeFilePath(importerId);
383
+ if (namesBySource.size === 0) return;
384
+ for (const [source, names] of namesBySource) {
385
+ try {
386
+ const resolvedId = await resolveSource(source);
387
+ if (!resolvedId) continue;
388
+ namesBySource.set(normalizeFilePath(resolvedId), names);
389
+ namesBySource.set(resolveExtensionlessAbsoluteId(resolvedId), names);
390
+ } catch {
391
+ }
392
+ }
393
+ const existing = env.mockExportsByImporter.get(importerFile);
394
+ if (!existing) {
395
+ env.mockExportsByImporter.set(importerFile, namesBySource);
396
+ return;
397
+ }
398
+ for (const [source, names] of namesBySource) {
399
+ const prev = existing.get(source);
400
+ if (!prev) {
401
+ existing.set(source, names);
402
+ continue;
403
+ }
404
+ const union = /* @__PURE__ */ new Set([...prev, ...names]);
405
+ existing.set(source, Array.from(union).sort());
406
+ }
407
+ }
244
408
  const shouldCheckImporterCache = /* @__PURE__ */ new Map();
245
409
  function shouldCheckImporter(importer) {
246
410
  let result = shouldCheckImporterCache.get(importer);
247
411
  if (result !== void 0) return result;
248
412
  const relativePath = relativizePath(importer, config.root);
249
- if (config.excludeMatchers.length > 0 && matchesAny(relativePath, config.excludeMatchers)) {
250
- result = false;
251
- } else if (config.ignoreImporterMatchers.length > 0 && matchesAny(relativePath, config.ignoreImporterMatchers)) {
413
+ const excluded = config.excludeMatchers.length > 0 && matchesAny(relativePath, config.excludeMatchers) || config.ignoreImporterMatchers.length > 0 && matchesAny(relativePath, config.ignoreImporterMatchers);
414
+ if (excluded) {
252
415
  result = false;
253
416
  } else if (config.includeMatchers.length > 0) {
254
417
  result = !!matchesAny(relativePath, config.includeMatchers);
255
418
  } else if (config.srcDirectory) {
256
- result = importer.startsWith(config.srcDirectory);
419
+ result = isInsideDirectory(importer, config.srcDirectory);
257
420
  } else {
258
421
  result = true;
259
422
  }
260
423
  shouldCheckImporterCache.set(importer, result);
261
424
  return result;
262
425
  }
263
- function dedupeKey(type, importer, specifier, resolved) {
264
- return `${type}:${importer}:${specifier}:${resolved ?? ""}`;
426
+ function dedupeKey(info) {
427
+ return `${info.type}:${info.importer}:${info.specifier}:${info.resolved ?? ""}`;
265
428
  }
266
429
  function hasSeen(env, key) {
267
430
  if (config.logMode === "always") return false;
@@ -272,6 +435,56 @@ function importProtectionPlugin(opts) {
272
435
  function getRelativePath(absolutePath) {
273
436
  return relativizePath(normalizePath(absolutePath), config.root);
274
437
  }
438
+ function clearEnvState(envState) {
439
+ envState.resolveCache.clear();
440
+ envState.resolveCacheByFile.clear();
441
+ envState.importLocCache.clear();
442
+ envState.seenViolations.clear();
443
+ envState.transformResultCache.clear();
444
+ envState.transformResultKeysByFile.clear();
445
+ envState.postTransformImports.clear();
446
+ envState.serverFnLookupModules.clear();
447
+ envState.pendingViolations.clear();
448
+ envState.deferredBuildViolations.length = 0;
449
+ envState.graph.clear();
450
+ envState.mockExportsByImporter.clear();
451
+ }
452
+ function invalidateFileFromEnv(envState, file) {
453
+ envState.importLocCache.deleteByFile(file);
454
+ const resolveKeys = envState.resolveCacheByFile.get(file);
455
+ if (resolveKeys) {
456
+ for (const key of resolveKeys) envState.resolveCache.delete(key);
457
+ envState.resolveCacheByFile.delete(file);
458
+ }
459
+ envState.graph.invalidate(file);
460
+ envState.mockExportsByImporter.delete(file);
461
+ envState.serverFnLookupModules.delete(file);
462
+ envState.pendingViolations.delete(file);
463
+ const transformKeys = envState.transformResultKeysByFile.get(file);
464
+ if (transformKeys) {
465
+ for (const key of transformKeys) {
466
+ envState.transformResultCache.delete(key);
467
+ envState.postTransformImports.delete(key);
468
+ }
469
+ envState.transformResultKeysByFile.delete(file);
470
+ } else {
471
+ envState.transformResultCache.delete(file);
472
+ envState.postTransformImports.delete(file);
473
+ }
474
+ }
475
+ function cacheTransformResult(envState, file, cacheKey, result) {
476
+ envState.transformResultCache.set(cacheKey, result);
477
+ const keySet = getOrCreate(
478
+ envState.transformResultKeysByFile,
479
+ file,
480
+ () => /* @__PURE__ */ new Set()
481
+ );
482
+ keySet.add(cacheKey);
483
+ if (cacheKey !== file) {
484
+ envState.transformResultCache.set(file, result);
485
+ keySet.add(file);
486
+ }
487
+ }
275
488
  function registerEntries() {
276
489
  const { resolvedStartConfig } = opts.getConfig();
277
490
  for (const envDef of opts.environments) {
@@ -288,6 +501,46 @@ function importProtectionPlugin(opts) {
288
501
  }
289
502
  }
290
503
  }
504
+ function getPostTransformImports(env, file) {
505
+ const keySet = env.transformResultKeysByFile.get(file);
506
+ let merged = null;
507
+ if (keySet) {
508
+ for (const k of keySet) {
509
+ if (k.includes(SERVER_FN_LOOKUP_QUERY)) continue;
510
+ const imports = env.postTransformImports.get(k);
511
+ if (imports) {
512
+ if (!merged) merged = new Set(imports);
513
+ else for (const v of imports) merged.add(v);
514
+ }
515
+ }
516
+ }
517
+ if (!merged) {
518
+ const imports = env.postTransformImports.get(file);
519
+ if (imports) merged = new Set(imports);
520
+ }
521
+ return merged;
522
+ }
523
+ function checkEdgeLiveness(env, parent, target) {
524
+ const keySet = env.transformResultKeysByFile.get(parent);
525
+ let anyVariantCached = false;
526
+ if (keySet) {
527
+ for (const k of keySet) {
528
+ if (k.includes(SERVER_FN_LOOKUP_QUERY)) continue;
529
+ const imports = env.postTransformImports.get(k);
530
+ if (imports) {
531
+ anyVariantCached = true;
532
+ if (imports.has(target)) return "live";
533
+ }
534
+ }
535
+ }
536
+ if (!anyVariantCached) {
537
+ const imports = env.postTransformImports.get(parent);
538
+ if (imports) return imports.has(target) ? "live" : "dead";
539
+ const hasTransformResult = env.transformResultCache.has(parent) || (keySet ? keySet.size > 0 : false);
540
+ return hasTransformResult ? "pending" : "no-data";
541
+ }
542
+ return "dead";
543
+ }
291
544
  function checkPostTransformReachability(env, file) {
292
545
  const visited = /* @__PURE__ */ new Set();
293
546
  const queue = [file];
@@ -304,147 +557,148 @@ function importProtectionPlugin(opts) {
304
557
  if (!importers) continue;
305
558
  for (const [parent] of importers) {
306
559
  if (visited.has(parent)) continue;
307
- const keySet = env.transformResultKeysByFile.get(parent);
308
- let anyVariantCached = false;
309
- let edgeLive = false;
310
- if (keySet) {
311
- for (const k of keySet) {
312
- if (k.includes(SERVER_FN_LOOKUP_QUERY)) continue;
313
- const resolvedImports = env.postTransformImports.get(k);
314
- if (resolvedImports) {
315
- anyVariantCached = true;
316
- if (resolvedImports.has(current)) {
317
- edgeLive = true;
318
- break;
319
- }
320
- }
321
- }
322
- }
323
- if (!anyVariantCached) {
324
- const resolvedImports = env.postTransformImports.get(parent);
325
- if (resolvedImports) {
326
- anyVariantCached = true;
327
- if (resolvedImports.has(current)) {
328
- edgeLive = true;
329
- }
330
- }
331
- }
332
- if (!anyVariantCached) {
333
- const hasTransformResult = env.transformResultCache.has(parent) || (keySet ? keySet.size > 0 : false);
334
- if (hasTransformResult) {
335
- hasUnknownEdge = true;
336
- continue;
337
- }
338
- queue.push(parent);
339
- continue;
340
- }
341
- if (edgeLive) {
560
+ const liveness = checkEdgeLiveness(env, parent, current);
561
+ if (liveness === "live" || liveness === "no-data") {
342
562
  queue.push(parent);
563
+ } else if (liveness === "pending") {
564
+ hasUnknownEdge = true;
343
565
  }
344
566
  }
345
567
  }
346
568
  return hasUnknownEdge ? "unknown" : "unreachable";
347
569
  }
570
+ function filterEdgeSurvival(env, file, violations) {
571
+ const postTransform = getPostTransformImports(env, file);
572
+ if (postTransform) {
573
+ const surviving = violations.filter(
574
+ (pv) => !pv.info.resolved || postTransform.has(pv.info.resolved)
575
+ );
576
+ if (surviving.length === 0) return "all-stripped";
577
+ env.pendingViolations.set(file, surviving);
578
+ return { active: surviving, edgeSurvivalApplied: true };
579
+ }
580
+ if (violations.some((pv) => pv.fromPreTransformResolve)) {
581
+ return "await-transform";
582
+ }
583
+ return { active: violations, edgeSurvivalApplied: false };
584
+ }
348
585
  async function processPendingViolations(env, warnFn) {
349
586
  if (env.pendingViolations.size === 0) return;
350
587
  const toDelete = [];
351
588
  for (const [file, violations] of env.pendingViolations) {
589
+ const filtered = filterEdgeSurvival(env, file, violations);
590
+ if (filtered === "all-stripped") {
591
+ toDelete.push(file);
592
+ continue;
593
+ }
594
+ if (filtered === "await-transform") continue;
595
+ const { active, edgeSurvivalApplied } = filtered;
352
596
  const status = env.graph.entries.size > 0 ? checkPostTransformReachability(env, file) : "unknown";
353
597
  if (status === "reachable") {
354
- for (const pv of violations) {
355
- const key = dedupeKey(
356
- pv.info.type,
357
- pv.info.importer,
358
- pv.info.specifier,
359
- pv.info.resolved
360
- );
361
- if (!hasSeen(env, key)) {
362
- const freshTrace = await rebuildAndAnnotateTrace(
363
- env.transformResultProvider,
364
- env,
365
- pv.info.env,
366
- pv.info.importer,
367
- pv.info.specifier,
368
- pv.info.importerLoc
369
- );
370
- if (freshTrace.length > pv.info.trace.length) {
371
- pv.info.trace = freshTrace;
372
- }
373
- if (config.onViolation) {
374
- const result = await config.onViolation(pv.info);
375
- if (result === false) continue;
376
- }
377
- warnFn(formatViolation(pv.info, config.root));
378
- }
598
+ for (const pv of active) {
599
+ await emitPendingViolation(env, warnFn, pv);
379
600
  }
380
601
  toDelete.push(file);
381
602
  } else if (status === "unreachable") {
382
603
  toDelete.push(file);
604
+ } else if (config.command === "serve") {
605
+ let emittedAny = false;
606
+ for (const pv of active) {
607
+ if (pv.fromPreTransformResolve) continue;
608
+ const shouldEmit = edgeSurvivalApplied || pv.info.type === "file" && !!pv.info.resolved && isInsideDirectory(pv.info.resolved, config.srcDirectory);
609
+ if (shouldEmit) {
610
+ emittedAny = await emitPendingViolation(env, warnFn, pv) || emittedAny;
611
+ }
612
+ }
613
+ if (emittedAny) {
614
+ toDelete.push(file);
615
+ }
383
616
  }
384
617
  }
385
618
  for (const file of toDelete) {
386
619
  env.pendingViolations.delete(file);
387
620
  }
388
621
  }
389
- function deferViolation(env, importerFile, info, mockReturnValue) {
622
+ async function emitPendingViolation(env, warnFn, pv) {
623
+ if (!pv.info.importerLoc) {
624
+ const sourceCandidates = buildSourceCandidates(
625
+ pv.info.specifier,
626
+ pv.info.resolved,
627
+ config.root
628
+ );
629
+ const loc = await resolveImporterLocation(
630
+ env.transformResultProvider,
631
+ env,
632
+ pv.info.importer,
633
+ sourceCandidates
634
+ );
635
+ if (loc) {
636
+ pv.info.importerLoc = loc;
637
+ pv.info.snippet = buildCodeSnippet(
638
+ env.transformResultProvider,
639
+ pv.info.importer,
640
+ loc
641
+ );
642
+ }
643
+ }
644
+ if (hasSeen(env, dedupeKey(pv.info))) {
645
+ return false;
646
+ }
647
+ const freshTrace = await rebuildAndAnnotateTrace(
648
+ env.transformResultProvider,
649
+ env,
650
+ pv.info.env,
651
+ pv.info.importer,
652
+ pv.info.specifier,
653
+ pv.info.importerLoc
654
+ );
655
+ if (freshTrace.length > pv.info.trace.length) {
656
+ pv.info.trace = freshTrace;
657
+ }
658
+ if (config.onViolation) {
659
+ const result = await config.onViolation(pv.info);
660
+ if (result === false) return false;
661
+ }
662
+ warnFn(formatViolation(pv.info, config.root));
663
+ return true;
664
+ }
665
+ function deferViolation(env, importerFile, info, isPreTransformResolve) {
390
666
  getOrCreate(env.pendingViolations, importerFile, () => []).push({
391
667
  info,
392
- mockReturnValue: mockReturnValue ?? ""
668
+ fromPreTransformResolve: isPreTransformResolve
393
669
  });
394
670
  }
395
671
  let buildViolationCounter = 0;
396
- async function handleViolation(ctx, env, info, violationOpts) {
397
- const key = dedupeKey(
398
- info.type,
399
- info.importer,
400
- info.specifier,
401
- info.resolved
402
- );
672
+ async function handleViolation(ctx, env, info, importerIdHint, violationOpts) {
403
673
  if (!violationOpts?.silent) {
404
674
  if (config.onViolation) {
405
675
  const result = await config.onViolation(info);
406
- if (result === false) {
407
- return void 0;
408
- }
676
+ if (result === false) return void 0;
409
677
  }
410
678
  if (config.effectiveBehavior === "error") {
411
679
  return ctx.error(formatViolation(info, config.root));
412
680
  }
413
- const seen = hasSeen(env, key);
414
- if (!seen) {
681
+ if (!hasSeen(env, dedupeKey(info))) {
415
682
  ctx.warn(formatViolation(info, config.root));
416
683
  }
417
- } else {
418
- if (config.effectiveBehavior === "error" && config.command !== "build") {
419
- return void 0;
420
- }
684
+ } else if (config.effectiveBehavior === "error" && config.command !== "build") {
685
+ return void 0;
421
686
  }
422
- env.deniedSources.add(info.specifier);
423
- getOrCreate(env.deniedEdges, info.importer, () => /* @__PURE__ */ new Set()).add(
424
- info.specifier
425
- );
426
- if (config.command === "serve") {
427
- const runtimeId = mockRuntimeModuleIdFromViolation(
428
- info,
429
- config.mockAccess,
430
- config.root
431
- );
432
- const importerFile2 = normalizeFilePath(info.importer);
433
- const exports2 = env.mockExportsByImporter.get(importerFile2)?.get(info.specifier) ?? [];
434
- return resolveViteId(
435
- makeMockEdgeModuleId(exports2, info.specifier, runtimeId)
436
- );
437
- }
438
- const baseMockId = `${MOCK_BUILD_PREFIX}${buildViolationCounter++}`;
439
- const importerFile = normalizeFilePath(info.importer);
440
- const exports = env.mockExportsByImporter.get(importerFile)?.get(info.specifier) ?? [];
441
- return resolveViteId(
442
- makeMockEdgeModuleId(exports, info.specifier, baseMockId)
687
+ if (info.type === "file") return info.resolved;
688
+ const exports = await resolveExportsForDeniedSpecifier(
689
+ env,
690
+ ctx,
691
+ info,
692
+ importerIdHint
443
693
  );
694
+ const baseMockId = config.command === "serve" ? mockRuntimeModuleIdFromViolation(info, config.mockAccess, config.root) : `${MOCK_BUILD_PREFIX}${buildViolationCounter++}`;
695
+ return resolveViteId(makeMockEdgeModuleId(exports, baseMockId));
444
696
  }
445
- async function reportOrDeferViolation(ctx, env, importerFile, info, shouldDefer, isPreTransformResolve) {
697
+ async function reportOrDeferViolation(ctx, env, importerFile, importerIdHint, info, shouldDefer, isPreTransformResolve) {
446
698
  if (shouldDefer) {
447
- const result = await handleViolation(ctx, env, info, { silent: true });
699
+ const result = await handleViolation(ctx, env, info, importerIdHint, {
700
+ silent: true
701
+ });
448
702
  if (config.command === "build") {
449
703
  const mockId = result ?? "";
450
704
  env.deferredBuildViolations.push({
@@ -454,12 +708,12 @@ function importProtectionPlugin(opts) {
454
708
  checkModuleId: info.type === "marker" ? info.importer : void 0
455
709
  });
456
710
  } else {
457
- deferViolation(env, importerFile, info, result);
711
+ deferViolation(env, importerFile, info, isPreTransformResolve);
458
712
  await processPendingViolations(env, ctx.warn.bind(ctx));
459
713
  }
460
714
  return result;
461
715
  }
462
- return await handleViolation(ctx, env, info, {
716
+ return handleViolation(ctx, env, info, importerIdHint, {
463
717
  silent: isPreTransformResolve
464
718
  });
465
719
  }
@@ -476,24 +730,17 @@ function importProtectionPlugin(opts) {
476
730
  config.command = viteConfig.command;
477
731
  const { startConfig, resolvedStartConfig } = opts.getConfig();
478
732
  config.srcDirectory = resolvedStartConfig.srcDirectory;
479
- config.entryFiles = [
480
- resolvedStartConfig.routerFilePath,
481
- resolvedStartConfig.startFilePath
482
- ].filter((f) => Boolean(f));
483
733
  const userOpts = startConfig.importProtection;
484
734
  if (userOpts?.enabled === false) {
485
735
  config.enabled = false;
486
736
  return;
487
737
  }
488
738
  config.enabled = true;
489
- if (userOpts?.behavior) {
490
- if (typeof userOpts.behavior === "string") {
491
- config.effectiveBehavior = userOpts.behavior;
492
- } else {
493
- config.effectiveBehavior = viteConfig.command === "serve" ? userOpts.behavior.dev ?? "mock" : userOpts.behavior.build ?? "error";
494
- }
739
+ const behavior = userOpts?.behavior;
740
+ if (typeof behavior === "string") {
741
+ config.effectiveBehavior = behavior;
495
742
  } else {
496
- config.effectiveBehavior = viteConfig.command === "serve" ? "mock" : "error";
743
+ config.effectiveBehavior = viteConfig.command === "serve" ? behavior?.dev ?? "mock" : behavior?.build ?? "error";
497
744
  }
498
745
  config.logMode = userOpts?.log ?? "once";
499
746
  config.mockAccess = userOpts?.mockAccess ?? "error";
@@ -503,36 +750,38 @@ function importProtectionPlugin(opts) {
503
750
  config.onViolation = (info) => fn(info);
504
751
  }
505
752
  const defaults = getDefaultImportProtectionRules();
753
+ const pick = (user, fallback) => user ? [...user] : [...fallback];
506
754
  const clientSpecifiers = dedupePatterns([
507
755
  ...defaults.client.specifiers,
508
756
  ...userOpts?.client?.specifiers ?? []
509
757
  ]);
510
- const clientFiles = userOpts?.client?.files ? [...userOpts.client.files] : [...defaults.client.files];
511
- const clientExcludeFiles = userOpts?.client?.excludeFiles ? [...userOpts.client.excludeFiles] : [...defaults.client.excludeFiles];
512
- const serverSpecifiers = userOpts?.server?.specifiers ? dedupePatterns([...userOpts.server.specifiers]) : dedupePatterns([...defaults.server.specifiers]);
513
- const serverFiles = userOpts?.server?.files ? [...userOpts.server.files] : [...defaults.server.files];
514
- const serverExcludeFiles = userOpts?.server?.excludeFiles ? [...userOpts.server.excludeFiles] : [...defaults.server.excludeFiles];
515
758
  config.compiledRules.client = {
516
759
  specifiers: compileMatchers(clientSpecifiers),
517
- files: compileMatchers(clientFiles),
518
- excludeFiles: compileMatchers(clientExcludeFiles)
760
+ files: compileMatchers(
761
+ pick(userOpts?.client?.files, defaults.client.files)
762
+ ),
763
+ excludeFiles: compileMatchers(
764
+ pick(userOpts?.client?.excludeFiles, defaults.client.excludeFiles)
765
+ )
519
766
  };
520
767
  config.compiledRules.server = {
521
- specifiers: compileMatchers(serverSpecifiers),
522
- files: compileMatchers(serverFiles),
523
- excludeFiles: compileMatchers(serverExcludeFiles)
768
+ specifiers: compileMatchers(
769
+ dedupePatterns(
770
+ pick(userOpts?.server?.specifiers, defaults.server.specifiers)
771
+ )
772
+ ),
773
+ files: compileMatchers(
774
+ pick(userOpts?.server?.files, defaults.server.files)
775
+ ),
776
+ excludeFiles: compileMatchers(
777
+ pick(userOpts?.server?.excludeFiles, defaults.server.excludeFiles)
778
+ )
524
779
  };
525
- if (userOpts?.include) {
526
- config.includeMatchers = compileMatchers(userOpts.include);
527
- }
528
- if (userOpts?.exclude) {
529
- config.excludeMatchers = compileMatchers(userOpts.exclude);
530
- }
531
- if (userOpts?.ignoreImporters) {
532
- config.ignoreImporterMatchers = compileMatchers(
533
- userOpts.ignoreImporters
534
- );
535
- }
780
+ config.includeMatchers = compileMatchers(userOpts?.include ?? []);
781
+ config.excludeMatchers = compileMatchers(userOpts?.exclude ?? []);
782
+ config.ignoreImporterMatchers = compileMatchers(
783
+ userOpts?.ignoreImporters ?? []
784
+ );
536
785
  const markers = getMarkerSpecifiers();
537
786
  config.markerSpecifiers = {
538
787
  serverOnly: new Set(markers.serverOnly),
@@ -545,23 +794,11 @@ function importProtectionPlugin(opts) {
545
794
  buildStart() {
546
795
  if (!config.enabled) return;
547
796
  clearNormalizeFilePathCache();
548
- clearImportPatternCache();
797
+ extensionlessIdResolver.clear();
798
+ importPatternCache.clear();
549
799
  shouldCheckImporterCache.clear();
550
800
  for (const envState of envStates.values()) {
551
- envState.resolveCache.clear();
552
- envState.resolveCacheByFile.clear();
553
- envState.importLocCache.clear();
554
- envState.seenViolations.clear();
555
- envState.transformResultCache.clear();
556
- envState.transformResultKeysByFile.clear();
557
- envState.postTransformImports.clear();
558
- envState.serverFnLookupModules.clear();
559
- envState.pendingViolations.clear();
560
- envState.deferredBuildViolations.length = 0;
561
- envState.graph.clear();
562
- envState.deniedSources.clear();
563
- envState.deniedEdges.clear();
564
- envState.mockExportsByImporter.clear();
801
+ clearEnvState(envState);
565
802
  }
566
803
  shared.fileMarkerKind.clear();
567
804
  registerEntries();
@@ -572,32 +809,10 @@ function importProtectionPlugin(opts) {
572
809
  if (mod.id) {
573
810
  const id = mod.id;
574
811
  const importerFile = normalizeFilePath(id);
812
+ extensionlessIdResolver.invalidateByFile(importerFile);
575
813
  shared.fileMarkerKind.delete(importerFile);
576
814
  for (const envState of envStates.values()) {
577
- envState.importLocCache.deleteByFile(importerFile);
578
- const resolveKeys = envState.resolveCacheByFile.get(importerFile);
579
- if (resolveKeys) {
580
- for (const key of resolveKeys) {
581
- envState.resolveCache.delete(key);
582
- }
583
- envState.resolveCacheByFile.delete(importerFile);
584
- }
585
- envState.graph.invalidate(importerFile);
586
- envState.deniedEdges.delete(importerFile);
587
- envState.mockExportsByImporter.delete(importerFile);
588
- envState.serverFnLookupModules.delete(importerFile);
589
- envState.pendingViolations.delete(importerFile);
590
- const transformKeys = envState.transformResultKeysByFile.get(importerFile);
591
- if (transformKeys) {
592
- for (const key of transformKeys) {
593
- envState.transformResultCache.delete(key);
594
- envState.postTransformImports.delete(key);
595
- }
596
- envState.transformResultKeysByFile.delete(importerFile);
597
- } else {
598
- envState.transformResultCache.delete(importerFile);
599
- envState.postTransformImports.delete(importerFile);
600
- }
815
+ invalidateFileFromEnv(envState, importerFile);
601
816
  }
602
817
  }
603
818
  }
@@ -611,7 +826,7 @@ function importProtectionPlugin(opts) {
611
826
  if (IMPORT_PROTECTION_DEBUG) {
612
827
  const importerPath = importer ? normalizeFilePath(importer) : "(entry)";
613
828
  const isEntryResolve = !importer;
614
- const filtered = IMPORT_PROTECTION_DEBUG_FILTER === "entry" ? isEntryResolve : matchesDebugFilter(source, importerPath);
829
+ const filtered = process.env.TSR_IMPORT_PROTECTION_DEBUG_FILTER === "entry" ? isEntryResolve : matchesDebugFilter(source, importerPath);
615
830
  if (filtered) {
616
831
  debugLog("resolveId", {
617
832
  env: envName,
@@ -619,8 +834,7 @@ function importProtectionPlugin(opts) {
619
834
  source,
620
835
  importer: importerPath,
621
836
  isEntryResolve,
622
- command: config.command,
623
- behavior: config.effectiveBehavior
837
+ command: config.command
624
838
  });
625
839
  }
626
840
  }
@@ -642,7 +856,20 @@ function importProtectionPlugin(opts) {
642
856
  const isPreTransformResolve = isDirectLookup || env.serverFnLookupModules.has(normalizedImporter) || isScanResolve;
643
857
  const isDevMock = config.command === "serve" && config.effectiveBehavior === "mock";
644
858
  const isBuild = config.command === "build";
645
- const shouldDefer = isDevMock && !isPreTransformResolve || isBuild;
859
+ const shouldDefer = shouldDeferViolation({ isBuild, isDevMock });
860
+ const resolveAgainstImporter = async () => {
861
+ const primary = await this.resolve(source, importer, {
862
+ skipSelf: true
863
+ });
864
+ if (primary) {
865
+ return canonicalizeResolvedId(
866
+ primary.id,
867
+ config.root,
868
+ resolveExtensionlessAbsoluteId
869
+ );
870
+ }
871
+ return null;
872
+ };
646
873
  const markerKind = config.markerSpecifiers.serverOnly.has(source) ? "server" : config.markerSpecifiers.clientOnly.has(source) ? "client" : void 0;
647
874
  if (markerKind) {
648
875
  const existing = shared.fileMarkerKind.get(normalizedImporter);
@@ -664,13 +891,17 @@ function importProtectionPlugin(opts) {
664
891
  source,
665
892
  {
666
893
  type: "marker",
667
- message: markerKind === "server" ? `Module "${getRelativePath(normalizedImporter)}" is marked server-only but is imported in the client environment` : `Module "${getRelativePath(normalizedImporter)}" is marked client-only but is imported in the server environment`
894
+ message: buildMarkerViolationMessage(
895
+ getRelativePath(normalizedImporter),
896
+ markerKind
897
+ )
668
898
  }
669
899
  );
670
900
  const markerResult = await reportOrDeferViolation(
671
901
  this,
672
902
  env,
673
903
  normalizedImporter,
904
+ importer,
674
905
  info,
675
906
  shouldDefer,
676
907
  isPreTransformResolve
@@ -679,6 +910,40 @@ function importProtectionPlugin(opts) {
679
910
  return markerResult;
680
911
  }
681
912
  }
913
+ const envRetroKey = `retro-marker:${normalizedImporter}`;
914
+ if (violatesEnv && !env.seenViolations.has(envRetroKey)) {
915
+ env.seenViolations.add(envRetroKey);
916
+ let retroDeferred = false;
917
+ const importersMap = env.graph.reverseEdges.get(normalizedImporter);
918
+ if (importersMap && importersMap.size > 0) {
919
+ for (const [importerFile, specifier] of importersMap) {
920
+ if (!specifier) continue;
921
+ if (!shouldCheckImporter(importerFile)) continue;
922
+ const markerInfo = await buildMarkerViolationFromResolvedImport(
923
+ provider,
924
+ env,
925
+ envName,
926
+ envType,
927
+ importerFile,
928
+ specifier,
929
+ normalizedImporter,
930
+ getRelativePath(normalizedImporter)
931
+ );
932
+ if (markerInfo) {
933
+ deferViolation(
934
+ env,
935
+ importerFile,
936
+ markerInfo,
937
+ isPreTransformResolve
938
+ );
939
+ retroDeferred = true;
940
+ }
941
+ }
942
+ }
943
+ if (retroDeferred) {
944
+ await processPendingViolations(env, this.warn.bind(this));
945
+ }
946
+ }
682
947
  return markerKind === "server" ? resolvedMarkerVirtualModuleId("server") : resolvedMarkerVirtualModuleId("client");
683
948
  }
684
949
  if (!shouldCheckImporter(normalizedImporter)) {
@@ -687,7 +952,9 @@ function importProtectionPlugin(opts) {
687
952
  const matchers = getRulesForEnvironment(envName);
688
953
  const specifierMatch = matchesAny(source, matchers.specifiers);
689
954
  if (specifierMatch) {
690
- env.graph.addEdge(source, normalizedImporter, source);
955
+ if (!isPreTransformResolve) {
956
+ env.graph.addEdge(source, normalizedImporter, source);
957
+ }
691
958
  const info = await buildViolationInfo(
692
959
  provider,
693
960
  env,
@@ -702,10 +969,18 @@ function importProtectionPlugin(opts) {
702
969
  message: `Import "${source}" is denied in the ${envType} environment`
703
970
  }
704
971
  );
972
+ if (shouldDefer && !info.resolved) {
973
+ try {
974
+ const resolvedForInfo = await resolveAgainstImporter();
975
+ if (resolvedForInfo) info.resolved = resolvedForInfo;
976
+ } catch {
977
+ }
978
+ }
705
979
  return reportOrDeferViolation(
706
980
  this,
707
981
  env,
708
982
  normalizedImporter,
983
+ importer,
709
984
  info,
710
985
  shouldDefer,
711
986
  isPreTransformResolve
@@ -716,28 +991,29 @@ function importProtectionPlugin(opts) {
716
991
  if (env.resolveCache.has(cacheKey)) {
717
992
  resolved = env.resolveCache.get(cacheKey) ?? null;
718
993
  } else {
719
- const result = await this.resolve(source, importer, {
720
- skipSelf: true
721
- });
722
- resolved = result ? normalizeFilePath(result.id) : null;
723
- env.resolveCache.set(cacheKey, resolved);
724
- getOrCreate(
725
- env.resolveCacheByFile,
726
- normalizedImporter,
727
- () => /* @__PURE__ */ new Set()
728
- ).add(cacheKey);
994
+ resolved = await resolveAgainstImporter();
995
+ if (resolved !== null) {
996
+ env.resolveCache.set(cacheKey, resolved);
997
+ getOrCreate(
998
+ env.resolveCacheByFile,
999
+ normalizedImporter,
1000
+ () => /* @__PURE__ */ new Set()
1001
+ ).add(cacheKey);
1002
+ }
729
1003
  }
730
1004
  if (resolved) {
731
1005
  const relativePath = getRelativePath(resolved);
732
1006
  if (isPreTransformResolve && !isScanResolve) {
733
1007
  env.serverFnLookupModules.add(resolved);
734
1008
  }
735
- env.graph.addEdge(resolved, normalizedImporter, source);
1009
+ if (!isPreTransformResolve) {
1010
+ env.graph.addEdge(resolved, normalizedImporter, source);
1011
+ }
736
1012
  const isExcludedFile = matchers.excludeFiles.length > 0 && matchesAny(relativePath, matchers.excludeFiles);
737
1013
  if (!isExcludedFile) {
738
1014
  const fileMatch = matchers.files.length > 0 ? matchesAny(relativePath, matchers.files) : void 0;
739
1015
  if (fileMatch) {
740
- const info = await buildViolationInfo(
1016
+ const info = await buildFileViolationInfo(
741
1017
  provider,
742
1018
  env,
743
1019
  envName,
@@ -745,17 +1021,14 @@ function importProtectionPlugin(opts) {
745
1021
  importer,
746
1022
  normalizedImporter,
747
1023
  source,
748
- {
749
- type: "file",
750
- pattern: fileMatch.pattern,
751
- resolved,
752
- message: `Import "${source}" (resolved to "${relativePath}") is denied in the ${envType} environment`
753
- }
1024
+ resolved,
1025
+ fileMatch.pattern
754
1026
  );
755
1027
  return reportOrDeferViolation(
756
1028
  this,
757
1029
  env,
758
1030
  normalizedImporter,
1031
+ importer,
759
1032
  info,
760
1033
  shouldDefer,
761
1034
  isPreTransformResolve
@@ -776,6 +1049,7 @@ function importProtectionPlugin(opts) {
776
1049
  this,
777
1050
  env,
778
1051
  normalizedImporter,
1052
+ importer,
779
1053
  markerInfo,
780
1054
  shouldDefer,
781
1055
  isPreTransformResolve
@@ -807,22 +1081,70 @@ function importProtectionPlugin(opts) {
807
1081
  const envName = this.environment.name;
808
1082
  const env = envStates.get(envName);
809
1083
  if (!env || env.deferredBuildViolations.length === 0) return;
1084
+ const candidateCache = /* @__PURE__ */ new Map();
1085
+ const toModuleIdCandidates = (id) => {
1086
+ let cached = candidateCache.get(id);
1087
+ if (cached) return cached;
1088
+ const out = /* @__PURE__ */ new Set();
1089
+ const normalized = normalizeFilePath(id);
1090
+ out.add(id);
1091
+ out.add(normalized);
1092
+ out.add(relativizePath(normalized, config.root));
1093
+ if (normalized.startsWith(VITE_BROWSER_VIRTUAL_PREFIX)) {
1094
+ const internal = `\0${normalized.slice(VITE_BROWSER_VIRTUAL_PREFIX.length)}`;
1095
+ out.add(internal);
1096
+ out.add(relativizePath(normalizeFilePath(internal), config.root));
1097
+ }
1098
+ if (normalized.startsWith("\0")) {
1099
+ const browser = `${VITE_BROWSER_VIRTUAL_PREFIX}${normalized.slice(1)}`;
1100
+ out.add(browser);
1101
+ out.add(relativizePath(normalizeFilePath(browser), config.root));
1102
+ }
1103
+ cached = Array.from(out);
1104
+ candidateCache.set(id, cached);
1105
+ return cached;
1106
+ };
810
1107
  const survivingModules = /* @__PURE__ */ new Set();
811
1108
  for (const chunk of Object.values(bundle)) {
812
1109
  if (chunk.type === "chunk") {
813
1110
  for (const moduleId of Object.keys(chunk.modules)) {
814
- survivingModules.add(moduleId);
1111
+ for (const candidate of toModuleIdCandidates(moduleId)) {
1112
+ survivingModules.add(candidate);
1113
+ }
815
1114
  }
816
1115
  }
817
1116
  }
1117
+ const didModuleSurvive = (moduleId) => toModuleIdCandidates(moduleId).some(
1118
+ (candidate) => survivingModules.has(candidate)
1119
+ );
818
1120
  const realViolations = [];
819
1121
  for (const {
820
1122
  info,
821
1123
  mockModuleId,
822
1124
  checkModuleId
823
1125
  } of env.deferredBuildViolations) {
824
- const checkId = checkModuleId ?? mockModuleId;
825
- if (!survivingModules.has(checkId)) continue;
1126
+ let survived;
1127
+ if (checkModuleId != null) {
1128
+ const importerVariantIds = /* @__PURE__ */ new Set([info.importer]);
1129
+ const importerKeys = env.transformResultKeysByFile.get(
1130
+ normalizeFilePath(info.importer)
1131
+ );
1132
+ if (importerKeys) {
1133
+ for (const key of importerKeys) {
1134
+ importerVariantIds.add(key);
1135
+ }
1136
+ }
1137
+ survived = false;
1138
+ for (const importerId of importerVariantIds) {
1139
+ if (didModuleSurvive(importerId)) {
1140
+ survived = true;
1141
+ break;
1142
+ }
1143
+ }
1144
+ } else {
1145
+ survived = didModuleSurvive(mockModuleId);
1146
+ }
1147
+ if (!survived) continue;
826
1148
  if (config.onViolation) {
827
1149
  const result = await config.onViolation(info);
828
1150
  if (result === false) continue;
@@ -835,12 +1157,7 @@ function importProtectionPlugin(opts) {
835
1157
  } else {
836
1158
  const seen = /* @__PURE__ */ new Set();
837
1159
  for (const info of realViolations) {
838
- const key = dedupeKey(
839
- info.type,
840
- info.importer,
841
- info.specifier,
842
- info.resolved
843
- );
1160
+ const key = dedupeKey(info);
844
1161
  if (!seen.has(key)) {
845
1162
  seen.add(key);
846
1163
  this.warn(formatViolation(info, config.root));
@@ -867,6 +1184,9 @@ function importProtectionPlugin(opts) {
867
1184
  async handler(code, id) {
868
1185
  const envName = this.environment.name;
869
1186
  const file = normalizeFilePath(id);
1187
+ const envType = getEnvType(envName);
1188
+ const matchers = getRulesForEnvironment(envName);
1189
+ const isBuild = config.command === "build";
870
1190
  if (IMPORT_PROTECTION_DEBUG) {
871
1191
  if (matchesDebugFilter(file)) {
872
1192
  debugLog("transform-cache", {
@@ -879,6 +1199,31 @@ function importProtectionPlugin(opts) {
879
1199
  if (!shouldCheckImporter(file)) {
880
1200
  return void 0;
881
1201
  }
1202
+ const selfFileMatch = checkFileDenial(getRelativePath(file), matchers);
1203
+ if (selfFileMatch) {
1204
+ let exportNames = [];
1205
+ try {
1206
+ exportNames = collectNamedExports(code);
1207
+ } catch {
1208
+ }
1209
+ if (isBuild) {
1210
+ return generateSelfContainedMockModule(exportNames);
1211
+ }
1212
+ const runtimeId = mockRuntimeModuleIdFromViolation(
1213
+ {
1214
+ env: envType,
1215
+ behavior: config.effectiveBehavior === "error" ? "error" : "mock",
1216
+ importer: file,
1217
+ specifier: relativizePath(file, config.root),
1218
+ pattern: selfFileMatch.pattern,
1219
+ message: `File "${relativizePath(file, config.root)}" is denied in the ${envType} environment`,
1220
+ trace: []
1221
+ },
1222
+ config.mockAccess,
1223
+ config.root
1224
+ );
1225
+ return generateDevSelfDenialModule(exportNames, runtimeId);
1226
+ }
882
1227
  let map;
883
1228
  try {
884
1229
  map = this.getCombinedSourcemap();
@@ -896,77 +1241,119 @@ function importProtectionPlugin(opts) {
896
1241
  const lineIndex = buildLineIndex(code);
897
1242
  const cacheKey = normalizePath(id);
898
1243
  const envState = getEnv(envName);
899
- if (id.includes(SERVER_FN_LOOKUP_QUERY)) {
1244
+ const isServerFnLookup = id.includes(SERVER_FN_LOOKUP_QUERY);
1245
+ if (isServerFnLookup) {
900
1246
  envState.serverFnLookupModules.add(file);
901
1247
  }
902
- envState.transformResultCache.set(cacheKey, {
1248
+ const result = {
903
1249
  code,
904
1250
  map,
905
1251
  originalCode,
906
1252
  lineIndex
907
- });
908
- const keySet = getOrCreate(
909
- envState.transformResultKeysByFile,
910
- file,
911
- () => /* @__PURE__ */ new Set()
912
- );
913
- keySet.add(cacheKey);
914
- if (cacheKey !== file) {
915
- envState.transformResultCache.set(file, {
916
- code,
917
- map,
918
- originalCode,
919
- lineIndex
920
- });
921
- keySet.add(file);
922
- }
1253
+ };
1254
+ cacheTransformResult(envState, file, cacheKey, result);
1255
+ if (isBuild) return void 0;
1256
+ const isDevMock = config.effectiveBehavior === "mock";
923
1257
  const importSources = extractImportSources(code);
924
1258
  const resolvedChildren = /* @__PURE__ */ new Set();
1259
+ const deniedSourceReplacements = /* @__PURE__ */ new Map();
925
1260
  for (const src of importSources) {
926
1261
  try {
927
1262
  const resolved = await this.resolve(src, id, { skipSelf: true });
928
1263
  if (resolved && !resolved.external) {
929
- const resolvedPath = normalizeFilePath(resolved.id);
1264
+ const resolvedPath = canonicalizeResolvedId(
1265
+ resolved.id,
1266
+ config.root,
1267
+ resolveExtensionlessAbsoluteId
1268
+ );
930
1269
  resolvedChildren.add(resolvedPath);
1270
+ if (resolved.id.includes("tanstack-start-import-protection:")) {
1271
+ let physicalPath;
1272
+ const pending = envState.pendingViolations.get(file);
1273
+ if (pending) {
1274
+ const match = pending.find(
1275
+ (pv) => pv.info.specifier === src && pv.info.resolved
1276
+ );
1277
+ if (match) physicalPath = match.info.resolved;
1278
+ }
1279
+ if (physicalPath && physicalPath !== resolvedPath) {
1280
+ resolvedChildren.add(physicalPath);
1281
+ envState.graph.addEdge(physicalPath, file, src);
1282
+ }
1283
+ }
931
1284
  envState.graph.addEdge(resolvedPath, file, src);
1285
+ if (isDevMock) {
1286
+ const relativePath = getRelativePath(resolvedPath);
1287
+ const fileMatch = checkFileDenial(relativePath, matchers);
1288
+ if (fileMatch) {
1289
+ const info = await buildFileViolationInfo(
1290
+ envState.transformResultProvider,
1291
+ envState,
1292
+ envName,
1293
+ envType,
1294
+ id,
1295
+ file,
1296
+ src,
1297
+ resolvedPath,
1298
+ fileMatch.pattern
1299
+ );
1300
+ const replacement = await reportOrDeferViolation(
1301
+ this,
1302
+ envState,
1303
+ file,
1304
+ id,
1305
+ info,
1306
+ isDevMock,
1307
+ isServerFnLookup
1308
+ );
1309
+ if (replacement) {
1310
+ deniedSourceReplacements.set(
1311
+ src,
1312
+ replacement.startsWith("\0") ? VITE_BROWSER_VIRTUAL_PREFIX + replacement.slice(1) : replacement
1313
+ );
1314
+ }
1315
+ }
1316
+ }
932
1317
  }
933
1318
  } catch {
934
1319
  }
935
1320
  }
936
1321
  envState.postTransformImports.set(cacheKey, resolvedChildren);
937
- if (cacheKey !== file) {
1322
+ if (cacheKey !== file && !isServerFnLookup) {
938
1323
  envState.postTransformImports.set(file, resolvedChildren);
939
1324
  }
940
1325
  await processPendingViolations(envState, this.warn.bind(this));
941
- return void 0;
942
- }
943
- }
944
- },
945
- {
946
- // Separate plugin so the transform can be enabled/disabled per-environment.
947
- name: "tanstack-start-core:import-protection-mock-rewrite",
948
- enforce: "pre",
949
- applyToEnvironment(env) {
950
- if (!config.enabled) return false;
951
- return environmentNames.has(env.name);
952
- },
953
- transform: {
954
- filter: {
955
- id: {
956
- include: [/\.[cm]?[tj]sx?($|\?)/]
957
- }
958
- },
959
- handler(code, id) {
960
- const envName = this.environment.name;
961
- const envState = envStates.get(envName);
962
- if (!envState) return void 0;
963
- try {
964
- const importerFile = normalizeFilePath(id);
965
- envState.mockExportsByImporter.set(
966
- importerFile,
967
- collectMockExportNamesBySource(code)
968
- );
969
- } catch {
1326
+ if (deniedSourceReplacements.size > 0) {
1327
+ try {
1328
+ const rewritten = rewriteDeniedImports(
1329
+ code,
1330
+ id,
1331
+ new Set(deniedSourceReplacements.keys()),
1332
+ (source) => deniedSourceReplacements.get(source) ?? source
1333
+ );
1334
+ if (!rewritten) {
1335
+ return void 0;
1336
+ }
1337
+ const normalizedMap = rewritten.map ? {
1338
+ ...rewritten.map,
1339
+ version: Number(rewritten.map.version),
1340
+ sourcesContent: rewritten.map.sourcesContent?.map(
1341
+ (s) => s ?? ""
1342
+ ) ?? []
1343
+ } : {
1344
+ version: 3,
1345
+ file: id,
1346
+ names: [],
1347
+ sources: [id],
1348
+ sourcesContent: [code],
1349
+ mappings: ""
1350
+ };
1351
+ return {
1352
+ code: rewritten.code,
1353
+ map: normalizedMap
1354
+ };
1355
+ } catch {
1356
+ }
970
1357
  }
971
1358
  return void 0;
972
1359
  }
@@ -975,8 +1362,6 @@ function importProtectionPlugin(opts) {
975
1362
  ];
976
1363
  }
977
1364
  export {
978
- dedupePatterns,
979
- extractImportSources,
980
1365
  importProtectionPlugin
981
1366
  };
982
1367
  //# sourceMappingURL=plugin.js.map