@tanstack/start-plugin-core 1.161.4 → 1.162.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.
Files changed (32) hide show
  1. package/dist/esm/import-protection-plugin/defaults.d.ts +6 -4
  2. package/dist/esm/import-protection-plugin/defaults.js +3 -12
  3. package/dist/esm/import-protection-plugin/defaults.js.map +1 -1
  4. package/dist/esm/import-protection-plugin/plugin.d.ts +1 -1
  5. package/dist/esm/import-protection-plugin/plugin.js +488 -257
  6. package/dist/esm/import-protection-plugin/plugin.js.map +1 -1
  7. package/dist/esm/import-protection-plugin/postCompileUsage.d.ts +4 -2
  8. package/dist/esm/import-protection-plugin/postCompileUsage.js +31 -150
  9. package/dist/esm/import-protection-plugin/postCompileUsage.js.map +1 -1
  10. package/dist/esm/import-protection-plugin/rewriteDeniedImports.js +13 -9
  11. package/dist/esm/import-protection-plugin/rewriteDeniedImports.js.map +1 -1
  12. package/dist/esm/import-protection-plugin/sourceLocation.d.ts +32 -66
  13. package/dist/esm/import-protection-plugin/sourceLocation.js +129 -56
  14. package/dist/esm/import-protection-plugin/sourceLocation.js.map +1 -1
  15. package/dist/esm/import-protection-plugin/trace.d.ts +10 -0
  16. package/dist/esm/import-protection-plugin/trace.js +30 -44
  17. package/dist/esm/import-protection-plugin/trace.js.map +1 -1
  18. package/dist/esm/import-protection-plugin/utils.d.ts +8 -4
  19. package/dist/esm/import-protection-plugin/utils.js +43 -1
  20. package/dist/esm/import-protection-plugin/utils.js.map +1 -1
  21. package/dist/esm/import-protection-plugin/virtualModules.d.ts +7 -1
  22. package/dist/esm/import-protection-plugin/virtualModules.js +104 -135
  23. package/dist/esm/import-protection-plugin/virtualModules.js.map +1 -1
  24. package/package.json +6 -6
  25. package/src/import-protection-plugin/defaults.ts +8 -19
  26. package/src/import-protection-plugin/plugin.ts +776 -433
  27. package/src/import-protection-plugin/postCompileUsage.ts +57 -229
  28. package/src/import-protection-plugin/rewriteDeniedImports.ts +34 -42
  29. package/src/import-protection-plugin/sourceLocation.ts +184 -185
  30. package/src/import-protection-plugin/trace.ts +38 -49
  31. package/src/import-protection-plugin/utils.ts +62 -1
  32. package/src/import-protection-plugin/virtualModules.ts +163 -177
@@ -1,23 +1,102 @@
1
- import * as path from "pathe";
2
1
  import { normalizePath } from "vite";
3
2
  import { resolveViteId } from "../utils.js";
4
3
  import { VITE_ENVIRONMENT_NAMES } from "../constants.js";
5
4
  import { SERVER_FN_LOOKUP } from "../start-compiler-plugin/plugin.js";
6
- import { formatViolation, ImportGraph, buildTrace } from "./trace.js";
5
+ import { ImportGraph, formatViolation, buildTrace } from "./trace.js";
7
6
  import { getDefaultImportProtectionRules, getMarkerSpecifiers } from "./defaults.js";
8
7
  import { findPostCompileUsagePos } from "./postCompileUsage.js";
9
8
  import { matchesAny, compileMatchers } from "./matchers.js";
10
- import { normalizeFilePath, dedupePatterns } from "./utils.js";
9
+ import { escapeRegExp, normalizeFilePath, getOrCreate, clearNormalizeFilePathCache, dedupePatterns, extractImportSources, relativizePath } from "./utils.js";
11
10
  import { collectMockExportNamesBySource } from "./rewriteDeniedImports.js";
12
11
  import { RESOLVED_MOCK_MODULE_ID, loadSilentMockModule, RESOLVED_MOCK_EDGE_PREFIX, loadMockEdgeModule, RESOLVED_MOCK_RUNTIME_PREFIX, loadMockRuntimeModule, RESOLVED_MARKER_PREFIX, loadMarkerModule, MOCK_MODULE_ID, MOCK_EDGE_PREFIX, MOCK_RUNTIME_PREFIX, MARKER_PREFIX, mockRuntimeModuleIdFromViolation, makeMockEdgeModuleId } from "./virtualModules.js";
13
- import { pickOriginalCodeFromSourcesContent, buildLineIndex, addTraceImportLocations, findPostCompileUsageLocation, findImportStatementLocationFromTransformed, buildCodeSnippet } from "./sourceLocation.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 RESOLVED_MARKER_SERVER_ONLY = resolveViteId(`${MARKER_PREFIX}server-only`);
15
+ const RESOLVED_MARKER_CLIENT_ONLY = resolveViteId(`${MARKER_PREFIX}client-only`);
16
+ const IMPORT_PROTECTION_DEBUG = process.env.TSR_IMPORT_PROTECTION_DEBUG === "1" || process.env.TSR_IMPORT_PROTECTION_DEBUG === "true";
17
+ const IMPORT_PROTECTION_DEBUG_FILTER = process.env.TSR_IMPORT_PROTECTION_DEBUG_FILTER;
18
+ function debugLog(...args) {
19
+ if (!IMPORT_PROTECTION_DEBUG) return;
20
+ console.warn("[import-protection:debug]", ...args);
21
+ }
22
+ function matchesDebugFilter(...values) {
23
+ if (!IMPORT_PROTECTION_DEBUG_FILTER) return true;
24
+ return values.some((v) => v.includes(IMPORT_PROTECTION_DEBUG_FILTER));
25
+ }
14
26
  function importProtectionPlugin(opts) {
27
+ let devServer = null;
28
+ function buildTraceFromModuleGraph(envName, env, targetFile) {
29
+ if (!devServer) return null;
30
+ const environment = devServer.environments[envName];
31
+ if (!environment) return null;
32
+ const file = normalizeFilePath(targetFile);
33
+ const start = environment.moduleGraph.getModuleById(file);
34
+ if (!start) return null;
35
+ const nodeIds = /* @__PURE__ */ new Map();
36
+ function nodeId(n) {
37
+ let cached = nodeIds.get(n);
38
+ if (cached === void 0) {
39
+ cached = n.id ? normalizeFilePath(n.id) : n.url ? normalizeFilePath(n.url) : "";
40
+ nodeIds.set(n, cached);
41
+ }
42
+ return cached;
43
+ }
44
+ const queue = [start];
45
+ const visited = /* @__PURE__ */ new Set([start]);
46
+ const parent = /* @__PURE__ */ new Map();
47
+ let entryRoot = null;
48
+ let fallbackRoot = null;
49
+ let qi = 0;
50
+ while (qi < queue.length) {
51
+ const node = queue[qi++];
52
+ const id = nodeId(node);
53
+ if (id && env.graph.entries.has(id)) {
54
+ entryRoot = node;
55
+ break;
56
+ }
57
+ const importers = node.importers;
58
+ if (importers.size === 0) {
59
+ if (!fallbackRoot) fallbackRoot = node;
60
+ continue;
61
+ }
62
+ for (const imp of importers) {
63
+ if (visited.has(imp)) continue;
64
+ visited.add(imp);
65
+ parent.set(imp, node);
66
+ queue.push(imp);
67
+ }
68
+ }
69
+ const root = entryRoot ?? fallbackRoot;
70
+ if (!root) return null;
71
+ const chain = [];
72
+ let cur = root;
73
+ for (let i = 0; i < config.maxTraceDepth + 2 && cur; i++) {
74
+ chain.push(cur);
75
+ if (cur === start) break;
76
+ cur = parent.get(cur);
77
+ }
78
+ const steps = [];
79
+ for (let i = 0; i < chain.length; i++) {
80
+ const id = nodeId(chain[i]);
81
+ if (!id) continue;
82
+ let specifier;
83
+ if (i + 1 < chain.length) {
84
+ const nextId = nodeId(chain[i + 1]);
85
+ if (nextId) {
86
+ specifier = env.graph.reverseEdges.get(nextId)?.get(id);
87
+ }
88
+ }
89
+ steps.push(specifier ? { file: id, specifier } : { file: id });
90
+ }
91
+ return steps.length ? steps : null;
92
+ }
15
93
  const config = {
16
94
  enabled: true,
17
95
  root: "",
18
96
  command: "build",
19
97
  srcDirectory: "",
20
98
  framework: opts.framework,
99
+ entryFiles: [],
21
100
  effectiveBehavior: "error",
22
101
  mockAccess: "error",
23
102
  logMode: "once",
@@ -35,51 +114,34 @@ function importProtectionPlugin(opts) {
35
114
  };
36
115
  const envStates = /* @__PURE__ */ new Map();
37
116
  const shared = { fileMarkerKind: /* @__PURE__ */ new Map() };
38
- function createImportLocCache(env) {
39
- const cache = /* @__PURE__ */ new Map();
40
- const originalSet = cache.set.bind(cache);
41
- cache.set = function(key, value) {
42
- originalSet(key, value);
43
- const sepIdx = key.indexOf("::");
44
- if (sepIdx !== -1) {
45
- const file = key.slice(0, sepIdx);
46
- let fileKeys = env.importLocByFile.get(file);
47
- if (!fileKeys) {
48
- fileKeys = /* @__PURE__ */ new Set();
49
- env.importLocByFile.set(file, fileKeys);
50
- }
51
- fileKeys.add(key);
52
- }
53
- return this;
54
- };
55
- return cache;
56
- }
57
- function getMockEdgeExports(env, importerId, source) {
58
- const importerFile = normalizeFilePath(importerId);
59
- return env.mockExportsByImporter.get(importerFile)?.get(source) ?? [];
60
- }
61
117
  function getMarkerKindForFile(fileId) {
62
118
  const file = normalizeFilePath(fileId);
63
119
  return shared.fileMarkerKind.get(file);
64
120
  }
65
- function getTransformResultProvider(env) {
66
- return {
67
- getTransformResult(id) {
68
- const fullKey = normalizePath(id);
69
- const exact = env.transformResultCache.get(fullKey);
70
- if (exact) return exact;
71
- const strippedKey = normalizeFilePath(id);
72
- return strippedKey !== fullKey ? env.transformResultCache.get(strippedKey) : void 0;
121
+ async function rebuildAndAnnotateTrace(provider, env, envName, normalizedImporter, specifier, importerLoc, traceOverride) {
122
+ let trace = traceOverride ?? buildTrace(env.graph, normalizedImporter, config.maxTraceDepth);
123
+ if (config.command === "serve") {
124
+ const mgTrace = buildTraceFromModuleGraph(
125
+ envName,
126
+ env,
127
+ normalizedImporter
128
+ );
129
+ if (mgTrace && mgTrace.length > trace.length) {
130
+ trace = mgTrace;
73
131
  }
74
- };
75
- }
76
- async function buildViolationInfo(provider, env, envName, envType, importer, normalizedImporter, source, overrides) {
77
- const trace = buildTrace(
78
- env.graph,
79
- normalizedImporter,
80
- config.maxTraceDepth
81
- );
132
+ }
82
133
  await addTraceImportLocations(provider, trace, env.importLocCache);
134
+ if (trace.length > 0) {
135
+ const last = trace[trace.length - 1];
136
+ if (!last.specifier) last.specifier = specifier;
137
+ if (importerLoc && last.line == null) {
138
+ last.line = importerLoc.line;
139
+ last.column = importerLoc.column;
140
+ }
141
+ }
142
+ return trace;
143
+ }
144
+ async function buildViolationInfo(provider, env, envName, envType, importer, normalizedImporter, source, overrides, traceOverride) {
83
145
  const loc = await findPostCompileUsageLocation(
84
146
  provider,
85
147
  importer,
@@ -91,14 +153,15 @@ function importProtectionPlugin(opts) {
91
153
  source,
92
154
  env.importLocCache
93
155
  );
94
- if (trace.length > 0) {
95
- const last = trace[trace.length - 1];
96
- if (!last.specifier) last.specifier = source;
97
- if (loc && last.line == null) {
98
- last.line = loc.line;
99
- last.column = loc.column;
100
- }
101
- }
156
+ const trace = await rebuildAndAnnotateTrace(
157
+ provider,
158
+ env,
159
+ envName,
160
+ normalizedImporter,
161
+ source,
162
+ loc,
163
+ traceOverride
164
+ );
102
165
  const snippet = loc ? buildCodeSnippet(provider, importer, loc) : void 0;
103
166
  return {
104
167
  env: envName,
@@ -112,12 +175,12 @@ function importProtectionPlugin(opts) {
112
175
  ...overrides
113
176
  };
114
177
  }
115
- async function maybeReportMarkerViolationFromResolvedImport(ctx, provider, env, envName, envType, importer, source, resolvedId, relativePath, opts2) {
178
+ async function buildMarkerViolationFromResolvedImport(provider, env, envName, envType, importer, source, resolvedId, relativePath, traceOverride) {
116
179
  const markerKind = getMarkerKindForFile(resolvedId);
117
180
  const violates = envType === "client" && markerKind === "server" || envType === "server" && markerKind === "client";
118
181
  if (!violates) return void 0;
119
182
  const normalizedImporter = normalizeFilePath(importer);
120
- const info = await buildViolationInfo(
183
+ return buildViolationInfo(
121
184
  provider,
122
185
  env,
123
186
  envName,
@@ -129,13 +192,9 @@ function importProtectionPlugin(opts) {
129
192
  type: "marker",
130
193
  resolved: normalizeFilePath(resolvedId),
131
194
  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`
132
- }
195
+ },
196
+ traceOverride
133
197
  );
134
- return handleViolation.call(ctx, env, info, opts2);
135
- }
136
- function buildMockEdgeModuleId(env, importerId, source, runtimeId) {
137
- const exports = getMockEdgeExports(env, importerId, source);
138
- return makeMockEdgeModuleId(exports, source, runtimeId);
139
198
  }
140
199
  function getEnvType(envName) {
141
200
  return config.envTypeMap.get(envName) ?? "server";
@@ -154,7 +213,7 @@ function importProtectionPlugin(opts) {
154
213
  function getEnv(envName) {
155
214
  let envState = envStates.get(envName);
156
215
  if (!envState) {
157
- const importLocByFile = /* @__PURE__ */ new Map();
216
+ const transformResultCache = /* @__PURE__ */ new Map();
158
217
  envState = {
159
218
  graph: new ImportGraph(),
160
219
  deniedSources: /* @__PURE__ */ new Set(),
@@ -162,33 +221,46 @@ function importProtectionPlugin(opts) {
162
221
  mockExportsByImporter: /* @__PURE__ */ new Map(),
163
222
  resolveCache: /* @__PURE__ */ new Map(),
164
223
  resolveCacheByFile: /* @__PURE__ */ new Map(),
165
- importLocCache: /* @__PURE__ */ new Map(),
166
- // placeholder, replaced below
167
- importLocByFile,
224
+ importLocCache: new ImportLocCache(),
168
225
  seenViolations: /* @__PURE__ */ new Set(),
169
- transformResultCache: /* @__PURE__ */ new Map(),
170
- transformResultKeysByFile: /* @__PURE__ */ new Map()
226
+ transformResultCache,
227
+ transformResultKeysByFile: /* @__PURE__ */ new Map(),
228
+ transformResultProvider: {
229
+ getTransformResult(id) {
230
+ const fullKey = normalizePath(id);
231
+ const exact = transformResultCache.get(fullKey);
232
+ if (exact) return exact;
233
+ const strippedKey = normalizeFilePath(id);
234
+ return strippedKey !== fullKey ? transformResultCache.get(strippedKey) : void 0;
235
+ }
236
+ },
237
+ postTransformImports: /* @__PURE__ */ new Map(),
238
+ hasSeenEntry: false,
239
+ serverFnLookupModules: /* @__PURE__ */ new Set(),
240
+ pendingViolations: /* @__PURE__ */ new Map()
171
241
  };
172
- envState.importLocCache = createImportLocCache(envState);
173
242
  envStates.set(envName, envState);
174
243
  }
175
244
  return envState;
176
245
  }
246
+ const shouldCheckImporterCache = /* @__PURE__ */ new Map();
177
247
  function shouldCheckImporter(importer) {
178
- const relativePath = path.relative(config.root, importer);
248
+ let result = shouldCheckImporterCache.get(importer);
249
+ if (result !== void 0) return result;
250
+ const relativePath = relativizePath(importer, config.root);
179
251
  if (config.excludeMatchers.length > 0 && matchesAny(relativePath, config.excludeMatchers)) {
180
- return false;
181
- }
182
- if (config.ignoreImporterMatchers.length > 0 && matchesAny(relativePath, config.ignoreImporterMatchers)) {
183
- return false;
184
- }
185
- if (config.includeMatchers.length > 0) {
186
- return !!matchesAny(relativePath, config.includeMatchers);
187
- }
188
- if (config.srcDirectory) {
189
- return importer.startsWith(config.srcDirectory);
252
+ result = false;
253
+ } else if (config.ignoreImporterMatchers.length > 0 && matchesAny(relativePath, config.ignoreImporterMatchers)) {
254
+ result = false;
255
+ } else if (config.includeMatchers.length > 0) {
256
+ result = !!matchesAny(relativePath, config.includeMatchers);
257
+ } else if (config.srcDirectory) {
258
+ result = importer.startsWith(config.srcDirectory);
259
+ } else {
260
+ result = true;
190
261
  }
191
- return true;
262
+ shouldCheckImporterCache.set(importer, result);
263
+ return result;
192
264
  }
193
265
  function dedupeKey(type, importer, specifier, resolved) {
194
266
  return `${type}:${importer}:${specifier}:${resolved ?? ""}`;
@@ -200,7 +272,182 @@ function importProtectionPlugin(opts) {
200
272
  return false;
201
273
  }
202
274
  function getRelativePath(absolutePath) {
203
- return normalizePath(path.relative(config.root, absolutePath));
275
+ return relativizePath(normalizePath(absolutePath), config.root);
276
+ }
277
+ function registerEntries() {
278
+ const { resolvedStartConfig } = opts.getConfig();
279
+ for (const envDef of opts.environments) {
280
+ const envState = getEnv(envDef.name);
281
+ if (resolvedStartConfig.routerFilePath) {
282
+ envState.graph.addEntry(
283
+ normalizePath(resolvedStartConfig.routerFilePath)
284
+ );
285
+ }
286
+ if (resolvedStartConfig.startFilePath) {
287
+ envState.graph.addEntry(
288
+ normalizePath(resolvedStartConfig.startFilePath)
289
+ );
290
+ }
291
+ }
292
+ }
293
+ function checkPostTransformReachability(env, file) {
294
+ const visited = /* @__PURE__ */ new Set();
295
+ const queue = [file];
296
+ let hasUnknownEdge = false;
297
+ let qi = 0;
298
+ while (qi < queue.length) {
299
+ const current = queue[qi++];
300
+ if (visited.has(current)) continue;
301
+ visited.add(current);
302
+ if (env.graph.entries.has(current)) {
303
+ return "reachable";
304
+ }
305
+ const importers = env.graph.reverseEdges.get(current);
306
+ if (!importers) continue;
307
+ for (const [parent] of importers) {
308
+ if (visited.has(parent)) continue;
309
+ const keySet = env.transformResultKeysByFile.get(parent);
310
+ let anyVariantCached = false;
311
+ let edgeLive = false;
312
+ if (keySet) {
313
+ for (const k of keySet) {
314
+ const resolvedImports = env.postTransformImports.get(k);
315
+ if (resolvedImports) {
316
+ anyVariantCached = true;
317
+ if (resolvedImports.has(current)) {
318
+ edgeLive = true;
319
+ break;
320
+ }
321
+ }
322
+ }
323
+ }
324
+ if (!anyVariantCached) {
325
+ const resolvedImports = env.postTransformImports.get(parent);
326
+ if (resolvedImports) {
327
+ anyVariantCached = true;
328
+ if (resolvedImports.has(current)) {
329
+ edgeLive = true;
330
+ }
331
+ }
332
+ }
333
+ if (!anyVariantCached) {
334
+ const hasTransformResult = env.transformResultCache.has(parent) || (keySet ? keySet.size > 0 : false);
335
+ if (hasTransformResult) {
336
+ hasUnknownEdge = true;
337
+ continue;
338
+ }
339
+ queue.push(parent);
340
+ continue;
341
+ }
342
+ if (edgeLive) {
343
+ queue.push(parent);
344
+ }
345
+ }
346
+ }
347
+ return hasUnknownEdge ? "unknown" : "unreachable";
348
+ }
349
+ async function processPendingViolations(env, warnFn) {
350
+ if (env.pendingViolations.size === 0) return;
351
+ const toDelete = [];
352
+ for (const [file, violations] of env.pendingViolations) {
353
+ const status = env.hasSeenEntry ? checkPostTransformReachability(env, file) : "reachable";
354
+ if (status === "reachable") {
355
+ for (const pv of violations) {
356
+ const key = dedupeKey(
357
+ pv.info.type,
358
+ pv.info.importer,
359
+ pv.info.specifier,
360
+ pv.info.resolved
361
+ );
362
+ if (!hasSeen(env, key)) {
363
+ const freshTrace = await rebuildAndAnnotateTrace(
364
+ env.transformResultProvider,
365
+ env,
366
+ pv.info.env,
367
+ pv.info.importer,
368
+ pv.info.specifier,
369
+ pv.info.importerLoc
370
+ );
371
+ if (freshTrace.length > pv.info.trace.length) {
372
+ pv.info.trace = freshTrace;
373
+ }
374
+ if (config.onViolation) {
375
+ const result = config.onViolation(pv.info);
376
+ if (result === false) continue;
377
+ }
378
+ warnFn(formatViolation(pv.info, config.root));
379
+ }
380
+ }
381
+ toDelete.push(file);
382
+ } else if (status === "unreachable") {
383
+ toDelete.push(file);
384
+ }
385
+ }
386
+ for (const file of toDelete) {
387
+ env.pendingViolations.delete(file);
388
+ }
389
+ }
390
+ function deferViolation(env, importerFile, info, mockReturnValue) {
391
+ getOrCreate(env.pendingViolations, importerFile, () => []).push({
392
+ info,
393
+ mockReturnValue: typeof mockReturnValue === "string" ? mockReturnValue : mockReturnValue?.id ?? ""
394
+ });
395
+ }
396
+ function handleViolation(ctx, env, info, violationOpts) {
397
+ const key = dedupeKey(
398
+ info.type,
399
+ info.importer,
400
+ info.specifier,
401
+ info.resolved
402
+ );
403
+ if (!violationOpts?.silent) {
404
+ if (config.onViolation) {
405
+ const result = config.onViolation(info);
406
+ if (result === false) {
407
+ return void 0;
408
+ }
409
+ }
410
+ const seen = hasSeen(env, key);
411
+ if (config.effectiveBehavior === "error") {
412
+ if (!seen) ctx.error(formatViolation(info, config.root));
413
+ return void 0;
414
+ }
415
+ if (!seen) {
416
+ ctx.warn(formatViolation(info, config.root));
417
+ }
418
+ } else {
419
+ if (config.effectiveBehavior === "error") {
420
+ return void 0;
421
+ }
422
+ }
423
+ env.deniedSources.add(info.specifier);
424
+ getOrCreate(env.deniedEdges, info.importer, () => /* @__PURE__ */ new Set()).add(
425
+ info.specifier
426
+ );
427
+ if (config.command === "serve") {
428
+ const runtimeId = mockRuntimeModuleIdFromViolation(
429
+ info,
430
+ config.mockAccess,
431
+ config.root
432
+ );
433
+ const importerFile = normalizeFilePath(info.importer);
434
+ const exports = env.mockExportsByImporter.get(importerFile)?.get(info.specifier) ?? [];
435
+ return resolveViteId(
436
+ makeMockEdgeModuleId(exports, info.specifier, runtimeId)
437
+ );
438
+ }
439
+ return { id: RESOLVED_MOCK_MODULE_ID, syntheticNamedExports: true };
440
+ }
441
+ async function reportOrDeferViolation(ctx, env, importerFile, info, shouldDefer, isPreTransformResolve) {
442
+ if (shouldDefer) {
443
+ const result = handleViolation(ctx, env, info, { silent: true });
444
+ deferViolation(env, importerFile, info, result);
445
+ await processPendingViolations(env, ctx.warn.bind(ctx));
446
+ return result;
447
+ }
448
+ return handleViolation(ctx, env, info, {
449
+ silent: isPreTransformResolve
450
+ });
204
451
  }
205
452
  return [
206
453
  {
@@ -215,6 +462,10 @@ function importProtectionPlugin(opts) {
215
462
  config.command = viteConfig.command;
216
463
  const { startConfig, resolvedStartConfig } = opts.getConfig();
217
464
  config.srcDirectory = resolvedStartConfig.srcDirectory;
465
+ config.entryFiles = [
466
+ resolvedStartConfig.routerFilePath,
467
+ resolvedStartConfig.startFilePath
468
+ ].filter((f) => Boolean(f));
218
469
  const userOpts = startConfig.importProtection;
219
470
  if (userOpts?.enabled === false) {
220
471
  config.enabled = false;
@@ -233,8 +484,11 @@ function importProtectionPlugin(opts) {
233
484
  config.logMode = userOpts?.log ?? "once";
234
485
  config.mockAccess = userOpts?.mockAccess ?? "error";
235
486
  config.maxTraceDepth = userOpts?.maxTraceDepth ?? 20;
236
- config.onViolation = userOpts?.onViolation;
237
- const defaults = getDefaultImportProtectionRules(opts.framework);
487
+ if (userOpts?.onViolation) {
488
+ const fn = userOpts.onViolation;
489
+ config.onViolation = (info) => fn(info);
490
+ }
491
+ const defaults = getDefaultImportProtectionRules();
238
492
  const clientSpecifiers = dedupePatterns([
239
493
  ...defaults.client.specifiers,
240
494
  ...userOpts?.client?.specifiers ?? []
@@ -261,55 +515,37 @@ function importProtectionPlugin(opts) {
261
515
  userOpts.ignoreImporters
262
516
  );
263
517
  }
264
- const markers = getMarkerSpecifiers(opts.framework);
518
+ const markers = getMarkerSpecifiers();
265
519
  config.markerSpecifiers = {
266
520
  serverOnly: new Set(markers.serverOnly),
267
521
  clientOnly: new Set(markers.clientOnly)
268
522
  };
269
- for (const envDef of opts.environments) {
270
- const envState = getEnv(envDef.name);
271
- if (resolvedStartConfig.routerFilePath) {
272
- envState.graph.addEntry(
273
- normalizePath(resolvedStartConfig.routerFilePath)
274
- );
275
- }
276
- if (resolvedStartConfig.startFilePath) {
277
- envState.graph.addEntry(
278
- normalizePath(resolvedStartConfig.startFilePath)
279
- );
280
- }
281
- }
523
+ },
524
+ configureServer(server) {
525
+ devServer = server;
282
526
  },
283
527
  buildStart() {
284
528
  if (!config.enabled) return;
529
+ clearNormalizeFilePathCache();
530
+ clearImportPatternCache();
531
+ shouldCheckImporterCache.clear();
285
532
  for (const envState of envStates.values()) {
286
533
  envState.resolveCache.clear();
287
534
  envState.resolveCacheByFile.clear();
288
535
  envState.importLocCache.clear();
289
- envState.importLocByFile.clear();
290
536
  envState.seenViolations.clear();
291
537
  envState.transformResultCache.clear();
292
538
  envState.transformResultKeysByFile.clear();
539
+ envState.postTransformImports.clear();
540
+ envState.hasSeenEntry = false;
541
+ envState.serverFnLookupModules.clear();
293
542
  envState.graph.clear();
294
543
  envState.deniedSources.clear();
295
544
  envState.deniedEdges.clear();
296
545
  envState.mockExportsByImporter.clear();
297
546
  }
298
547
  shared.fileMarkerKind.clear();
299
- for (const envDef of opts.environments) {
300
- const envState = getEnv(envDef.name);
301
- const { resolvedStartConfig } = opts.getConfig();
302
- if (resolvedStartConfig.routerFilePath) {
303
- envState.graph.addEntry(
304
- normalizePath(resolvedStartConfig.routerFilePath)
305
- );
306
- }
307
- if (resolvedStartConfig.startFilePath) {
308
- envState.graph.addEntry(
309
- normalizePath(resolvedStartConfig.startFilePath)
310
- );
311
- }
312
- }
548
+ registerEntries();
313
549
  },
314
550
  hotUpdate(ctx) {
315
551
  if (!config.enabled) return;
@@ -319,13 +555,7 @@ function importProtectionPlugin(opts) {
319
555
  const importerFile = normalizeFilePath(id);
320
556
  shared.fileMarkerKind.delete(importerFile);
321
557
  for (const envState of envStates.values()) {
322
- const locKeys = envState.importLocByFile.get(importerFile);
323
- if (locKeys) {
324
- for (const key of locKeys) {
325
- envState.importLocCache.delete(key);
326
- }
327
- envState.importLocByFile.delete(importerFile);
328
- }
558
+ envState.importLocCache.deleteByFile(importerFile);
329
559
  const resolveKeys = envState.resolveCacheByFile.get(importerFile);
330
560
  if (resolveKeys) {
331
561
  for (const key of resolveKeys) {
@@ -336,25 +566,46 @@ function importProtectionPlugin(opts) {
336
566
  envState.graph.invalidate(importerFile);
337
567
  envState.deniedEdges.delete(importerFile);
338
568
  envState.mockExportsByImporter.delete(importerFile);
569
+ envState.serverFnLookupModules.delete(importerFile);
570
+ envState.pendingViolations.delete(importerFile);
339
571
  const transformKeys = envState.transformResultKeysByFile.get(importerFile);
340
572
  if (transformKeys) {
341
573
  for (const key of transformKeys) {
342
574
  envState.transformResultCache.delete(key);
575
+ envState.postTransformImports.delete(key);
343
576
  }
344
577
  envState.transformResultKeysByFile.delete(importerFile);
345
578
  } else {
346
579
  envState.transformResultCache.delete(importerFile);
580
+ envState.postTransformImports.delete(importerFile);
347
581
  }
348
582
  }
349
583
  }
350
584
  }
351
585
  },
352
586
  async resolveId(source, importer, _options) {
353
- if (!config.enabled) return void 0;
354
587
  const envName = this.environment.name;
355
588
  const env = getEnv(envName);
356
589
  const envType = getEnvType(envName);
357
- const provider = getTransformResultProvider(env);
590
+ const provider = env.transformResultProvider;
591
+ const isScanResolve = !!_options.scan;
592
+ if (IMPORT_PROTECTION_DEBUG) {
593
+ const importerPath = importer ? normalizeFilePath(importer) : "(entry)";
594
+ const isEntryResolve = !importer;
595
+ const filtered = IMPORT_PROTECTION_DEBUG_FILTER === "entry" ? isEntryResolve : matchesDebugFilter(source, importerPath);
596
+ if (filtered) {
597
+ debugLog("resolveId", {
598
+ env: envName,
599
+ envType,
600
+ source,
601
+ importer: importerPath,
602
+ isEntryResolve,
603
+ hasSeenEntry: env.hasSeenEntry,
604
+ command: config.command,
605
+ behavior: config.effectiveBehavior
606
+ });
607
+ }
608
+ }
358
609
  if (source === MOCK_MODULE_ID) {
359
610
  return RESOLVED_MOCK_MODULE_ID;
360
611
  }
@@ -369,71 +620,55 @@ function importProtectionPlugin(opts) {
369
620
  }
370
621
  if (!importer) {
371
622
  env.graph.addEntry(source);
623
+ env.hasSeenEntry = true;
372
624
  return void 0;
373
625
  }
374
626
  if (source.startsWith("\0") || source.startsWith("virtual:")) {
375
627
  return void 0;
376
628
  }
377
- const isPreTransformResolve = importer.includes("?" + SERVER_FN_LOOKUP) || !!_options.scan;
378
- if (config.markerSpecifiers.serverOnly.has(source)) {
379
- const resolvedImporter = normalizeFilePath(importer);
380
- const existing = shared.fileMarkerKind.get(resolvedImporter);
381
- if (existing && existing !== "server") {
629
+ const normalizedImporter = normalizeFilePath(importer);
630
+ const isDirectLookup = importer.includes(SERVER_FN_LOOKUP_QUERY);
631
+ if (isDirectLookup) {
632
+ env.serverFnLookupModules.add(normalizedImporter);
633
+ }
634
+ const isPreTransformResolve = isDirectLookup || env.serverFnLookupModules.has(normalizedImporter) || isScanResolve;
635
+ const isDevMock = config.command === "serve" && config.effectiveBehavior === "mock";
636
+ const shouldDefer = isDevMock && !isPreTransformResolve;
637
+ const markerKind = config.markerSpecifiers.serverOnly.has(source) ? "server" : config.markerSpecifiers.clientOnly.has(source) ? "client" : void 0;
638
+ if (markerKind) {
639
+ const existing = shared.fileMarkerKind.get(normalizedImporter);
640
+ if (existing && existing !== markerKind) {
382
641
  this.error(
383
- `[import-protection] File "${getRelativePath(resolvedImporter)}" has both server-only and client-only markers. This is not allowed.`
642
+ `[import-protection] File "${getRelativePath(normalizedImporter)}" has both server-only and client-only markers. This is not allowed.`
384
643
  );
385
644
  }
386
- shared.fileMarkerKind.set(resolvedImporter, "server");
387
- if (envType === "client") {
645
+ shared.fileMarkerKind.set(normalizedImporter, markerKind);
646
+ const violatesEnv = envType === "client" && markerKind === "server" || envType === "server" && markerKind === "client";
647
+ if (violatesEnv) {
388
648
  const info = await buildViolationInfo(
389
649
  provider,
390
650
  env,
391
651
  envName,
392
652
  envType,
393
653
  importer,
394
- resolvedImporter,
654
+ normalizedImporter,
395
655
  source,
396
656
  {
397
657
  type: "marker",
398
- message: `Module "${getRelativePath(resolvedImporter)}" is marked server-only but is imported in the client environment`
658
+ 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`
399
659
  }
400
660
  );
401
- handleViolation.call(this, env, info, {
402
- silent: isPreTransformResolve
403
- });
404
- }
405
- return resolveViteId(`${MARKER_PREFIX}server-only`);
406
- }
407
- if (config.markerSpecifiers.clientOnly.has(source)) {
408
- const resolvedImporter = normalizeFilePath(importer);
409
- const existing = shared.fileMarkerKind.get(resolvedImporter);
410
- if (existing && existing !== "client") {
411
- this.error(
412
- `[import-protection] File "${getRelativePath(resolvedImporter)}" has both server-only and client-only markers. This is not allowed.`
413
- );
414
- }
415
- shared.fileMarkerKind.set(resolvedImporter, "client");
416
- if (envType === "server") {
417
- const info = await buildViolationInfo(
418
- provider,
661
+ await reportOrDeferViolation(
662
+ this,
419
663
  env,
420
- envName,
421
- envType,
422
- importer,
423
- resolvedImporter,
424
- source,
425
- {
426
- type: "marker",
427
- message: `Module "${getRelativePath(resolvedImporter)}" is marked client-only but is imported in the server environment`
428
- }
664
+ normalizedImporter,
665
+ info,
666
+ shouldDefer,
667
+ isPreTransformResolve
429
668
  );
430
- handleViolation.call(this, env, info, {
431
- silent: isPreTransformResolve
432
- });
433
669
  }
434
- return resolveViteId(`${MARKER_PREFIX}client-only`);
670
+ return markerKind === "server" ? RESOLVED_MARKER_SERVER_ONLY : RESOLVED_MARKER_CLIENT_ONLY;
435
671
  }
436
- const normalizedImporter = normalizeFilePath(importer);
437
672
  if (!shouldCheckImporter(normalizedImporter)) {
438
673
  return void 0;
439
674
  }
@@ -452,32 +687,39 @@ function importProtectionPlugin(opts) {
452
687
  {
453
688
  type: "specifier",
454
689
  pattern: specifierMatch.pattern,
455
- message: `Import "${source}" is denied in the "${envName}" environment`
690
+ message: `Import "${source}" is denied in the ${envType} environment`
456
691
  }
457
692
  );
458
- return handleViolation.call(this, env, info, {
459
- silent: isPreTransformResolve
460
- });
693
+ return reportOrDeferViolation(
694
+ this,
695
+ env,
696
+ normalizedImporter,
697
+ info,
698
+ shouldDefer,
699
+ isPreTransformResolve
700
+ );
461
701
  }
462
702
  const cacheKey = `${normalizedImporter}:${source}`;
463
703
  let resolved;
464
704
  if (env.resolveCache.has(cacheKey)) {
465
- resolved = env.resolveCache.get(cacheKey) || null;
705
+ resolved = env.resolveCache.get(cacheKey) ?? null;
466
706
  } else {
467
707
  const result = await this.resolve(source, importer, {
468
708
  skipSelf: true
469
709
  });
470
710
  resolved = result ? normalizeFilePath(result.id) : null;
471
711
  env.resolveCache.set(cacheKey, resolved);
472
- let fileKeys = env.resolveCacheByFile.get(normalizedImporter);
473
- if (!fileKeys) {
474
- fileKeys = /* @__PURE__ */ new Set();
475
- env.resolveCacheByFile.set(normalizedImporter, fileKeys);
476
- }
477
- fileKeys.add(cacheKey);
712
+ getOrCreate(
713
+ env.resolveCacheByFile,
714
+ normalizedImporter,
715
+ () => /* @__PURE__ */ new Set()
716
+ ).add(cacheKey);
478
717
  }
479
718
  if (resolved) {
480
719
  const relativePath = getRelativePath(resolved);
720
+ if (isPreTransformResolve && !isScanResolve) {
721
+ env.serverFnLookupModules.add(resolved);
722
+ }
481
723
  env.graph.addEdge(resolved, normalizedImporter, source);
482
724
  const fileMatch = matchers.files.length > 0 ? matchesAny(relativePath, matchers.files) : void 0;
483
725
  if (fileMatch) {
@@ -493,15 +735,19 @@ function importProtectionPlugin(opts) {
493
735
  type: "file",
494
736
  pattern: fileMatch.pattern,
495
737
  resolved,
496
- message: `Import "${source}" (resolved to "${relativePath}") is denied in the "${envName}" environment`
738
+ message: `Import "${source}" (resolved to "${relativePath}") is denied in the ${envType} environment`
497
739
  }
498
740
  );
499
- return handleViolation.call(this, env, info, {
500
- silent: isPreTransformResolve
501
- });
741
+ return reportOrDeferViolation(
742
+ this,
743
+ env,
744
+ normalizedImporter,
745
+ info,
746
+ shouldDefer,
747
+ isPreTransformResolve
748
+ );
502
749
  }
503
- const markerRes = await maybeReportMarkerViolationFromResolvedImport(
504
- this,
750
+ const markerInfo = await buildMarkerViolationFromResolvedImport(
505
751
  provider,
506
752
  env,
507
753
  envName,
@@ -509,11 +755,17 @@ function importProtectionPlugin(opts) {
509
755
  importer,
510
756
  source,
511
757
  resolved,
512
- relativePath,
513
- { silent: isPreTransformResolve }
758
+ relativePath
514
759
  );
515
- if (markerRes !== void 0) {
516
- return markerRes;
760
+ if (markerInfo) {
761
+ return reportOrDeferViolation(
762
+ this,
763
+ env,
764
+ normalizedImporter,
765
+ markerInfo,
766
+ shouldDefer,
767
+ isPreTransformResolve
768
+ );
517
769
  }
518
770
  }
519
771
  return void 0;
@@ -521,11 +773,23 @@ function importProtectionPlugin(opts) {
521
773
  load: {
522
774
  filter: {
523
775
  id: new RegExp(
524
- `(${RESOLVED_MOCK_MODULE_ID.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}|${RESOLVED_MARKER_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}|${RESOLVED_MOCK_EDGE_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}|${RESOLVED_MOCK_RUNTIME_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`
776
+ [
777
+ RESOLVED_MOCK_MODULE_ID,
778
+ RESOLVED_MARKER_PREFIX,
779
+ RESOLVED_MOCK_EDGE_PREFIX,
780
+ RESOLVED_MOCK_RUNTIME_PREFIX
781
+ ].map(escapeRegExp).join("|")
525
782
  )
526
783
  },
527
784
  handler(id) {
528
- if (!config.enabled) return void 0;
785
+ if (IMPORT_PROTECTION_DEBUG) {
786
+ if (matchesDebugFilter(id)) {
787
+ debugLog("load:handler", {
788
+ env: this.environment.name,
789
+ id: normalizePath(id)
790
+ });
791
+ }
792
+ }
529
793
  if (id === RESOLVED_MOCK_MODULE_ID) {
530
794
  return loadSilentMockModule();
531
795
  }
@@ -547,25 +811,9 @@ function importProtectionPlugin(opts) {
547
811
  }
548
812
  },
549
813
  {
550
- // This plugin runs WITHOUT `enforce` so it executes after all
551
- // `enforce: 'pre'` transform hooks (including the Start compiler).
552
- // It captures the transformed code + composed sourcemap for every module
553
- // so that the `resolveId` hook (in the main plugin above) can look up
554
- // the importer's transform result and map violation locations back to
555
- // original source.
556
- //
557
- // Why not use `ctx.load()` in `resolveId`?
558
- // - Vite dev: `this.load()` returns a ModuleInfo proxy that throws on
559
- // `.code` access — code is not exposed.
560
- // - Rollup build: `ModuleInfo` has `.code` but NOT `.map`, so we
561
- // can't map generated positions back to original source.
562
- //
563
- // By caching in the transform hook we get both code and the composed
564
- // sourcemap that chains all the way back to the original file.
565
- //
566
- // Performance: only files under `srcDirectory` are cached because only
567
- // those can be importers in a violation. Third-party code in
568
- // node_modules is never checked.
814
+ // Captures transformed code + composed sourcemap for location mapping.
815
+ // Runs after all `enforce: 'pre'` hooks (including the Start compiler).
816
+ // Only files under `srcDirectory` are cached.
569
817
  name: "tanstack-start-core:import-protection-transform-cache",
570
818
  applyToEnvironment(env) {
571
819
  if (!config.enabled) return false;
@@ -577,10 +825,18 @@ function importProtectionPlugin(opts) {
577
825
  include: [/\.[cm]?[tj]sx?($|\?)/]
578
826
  }
579
827
  },
580
- handler(code, id) {
581
- if (!config.enabled) return void 0;
828
+ async handler(code, id) {
582
829
  const envName = this.environment.name;
583
830
  const file = normalizeFilePath(id);
831
+ if (IMPORT_PROTECTION_DEBUG) {
832
+ if (matchesDebugFilter(file)) {
833
+ debugLog("transform-cache", {
834
+ env: envName,
835
+ id: normalizePath(id),
836
+ file
837
+ });
838
+ }
839
+ }
584
840
  if (!shouldCheckImporter(file)) {
585
841
  return void 0;
586
842
  }
@@ -601,17 +857,20 @@ function importProtectionPlugin(opts) {
601
857
  const lineIndex = buildLineIndex(code);
602
858
  const cacheKey = normalizePath(id);
603
859
  const envState = getEnv(envName);
860
+ if (id.includes(SERVER_FN_LOOKUP_QUERY)) {
861
+ envState.serverFnLookupModules.add(file);
862
+ }
604
863
  envState.transformResultCache.set(cacheKey, {
605
864
  code,
606
865
  map,
607
866
  originalCode,
608
867
  lineIndex
609
868
  });
610
- let keySet = envState.transformResultKeysByFile.get(file);
611
- if (!keySet) {
612
- keySet = /* @__PURE__ */ new Set();
613
- envState.transformResultKeysByFile.set(file, keySet);
614
- }
869
+ const keySet = getOrCreate(
870
+ envState.transformResultKeysByFile,
871
+ file,
872
+ () => /* @__PURE__ */ new Set()
873
+ );
615
874
  keySet.add(cacheKey);
616
875
  if (cacheKey !== file) {
617
876
  envState.transformResultCache.set(file, {
@@ -622,6 +881,24 @@ function importProtectionPlugin(opts) {
622
881
  });
623
882
  keySet.add(file);
624
883
  }
884
+ const importSources = extractImportSources(code);
885
+ const resolvedChildren = /* @__PURE__ */ new Set();
886
+ for (const src of importSources) {
887
+ try {
888
+ const resolved = await this.resolve(src, id, { skipSelf: true });
889
+ if (resolved && !resolved.external) {
890
+ const resolvedPath = normalizeFilePath(resolved.id);
891
+ resolvedChildren.add(resolvedPath);
892
+ envState.graph.addEdge(resolvedPath, file, src);
893
+ }
894
+ } catch {
895
+ }
896
+ }
897
+ envState.postTransformImports.set(cacheKey, resolvedChildren);
898
+ if (cacheKey !== file) {
899
+ envState.postTransformImports.set(file, resolvedChildren);
900
+ }
901
+ await processPendingViolations(envState, this.warn.bind(this));
625
902
  return void 0;
626
903
  }
627
904
  }
@@ -644,7 +921,6 @@ function importProtectionPlugin(opts) {
644
921
  }
645
922
  },
646
923
  handler(code, id) {
647
- if (!config.enabled) return void 0;
648
924
  const envName = this.environment.name;
649
925
  const envState = envStates.get(envName);
650
926
  if (!envState) return void 0;
@@ -661,56 +937,11 @@ function importProtectionPlugin(opts) {
661
937
  }
662
938
  }
663
939
  ];
664
- function handleViolation(env, info, opts2) {
665
- const key = dedupeKey(
666
- info.type,
667
- info.importer,
668
- info.specifier,
669
- info.resolved
670
- );
671
- if (!opts2?.silent) {
672
- if (config.onViolation) {
673
- const result = config.onViolation(info);
674
- if (result === false) {
675
- return void 0;
676
- }
677
- }
678
- const seen = hasSeen(env, key);
679
- if (config.effectiveBehavior === "error") {
680
- if (!seen) this.error(formatViolation(info, config.root));
681
- return void 0;
682
- }
683
- if (!seen) {
684
- this.warn(formatViolation(info, config.root));
685
- }
686
- } else {
687
- if (config.effectiveBehavior === "error") {
688
- return void 0;
689
- }
690
- }
691
- env.deniedSources.add(info.specifier);
692
- let edgeSet = env.deniedEdges.get(info.importer);
693
- if (!edgeSet) {
694
- edgeSet = /* @__PURE__ */ new Set();
695
- env.deniedEdges.set(info.importer, edgeSet);
696
- }
697
- edgeSet.add(info.specifier);
698
- if (config.command === "serve") {
699
- const runtimeId = mockRuntimeModuleIdFromViolation(
700
- info,
701
- config.mockAccess,
702
- config.root
703
- );
704
- return resolveViteId(
705
- buildMockEdgeModuleId(env, info.importer, info.specifier, runtimeId)
706
- );
707
- }
708
- return { id: RESOLVED_MOCK_MODULE_ID, syntheticNamedExports: true };
709
- }
710
940
  }
711
941
  export {
712
942
  RESOLVED_MOCK_MODULE_ID,
713
943
  dedupePatterns,
944
+ extractImportSources,
714
945
  importProtectionPlugin
715
946
  };
716
947
  //# sourceMappingURL=plugin.js.map