@tanstack/start-plugin-core 1.161.4 → 1.162.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +2 -2
  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,4 +1,4 @@
1
- import * as path from "pathe";
1
+ import { getOrCreate, relativizePath } from "./utils.js";
2
2
  class ImportGraph {
3
3
  /**
4
4
  * resolvedId -> Map<importer, specifier>
@@ -16,18 +16,11 @@ class ImportGraph {
16
16
  forwardEdges = /* @__PURE__ */ new Map();
17
17
  entries = /* @__PURE__ */ new Set();
18
18
  addEdge(resolved, importer, specifier) {
19
- let importers = this.reverseEdges.get(resolved);
20
- if (!importers) {
21
- importers = /* @__PURE__ */ new Map();
22
- this.reverseEdges.set(resolved, importers);
23
- }
24
- importers.set(importer, specifier);
25
- let targets = this.forwardEdges.get(importer);
26
- if (!targets) {
27
- targets = /* @__PURE__ */ new Set();
28
- this.forwardEdges.set(importer, targets);
29
- }
30
- targets.add(resolved);
19
+ getOrCreate(this.reverseEdges, resolved, () => /* @__PURE__ */ new Map()).set(
20
+ importer,
21
+ specifier
22
+ );
23
+ getOrCreate(this.forwardEdges, importer, () => /* @__PURE__ */ new Set()).add(resolved);
31
24
  }
32
25
  /** Convenience for tests/debugging. */
33
26
  getEdges(resolved) {
@@ -63,11 +56,11 @@ function buildTrace(graph, startNode, maxDepth = 20) {
63
56
  const depthByNode = /* @__PURE__ */ new Map([[startNode, 0]]);
64
57
  const down = /* @__PURE__ */ new Map();
65
58
  const queue = [startNode];
66
- let queueIndex = 0;
59
+ let qi = 0;
67
60
  let root = null;
68
- while (queueIndex < queue.length) {
69
- const node = queue[queueIndex++];
70
- const depth = depthByNode.get(node) ?? 0;
61
+ while (qi < queue.length) {
62
+ const node = queue[qi++];
63
+ const depth = depthByNode.get(node);
71
64
  const importers = graph.reverseEdges.get(node);
72
65
  if (node !== startNode) {
73
66
  const isEntry = graph.entries.has(node) || !importers || importers.size === 0;
@@ -103,13 +96,19 @@ function buildTrace(graph, startNode, maxDepth = 20) {
103
96
  }
104
97
  return trace;
105
98
  }
99
+ const CLIENT_ENV_SUGGESTIONS = [
100
+ "Use createServerFn().handler(() => ...) to keep the logic on the server and call it from the client via an RPC bridge",
101
+ "Use createServerOnlyFn(() => ...) to mark it as server-only (it will throw if accidentally called from the client)",
102
+ "Use createIsomorphicFn().client(() => ...).server(() => ...) to provide separate client and server implementations",
103
+ "Move the server-only import out of this file into a separate .server.ts module that is not imported by any client code"
104
+ ];
105
+ const SERVER_ENV_SUGGESTIONS = [
106
+ "Use createClientOnlyFn(() => ...) to mark it as client-only (returns undefined on the server)",
107
+ "Use createIsomorphicFn().client(() => ...).server(() => ...) to provide separate client and server implementations",
108
+ "Move the client-only import out of this file into a separate .client.ts module that is not imported by any server code"
109
+ ];
106
110
  function formatViolation(info, root) {
107
- const rel = (p) => {
108
- if (p.startsWith(root)) {
109
- return path.relative(root, p);
110
- }
111
- return p;
112
- };
111
+ const rel = (p) => relativizePath(p, root);
113
112
  const relLoc = (p, loc) => {
114
113
  const r = rel(p);
115
114
  const file = loc?.file ? rel(loc.file) : r;
@@ -162,18 +161,9 @@ function formatViolation(info, root) {
162
161
  lines.push(``);
163
162
  if (info.envType === "client") {
164
163
  lines.push(` Suggestions:`);
165
- lines.push(
166
- ` - Use createServerFn().handler(() => ...) to keep the logic on the server and call it from the client via an RPC bridge`
167
- );
168
- lines.push(
169
- ` - Use createServerOnlyFn(() => ...) to mark it as server-only (it will throw if accidentally called from the client)`
170
- );
171
- lines.push(
172
- ` - Use createIsomorphicFn().client(() => ...).server(() => ...) to provide separate client and server implementations`
173
- );
174
- lines.push(
175
- ` - Move the server-only import out of this file into a separate .server.ts module that is not imported by any client code`
176
- );
164
+ for (const s of CLIENT_ENV_SUGGESTIONS) {
165
+ lines.push(` - ${s}`);
166
+ }
177
167
  } else {
178
168
  const snippetText = info.snippet?.lines.join("\n") ?? "";
179
169
  const looksLikeJsx = /<[A-Z]/.test(snippetText) || /\{.*\(.*\).*\}/.test(snippetText) && /</.test(snippetText);
@@ -183,21 +173,17 @@ function formatViolation(info, root) {
183
173
  ` - Wrap the JSX in <ClientOnly fallback={<Loading />}>...</ClientOnly> so it only renders in the browser after hydration`
184
174
  );
185
175
  }
186
- lines.push(
187
- ` - Use createClientOnlyFn(() => ...) to mark it as client-only (returns undefined on the server)`
188
- );
189
- lines.push(
190
- ` - Use createIsomorphicFn().client(() => ...).server(() => ...) to provide separate client and server implementations`
191
- );
192
- lines.push(
193
- ` - Move the client-only import out of this file into a separate .client.ts module that is not imported by any server code`
194
- );
176
+ for (const s of SERVER_ENV_SUGGESTIONS) {
177
+ lines.push(` - ${s}`);
178
+ }
195
179
  }
196
180
  lines.push(``);
197
181
  return lines.join("\n");
198
182
  }
199
183
  export {
184
+ CLIENT_ENV_SUGGESTIONS,
200
185
  ImportGraph,
186
+ SERVER_ENV_SUGGESTIONS,
201
187
  buildTrace,
202
188
  formatViolation
203
189
  };
@@ -1 +1 @@
1
- {"version":3,"file":"trace.js","sources":["../../../src/import-protection-plugin/trace.ts"],"sourcesContent":["import * as path from 'pathe'\n\nexport interface TraceEdge {\n importer: string\n specifier?: string\n}\n\n/**\n * Per-environment reverse import graph.\n * Maps a resolved module id to the set of modules that import it.\n */\nexport class ImportGraph {\n /**\n * resolvedId -> Map<importer, specifier>\n *\n * We use a Map instead of a Set of objects so edges dedupe correctly.\n */\n readonly reverseEdges: Map<string, Map<string, string | undefined>> =\n new Map()\n\n /**\n * Forward-edge index: importer -> Set<resolvedId>.\n *\n * Maintained alongside reverseEdges so that {@link invalidate} can remove\n * all outgoing edges for a file in O(outgoing-edges) instead of scanning\n * every reverse-edge map in the graph.\n */\n private readonly forwardEdges: Map<string, Set<string>> = new Map()\n\n readonly entries: Set<string> = new Set()\n\n addEdge(resolved: string, importer: string, specifier?: string): void {\n let importers = this.reverseEdges.get(resolved)\n if (!importers) {\n importers = new Map()\n this.reverseEdges.set(resolved, importers)\n }\n // Last writer wins; good enough for trace display.\n importers.set(importer, specifier)\n\n // Maintain forward index\n let targets = this.forwardEdges.get(importer)\n if (!targets) {\n targets = new Set()\n this.forwardEdges.set(importer, targets)\n }\n targets.add(resolved)\n }\n\n /** Convenience for tests/debugging. */\n getEdges(resolved: string): Set<TraceEdge> | undefined {\n const importers = this.reverseEdges.get(resolved)\n if (!importers) return undefined\n const out = new Set<TraceEdge>()\n for (const [importer, specifier] of importers) {\n out.add({ importer, specifier })\n }\n return out\n }\n\n addEntry(id: string): void {\n this.entries.add(id)\n }\n\n clear(): void {\n this.reverseEdges.clear()\n this.forwardEdges.clear()\n this.entries.clear()\n }\n\n invalidate(id: string): void {\n // Remove all outgoing edges (id as importer) using the forward index.\n const targets = this.forwardEdges.get(id)\n if (targets) {\n for (const resolved of targets) {\n this.reverseEdges.get(resolved)?.delete(id)\n }\n this.forwardEdges.delete(id)\n }\n // Remove as a target (id as resolved module)\n this.reverseEdges.delete(id)\n }\n}\n\nexport interface TraceStep {\n file: string\n specifier?: string\n line?: number\n column?: number\n}\n\nexport interface Loc {\n file?: string\n line: number\n column: number\n}\n\n/**\n * BFS from a node upward through reverse edges to find the shortest\n * path to an entry module.\n */\nexport function buildTrace(\n graph: ImportGraph,\n startNode: string,\n maxDepth: number = 20,\n): Array<TraceStep> {\n // BFS upward (startNode -> importers -> ...)\n const visited = new Set<string>([startNode])\n const depthByNode = new Map<string, number>([[startNode, 0]])\n\n // For any importer we visit, store the \"down\" link back toward startNode.\n // importer --(specifier)--> next\n const down = new Map<string, { next: string; specifier?: string }>()\n\n const queue: Array<string> = [startNode]\n let queueIndex = 0\n\n let root: string | null = null\n\n while (queueIndex < queue.length) {\n const node = queue[queueIndex++]!\n const depth = depthByNode.get(node) ?? 0\n const importers = graph.reverseEdges.get(node)\n\n if (node !== startNode) {\n const isEntry =\n graph.entries.has(node) || !importers || importers.size === 0\n if (isEntry) {\n root = node\n break\n }\n }\n\n if (depth >= maxDepth) {\n continue\n }\n\n if (!importers || importers.size === 0) {\n continue\n }\n\n for (const [importer, specifier] of importers) {\n if (visited.has(importer)) continue\n visited.add(importer)\n depthByNode.set(importer, depth + 1)\n down.set(importer, { next: node, specifier })\n queue.push(importer)\n }\n }\n\n // Best-effort: if we never found a root, just start from the original node.\n if (!root) {\n root = startNode\n }\n\n const trace: Array<TraceStep> = []\n let current = root\n for (let i = 0; i <= maxDepth + 1; i++) {\n const link = down.get(current)\n trace.push({ file: current, specifier: link?.specifier })\n if (!link) break\n current = link.next\n }\n\n return trace\n}\n\nexport interface ViolationInfo {\n env: string\n envType: 'client' | 'server'\n type: 'specifier' | 'file' | 'marker'\n behavior: 'error' | 'mock'\n pattern?: string | RegExp\n specifier: string\n importer: string\n importerLoc?: Loc\n resolved?: string\n trace: Array<TraceStep>\n message: string\n /** Vitest-style code snippet showing the offending usage in the leaf module. */\n snippet?: {\n lines: Array<string>\n highlightLine: number\n location: string\n }\n}\n\nexport function formatViolation(info: ViolationInfo, root: string): string {\n const rel = (p: string) => {\n if (p.startsWith(root)) {\n return path.relative(root, p)\n }\n return p\n }\n\n const relLoc = (p: string, loc?: Loc) => {\n const r = rel(p)\n const file = loc?.file ? rel(loc.file) : r\n return loc ? `${file}:${loc.line}:${loc.column}` : r\n }\n\n const relTraceStep = (step: TraceStep): string => {\n const file = rel(step.file)\n if (step.line == null) return file\n const col = step.column ?? 1\n return `${file}:${step.line}:${col}`\n }\n\n const lines: Array<string> = []\n lines.push(``)\n lines.push(`[import-protection] Import denied in ${info.envType} environment`)\n lines.push(``)\n\n if (info.type === 'specifier') {\n lines.push(` Denied by specifier pattern: ${String(info.pattern)}`)\n } else if (info.type === 'file') {\n lines.push(` Denied by file pattern: ${String(info.pattern)}`)\n } else {\n lines.push(\n ` Denied by marker: module is restricted to the opposite environment`,\n )\n }\n\n lines.push(` Importer: ${relLoc(info.importer, info.importerLoc)}`)\n lines.push(` Import: \"${rel(info.specifier)}\"`)\n if (info.resolved) {\n lines.push(` Resolved: ${rel(info.resolved)}`)\n }\n\n if (info.trace.length > 0) {\n lines.push(``)\n lines.push(` Trace:`)\n for (let i = 0; i < info.trace.length; i++) {\n const step = info.trace[i]!\n const isEntry = i === 0\n const tag = isEntry ? ' (entry)' : ''\n const spec = step.specifier ? ` (import \"${rel(step.specifier)}\")` : ''\n lines.push(` ${i + 1}. ${relTraceStep(step)}${tag}${spec}`)\n }\n }\n\n if (info.snippet) {\n lines.push(``)\n lines.push(` Code:`)\n for (const snippetLine of info.snippet.lines) {\n lines.push(snippetLine)\n }\n lines.push(``)\n lines.push(` ${rel(info.snippet.location)}`)\n }\n\n lines.push(``)\n\n // Add suggestions\n if (info.envType === 'client') {\n // Server-only code leaking into the client environment\n lines.push(` Suggestions:`)\n lines.push(\n ` - Use createServerFn().handler(() => ...) to keep the logic on the server and call it from the client via an RPC bridge`,\n )\n lines.push(\n ` - Use createServerOnlyFn(() => ...) to mark it as server-only (it will throw if accidentally called from the client)`,\n )\n lines.push(\n ` - Use createIsomorphicFn().client(() => ...).server(() => ...) to provide separate client and server implementations`,\n )\n lines.push(\n ` - Move the server-only import out of this file into a separate .server.ts module that is not imported by any client code`,\n )\n } else {\n // Client-only code leaking into the server environment\n const snippetText = info.snippet?.lines.join('\\n') ?? ''\n const looksLikeJsx =\n /<[A-Z]/.test(snippetText) ||\n (/\\{.*\\(.*\\).*\\}/.test(snippetText) && /</.test(snippetText))\n\n lines.push(` Suggestions:`)\n if (looksLikeJsx) {\n lines.push(\n ` - Wrap the JSX in <ClientOnly fallback={<Loading />}>...</ClientOnly> so it only renders in the browser after hydration`,\n )\n }\n lines.push(\n ` - Use createClientOnlyFn(() => ...) to mark it as client-only (returns undefined on the server)`,\n )\n lines.push(\n ` - Use createIsomorphicFn().client(() => ...).server(() => ...) to provide separate client and server implementations`,\n )\n lines.push(\n ` - Move the client-only import out of this file into a separate .client.ts module that is not imported by any server code`,\n )\n }\n\n lines.push(``)\n return lines.join('\\n')\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"trace.js","sources":["../../../src/import-protection-plugin/trace.ts"],"sourcesContent":["import { getOrCreate, relativizePath } from './utils'\n\nexport interface TraceEdge {\n importer: string\n specifier?: string\n}\n\n/**\n * Per-environment reverse import graph.\n * Maps a resolved module id to the set of modules that import it.\n */\nexport class ImportGraph {\n /**\n * resolvedId -> Map<importer, specifier>\n *\n * We use a Map instead of a Set of objects so edges dedupe correctly.\n */\n readonly reverseEdges: Map<string, Map<string, string | undefined>> =\n new Map()\n\n /**\n * Forward-edge index: importer -> Set<resolvedId>.\n *\n * Maintained alongside reverseEdges so that {@link invalidate} can remove\n * all outgoing edges for a file in O(outgoing-edges) instead of scanning\n * every reverse-edge map in the graph.\n */\n private readonly forwardEdges: Map<string, Set<string>> = new Map()\n\n readonly entries: Set<string> = new Set()\n\n addEdge(resolved: string, importer: string, specifier?: string): void {\n getOrCreate(this.reverseEdges, resolved, () => new Map()).set(\n importer,\n specifier,\n )\n getOrCreate(this.forwardEdges, importer, () => new Set()).add(resolved)\n }\n\n /** Convenience for tests/debugging. */\n getEdges(resolved: string): Set<TraceEdge> | undefined {\n const importers = this.reverseEdges.get(resolved)\n if (!importers) return undefined\n const out = new Set<TraceEdge>()\n for (const [importer, specifier] of importers) {\n out.add({ importer, specifier })\n }\n return out\n }\n\n addEntry(id: string): void {\n this.entries.add(id)\n }\n\n clear(): void {\n this.reverseEdges.clear()\n this.forwardEdges.clear()\n this.entries.clear()\n }\n\n invalidate(id: string): void {\n // Remove all outgoing edges (id as importer) using the forward index.\n const targets = this.forwardEdges.get(id)\n if (targets) {\n for (const resolved of targets) {\n this.reverseEdges.get(resolved)?.delete(id)\n }\n this.forwardEdges.delete(id)\n }\n // Remove as a target (id as resolved module)\n this.reverseEdges.delete(id)\n }\n}\n\nexport interface TraceStep {\n file: string\n specifier?: string\n line?: number\n column?: number\n}\n\nexport interface Loc {\n file?: string\n line: number\n column: number\n}\n\n/**\n * BFS from a node upward through reverse edges to find the shortest\n * path to an entry module.\n */\nexport function buildTrace(\n graph: ImportGraph,\n startNode: string,\n maxDepth: number = 20,\n): Array<TraceStep> {\n // BFS upward (startNode -> importers -> ...)\n const visited = new Set<string>([startNode])\n const depthByNode = new Map<string, number>([[startNode, 0]])\n\n // For any importer we visit, store the \"down\" link back toward startNode.\n // importer --(specifier)--> next\n const down = new Map<string, { next: string; specifier?: string }>()\n\n const queue: Array<string> = [startNode]\n let qi = 0\n\n let root: string | null = null\n\n while (qi < queue.length) {\n const node = queue[qi++]!\n const depth = depthByNode.get(node)!\n const importers = graph.reverseEdges.get(node)\n\n if (node !== startNode) {\n const isEntry =\n graph.entries.has(node) || !importers || importers.size === 0\n if (isEntry) {\n root = node\n break\n }\n }\n\n if (depth >= maxDepth) {\n continue\n }\n\n if (!importers || importers.size === 0) {\n continue\n }\n\n for (const [importer, specifier] of importers) {\n if (visited.has(importer)) continue\n visited.add(importer)\n depthByNode.set(importer, depth + 1)\n down.set(importer, { next: node, specifier })\n queue.push(importer)\n }\n }\n\n // Best-effort: if we never found a root, just start from the original node.\n if (!root) {\n root = startNode\n }\n\n const trace: Array<TraceStep> = []\n let current = root\n for (let i = 0; i <= maxDepth + 1; i++) {\n const link = down.get(current)\n trace.push({ file: current, specifier: link?.specifier })\n if (!link) break\n current = link.next\n }\n\n return trace\n}\n\nexport interface ViolationInfo {\n env: string\n envType: 'client' | 'server'\n type: 'specifier' | 'file' | 'marker'\n behavior: 'error' | 'mock'\n pattern?: string | RegExp\n specifier: string\n importer: string\n importerLoc?: Loc\n resolved?: string\n trace: Array<TraceStep>\n message: string\n /** Vitest-style code snippet showing the offending usage in the leaf module. */\n snippet?: {\n lines: Array<string>\n highlightLine: number\n location: string\n }\n}\n\n/**\n * Suggestion strings for server-only code leaking into client environments.\n * Used by both `formatViolation` (terminal) and runtime mock modules (browser).\n */\nexport const CLIENT_ENV_SUGGESTIONS = [\n 'Use createServerFn().handler(() => ...) to keep the logic on the server and call it from the client via an RPC bridge',\n 'Use createServerOnlyFn(() => ...) to mark it as server-only (it will throw if accidentally called from the client)',\n 'Use createIsomorphicFn().client(() => ...).server(() => ...) to provide separate client and server implementations',\n 'Move the server-only import out of this file into a separate .server.ts module that is not imported by any client code',\n] as const\n\n/**\n * Suggestion strings for client-only code leaking into server environments.\n * The JSX-specific suggestion is conditionally prepended by `formatViolation`.\n */\nexport const SERVER_ENV_SUGGESTIONS = [\n 'Use createClientOnlyFn(() => ...) to mark it as client-only (returns undefined on the server)',\n 'Use createIsomorphicFn().client(() => ...).server(() => ...) to provide separate client and server implementations',\n 'Move the client-only import out of this file into a separate .client.ts module that is not imported by any server code',\n] as const\n\nexport function formatViolation(info: ViolationInfo, root: string): string {\n const rel = (p: string) => relativizePath(p, root)\n\n const relLoc = (p: string, loc?: Loc) => {\n const r = rel(p)\n const file = loc?.file ? rel(loc.file) : r\n return loc ? `${file}:${loc.line}:${loc.column}` : r\n }\n\n const relTraceStep = (step: TraceStep): string => {\n const file = rel(step.file)\n if (step.line == null) return file\n const col = step.column ?? 1\n return `${file}:${step.line}:${col}`\n }\n\n const lines: Array<string> = []\n lines.push(``)\n lines.push(`[import-protection] Import denied in ${info.envType} environment`)\n lines.push(``)\n\n if (info.type === 'specifier') {\n lines.push(` Denied by specifier pattern: ${String(info.pattern)}`)\n } else if (info.type === 'file') {\n lines.push(` Denied by file pattern: ${String(info.pattern)}`)\n } else {\n lines.push(\n ` Denied by marker: module is restricted to the opposite environment`,\n )\n }\n\n lines.push(` Importer: ${relLoc(info.importer, info.importerLoc)}`)\n lines.push(` Import: \"${rel(info.specifier)}\"`)\n if (info.resolved) {\n lines.push(` Resolved: ${rel(info.resolved)}`)\n }\n\n if (info.trace.length > 0) {\n lines.push(``)\n lines.push(` Trace:`)\n for (let i = 0; i < info.trace.length; i++) {\n const step = info.trace[i]!\n const isEntry = i === 0\n const tag = isEntry ? ' (entry)' : ''\n const spec = step.specifier ? ` (import \"${rel(step.specifier)}\")` : ''\n lines.push(` ${i + 1}. ${relTraceStep(step)}${tag}${spec}`)\n }\n }\n\n if (info.snippet) {\n lines.push(``)\n lines.push(` Code:`)\n for (const snippetLine of info.snippet.lines) {\n lines.push(snippetLine)\n }\n lines.push(``)\n lines.push(` ${rel(info.snippet.location)}`)\n }\n\n lines.push(``)\n\n // Add suggestions\n if (info.envType === 'client') {\n lines.push(` Suggestions:`)\n for (const s of CLIENT_ENV_SUGGESTIONS) {\n lines.push(` - ${s}`)\n }\n } else {\n const snippetText = info.snippet?.lines.join('\\n') ?? ''\n const looksLikeJsx =\n /<[A-Z]/.test(snippetText) ||\n (/\\{.*\\(.*\\).*\\}/.test(snippetText) && /</.test(snippetText))\n\n lines.push(` Suggestions:`)\n if (looksLikeJsx) {\n lines.push(\n ` - Wrap the JSX in <ClientOnly fallback={<Loading />}>...</ClientOnly> so it only renders in the browser after hydration`,\n )\n }\n for (const s of SERVER_ENV_SUGGESTIONS) {\n lines.push(` - ${s}`)\n }\n }\n\n lines.push(``)\n return lines.join('\\n')\n}\n"],"names":[],"mappings":"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"}
@@ -1,8 +1,12 @@
1
1
  export type Pattern = string | RegExp;
2
2
  export declare function dedupePatterns(patterns: Array<Pattern>): Array<Pattern>;
3
3
  export declare function stripViteQuery(id: string): string;
4
- /**
5
- * Strip Vite query parameters and normalize the path in one step.
6
- * Replaces the repeated `normalizePath(stripViteQuery(id))` pattern.
7
- */
8
4
  export declare function normalizeFilePath(id: string): string;
5
+ /** Clear the memoization cache (call from buildStart to bound growth). */
6
+ export declare function clearNormalizeFilePathCache(): void;
7
+ export declare function escapeRegExp(s: string): string;
8
+ /** Get a value from a Map, creating it with `factory` if absent. */
9
+ export declare function getOrCreate<TKey, TValue>(map: Map<TKey, TValue>, key: TKey, factory: () => TValue): TValue;
10
+ /** Make a path relative to `root`, keeping non-rooted paths as-is. */
11
+ export declare function relativizePath(p: string, root: string): string;
12
+ export declare function extractImportSources(code: string): Array<string>;
@@ -18,12 +18,54 @@ function stripViteQuery(id) {
18
18
  if (h === -1) return id.slice(0, q);
19
19
  return id.slice(0, Math.min(q, h));
20
20
  }
21
+ const normalizeFilePathCache = /* @__PURE__ */ new Map();
21
22
  function normalizeFilePath(id) {
22
- return normalizePath(stripViteQuery(id));
23
+ let result = normalizeFilePathCache.get(id);
24
+ if (result === void 0) {
25
+ result = normalizePath(stripViteQuery(id));
26
+ normalizeFilePathCache.set(id, result);
27
+ }
28
+ return result;
29
+ }
30
+ function clearNormalizeFilePathCache() {
31
+ normalizeFilePathCache.clear();
32
+ }
33
+ const importSourceRe = /\bfrom\s+(?:"([^"]+)"|'([^']+)')|import\s*\(\s*(?:"([^"]+)"|'([^']+)')\s*\)/g;
34
+ function escapeRegExp(s) {
35
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
36
+ }
37
+ function getOrCreate(map, key, factory) {
38
+ let value = map.get(key);
39
+ if (value === void 0) {
40
+ value = factory();
41
+ map.set(key, value);
42
+ }
43
+ return value;
44
+ }
45
+ function relativizePath(p, root) {
46
+ if (!p.startsWith(root)) return p;
47
+ const ch = p.charCodeAt(root.length);
48
+ if (ch !== 47 && !Number.isNaN(ch)) return p;
49
+ return ch === 47 ? p.slice(root.length + 1) : p.slice(root.length);
50
+ }
51
+ function extractImportSources(code) {
52
+ const sources = [];
53
+ let m;
54
+ importSourceRe.lastIndex = 0;
55
+ while ((m = importSourceRe.exec(code)) !== null) {
56
+ const src = m[1] ?? m[2] ?? m[3] ?? m[4];
57
+ if (src) sources.push(src);
58
+ }
59
+ return sources;
23
60
  }
24
61
  export {
62
+ clearNormalizeFilePathCache,
25
63
  dedupePatterns,
64
+ escapeRegExp,
65
+ extractImportSources,
66
+ getOrCreate,
26
67
  normalizeFilePath,
68
+ relativizePath,
27
69
  stripViteQuery
28
70
  };
29
71
  //# sourceMappingURL=utils.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sources":["../../../src/import-protection-plugin/utils.ts"],"sourcesContent":["import { normalizePath } from 'vite'\n\nexport type Pattern = string | RegExp\n\nexport function dedupePatterns(patterns: Array<Pattern>): Array<Pattern> {\n const out: Array<Pattern> = []\n const seen = new Set<string>()\n for (const p of patterns) {\n const key = typeof p === 'string' ? `s:${p}` : `r:${p.toString()}`\n if (seen.has(key)) continue\n seen.add(key)\n out.push(p)\n }\n return out\n}\n\nexport function stripViteQuery(id: string): string {\n const q = id.indexOf('?')\n const h = id.indexOf('#')\n if (q === -1 && h === -1) return id\n if (q === -1) return id.slice(0, h)\n if (h === -1) return id.slice(0, q)\n return id.slice(0, Math.min(q, h))\n}\n\n/**\n * Strip Vite query parameters and normalize the path in one step.\n * Replaces the repeated `normalizePath(stripViteQuery(id))` pattern.\n */\nexport function normalizeFilePath(id: string): string {\n return normalizePath(stripViteQuery(id))\n}\n"],"names":[],"mappings":";AAIO,SAAS,eAAe,UAA0C;AACvE,QAAM,MAAsB,CAAA;AAC5B,QAAM,2BAAW,IAAA;AACjB,aAAW,KAAK,UAAU;AACxB,UAAM,MAAM,OAAO,MAAM,WAAW,KAAK,CAAC,KAAK,KAAK,EAAE,SAAA,CAAU;AAChE,QAAI,KAAK,IAAI,GAAG,EAAG;AACnB,SAAK,IAAI,GAAG;AACZ,QAAI,KAAK,CAAC;AAAA,EACZ;AACA,SAAO;AACT;AAEO,SAAS,eAAe,IAAoB;AACjD,QAAM,IAAI,GAAG,QAAQ,GAAG;AACxB,QAAM,IAAI,GAAG,QAAQ,GAAG;AACxB,MAAI,MAAM,MAAM,MAAM,GAAI,QAAO;AACjC,MAAI,MAAM,GAAI,QAAO,GAAG,MAAM,GAAG,CAAC;AAClC,MAAI,MAAM,GAAI,QAAO,GAAG,MAAM,GAAG,CAAC;AAClC,SAAO,GAAG,MAAM,GAAG,KAAK,IAAI,GAAG,CAAC,CAAC;AACnC;AAMO,SAAS,kBAAkB,IAAoB;AACpD,SAAO,cAAc,eAAe,EAAE,CAAC;AACzC;"}
1
+ {"version":3,"file":"utils.js","sources":["../../../src/import-protection-plugin/utils.ts"],"sourcesContent":["import { normalizePath } from 'vite'\n\nexport type Pattern = string | RegExp\n\nexport function dedupePatterns(patterns: Array<Pattern>): Array<Pattern> {\n const out: Array<Pattern> = []\n const seen = new Set<string>()\n for (const p of patterns) {\n const key = typeof p === 'string' ? `s:${p}` : `r:${p.toString()}`\n if (seen.has(key)) continue\n seen.add(key)\n out.push(p)\n }\n return out\n}\n\nexport function stripViteQuery(id: string): string {\n const q = id.indexOf('?')\n const h = id.indexOf('#')\n if (q === -1 && h === -1) return id\n if (q === -1) return id.slice(0, h)\n if (h === -1) return id.slice(0, q)\n return id.slice(0, Math.min(q, h))\n}\n\n/**\n * Strip Vite query parameters and normalize the path in one step.\n * Replaces the repeated `normalizePath(stripViteQuery(id))` pattern.\n *\n * Results are memoized because the same module IDs are processed many\n * times across resolveId, transform, and trace-building hooks.\n */\nconst normalizeFilePathCache = new Map<string, string>()\nexport function normalizeFilePath(id: string): string {\n let result = normalizeFilePathCache.get(id)\n if (result === undefined) {\n result = normalizePath(stripViteQuery(id))\n normalizeFilePathCache.set(id, result)\n }\n return result\n}\n\n/** Clear the memoization cache (call from buildStart to bound growth). */\nexport function clearNormalizeFilePathCache(): void {\n normalizeFilePathCache.clear()\n}\n\n/**\n * Lightweight regex to extract all import/re-export source strings from\n * post-transform code. Matches:\n * - `from \"...\"` / `from '...'` (static import/export)\n * - `import(\"...\")` / `import('...')` (dynamic import)\n */\nconst importSourceRe =\n /\\bfrom\\s+(?:\"([^\"]+)\"|'([^']+)')|import\\s*\\(\\s*(?:\"([^\"]+)\"|'([^']+)')\\s*\\)/g\n\nexport function escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/** Get a value from a Map, creating it with `factory` if absent. */\nexport function getOrCreate<TKey, TValue>(\n map: Map<TKey, TValue>,\n key: TKey,\n factory: () => TValue,\n): TValue {\n let value = map.get(key)\n if (value === undefined) {\n value = factory()\n map.set(key, value)\n }\n return value\n}\n\n/** Make a path relative to `root`, keeping non-rooted paths as-is. */\nexport function relativizePath(p: string, root: string): string {\n if (!p.startsWith(root)) return p\n const ch = p.charCodeAt(root.length)\n // Must be followed by a separator or end-of-string to be a true child\n if (ch !== 47 && !Number.isNaN(ch)) return p\n return ch === 47 ? p.slice(root.length + 1) : p.slice(root.length)\n}\n\nexport function extractImportSources(code: string): Array<string> {\n const sources: Array<string> = []\n let m: RegExpExecArray | null\n importSourceRe.lastIndex = 0\n while ((m = importSourceRe.exec(code)) !== null) {\n const src = m[1] ?? m[2] ?? m[3] ?? m[4]\n if (src) sources.push(src)\n }\n return sources\n}\n"],"names":[],"mappings":";AAIO,SAAS,eAAe,UAA0C;AACvE,QAAM,MAAsB,CAAA;AAC5B,QAAM,2BAAW,IAAA;AACjB,aAAW,KAAK,UAAU;AACxB,UAAM,MAAM,OAAO,MAAM,WAAW,KAAK,CAAC,KAAK,KAAK,EAAE,SAAA,CAAU;AAChE,QAAI,KAAK,IAAI,GAAG,EAAG;AACnB,SAAK,IAAI,GAAG;AACZ,QAAI,KAAK,CAAC;AAAA,EACZ;AACA,SAAO;AACT;AAEO,SAAS,eAAe,IAAoB;AACjD,QAAM,IAAI,GAAG,QAAQ,GAAG;AACxB,QAAM,IAAI,GAAG,QAAQ,GAAG;AACxB,MAAI,MAAM,MAAM,MAAM,GAAI,QAAO;AACjC,MAAI,MAAM,GAAI,QAAO,GAAG,MAAM,GAAG,CAAC;AAClC,MAAI,MAAM,GAAI,QAAO,GAAG,MAAM,GAAG,CAAC;AAClC,SAAO,GAAG,MAAM,GAAG,KAAK,IAAI,GAAG,CAAC,CAAC;AACnC;AASA,MAAM,6CAA6B,IAAA;AAC5B,SAAS,kBAAkB,IAAoB;AACpD,MAAI,SAAS,uBAAuB,IAAI,EAAE;AAC1C,MAAI,WAAW,QAAW;AACxB,aAAS,cAAc,eAAe,EAAE,CAAC;AACzC,2BAAuB,IAAI,IAAI,MAAM;AAAA,EACvC;AACA,SAAO;AACT;AAGO,SAAS,8BAAoC;AAClD,yBAAuB,MAAA;AACzB;AAQA,MAAM,iBACJ;AAEK,SAAS,aAAa,GAAmB;AAC9C,SAAO,EAAE,QAAQ,uBAAuB,MAAM;AAChD;AAGO,SAAS,YACd,KACA,KACA,SACQ;AACR,MAAI,QAAQ,IAAI,IAAI,GAAG;AACvB,MAAI,UAAU,QAAW;AACvB,YAAQ,QAAA;AACR,QAAI,IAAI,KAAK,KAAK;AAAA,EACpB;AACA,SAAO;AACT;AAGO,SAAS,eAAe,GAAW,MAAsB;AAC9D,MAAI,CAAC,EAAE,WAAW,IAAI,EAAG,QAAO;AAChC,QAAM,KAAK,EAAE,WAAW,KAAK,MAAM;AAEnC,MAAI,OAAO,MAAM,CAAC,OAAO,MAAM,EAAE,EAAG,QAAO;AAC3C,SAAO,OAAO,KAAK,EAAE,MAAM,KAAK,SAAS,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM;AACnE;AAEO,SAAS,qBAAqB,MAA6B;AAChE,QAAM,UAAyB,CAAA;AAC/B,MAAI;AACJ,iBAAe,YAAY;AAC3B,UAAQ,IAAI,eAAe,KAAK,IAAI,OAAO,MAAM;AAC/C,UAAM,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC;AACvC,QAAI,IAAK,SAAQ,KAAK,GAAG;AAAA,EAC3B;AACA,SAAO;AACT;"}
@@ -7,7 +7,12 @@ export declare const MOCK_RUNTIME_PREFIX = "tanstack-start-import-protection:moc
7
7
  export declare const RESOLVED_MOCK_RUNTIME_PREFIX: string;
8
8
  export declare const MARKER_PREFIX = "tanstack-start-import-protection:marker:";
9
9
  export declare const RESOLVED_MARKER_PREFIX: string;
10
- export type MockAccessMode = 'error' | 'warn' | 'off';
10
+ type MockAccessMode = 'error' | 'warn' | 'off';
11
+ /**
12
+ * Compact runtime suggestion text for browser console, derived from
13
+ * {@link CLIENT_ENV_SUGGESTIONS} so there's a single source of truth.
14
+ */
15
+ export declare const RUNTIME_SUGGESTION_TEXT: string;
11
16
  export declare function mockRuntimeModuleIdFromViolation(info: ViolationInfo, mode: MockAccessMode, root: string): string;
12
17
  export declare function makeMockEdgeModuleId(exports: Array<string>, source: string, runtimeId: string): string;
13
18
  export declare function loadSilentMockModule(): {
@@ -23,3 +28,4 @@ export declare function loadMockRuntimeModule(encodedPayload: string): {
23
28
  export declare function loadMarkerModule(): {
24
29
  code: string;
25
30
  };
31
+ export {};
@@ -1,8 +1,8 @@
1
- import { normalizePath } from "vite";
2
- import * as path from "pathe";
3
1
  import { resolveViteId } from "../utils.js";
4
2
  import { VITE_ENVIRONMENT_NAMES } from "../constants.js";
5
3
  import { isValidExportName } from "./rewriteDeniedImports.js";
4
+ import { CLIENT_ENV_SUGGESTIONS } from "./trace.js";
5
+ import { relativizePath } from "./utils.js";
6
6
  const MOCK_MODULE_ID = "tanstack-start-import-protection:mock";
7
7
  const RESOLVED_MOCK_MODULE_ID = resolveViteId(MOCK_MODULE_ID);
8
8
  const MOCK_EDGE_PREFIX = "tanstack-start-import-protection:mock-edge:";
@@ -17,118 +17,34 @@ function toBase64Url(input) {
17
17
  function fromBase64Url(input) {
18
18
  return Buffer.from(input, "base64url").toString("utf8");
19
19
  }
20
- function makeMockRuntimeModuleId(payload) {
21
- return `${MOCK_RUNTIME_PREFIX}${toBase64Url(JSON.stringify(payload))}`;
22
- }
23
- function stripTraceFormatting(trace, root) {
24
- const rel = (p) => {
25
- if (p.startsWith(root)) return normalizePath(path.relative(root, p));
26
- return p;
27
- };
28
- return trace.map((s) => {
20
+ const RUNTIME_SUGGESTION_TEXT = "Fix: " + CLIENT_ENV_SUGGESTIONS.join(". ") + '. To disable these runtime diagnostics, set importProtection.mockAccess: "off".';
21
+ function mockRuntimeModuleIdFromViolation(info, mode, root) {
22
+ if (mode === "off") return MOCK_MODULE_ID;
23
+ if (info.env !== VITE_ENVIRONMENT_NAMES.client) return MOCK_MODULE_ID;
24
+ const rel = (p) => relativizePath(p, root);
25
+ const trace = info.trace.map((s) => {
29
26
  const file = rel(s.file);
30
27
  if (s.line == null) return file;
31
28
  return `${file}:${s.line}:${s.column ?? 1}`;
32
29
  });
33
- }
34
- function mockRuntimeModuleIdFromViolation(info, mode, root) {
35
- if (mode === "off") return MOCK_MODULE_ID;
36
- if (info.env !== VITE_ENVIRONMENT_NAMES.client) return MOCK_MODULE_ID;
37
- return makeMockRuntimeModuleId({
30
+ const payload = {
38
31
  env: info.env,
39
32
  importer: info.importer,
40
33
  specifier: info.specifier,
41
- trace: stripTraceFormatting(info.trace, root),
34
+ trace,
42
35
  mode
43
- });
36
+ };
37
+ return `${MOCK_RUNTIME_PREFIX}${toBase64Url(JSON.stringify(payload))}`;
44
38
  }
45
39
  function makeMockEdgeModuleId(exports, source, runtimeId) {
46
- const payload = {
47
- source,
48
- exports,
49
- runtimeId
50
- };
40
+ const payload = { source, exports, runtimeId };
51
41
  return `${MOCK_EDGE_PREFIX}${toBase64Url(JSON.stringify(payload))}`;
52
42
  }
53
- function loadSilentMockModule() {
54
- return {
55
- // syntheticNamedExports tells Rollup to derive named exports
56
- // from the default export. Combined with the Proxy-based mock,
57
- // this allows `import { anything } from 'mock'` to work.
58
- syntheticNamedExports: true,
59
- code: `
60
- function createMock(name) {
61
- const fn = function () {};
62
- fn.prototype.name = name;
63
- const children = Object.create(null);
64
- const proxy = new Proxy(fn, {
65
- get(target, prop) {
66
- if (prop === '__esModule') return true;
67
- if (prop === 'default') return proxy;
68
- if (prop === 'caller') return null;
69
- if (typeof prop === 'symbol') return undefined;
70
- // Thenable support: prevent await from hanging
71
- if (prop === 'then') return (fn) => Promise.resolve(fn(proxy));
72
- if (prop === 'catch') return () => Promise.resolve(proxy);
73
- if (prop === 'finally') return (fn) => { fn(); return Promise.resolve(proxy); };
74
- // Memoize child proxies so mock.foo === mock.foo
75
- if (!(prop in children)) {
76
- children[prop] = createMock(name + '.' + prop);
77
- }
78
- return children[prop];
79
- },
80
- apply() {
81
- return createMock(name + '()');
82
- },
83
- construct() {
84
- return createMock('new ' + name);
85
- },
86
- });
87
- return proxy;
88
- }
89
- const mock = createMock('mock');
90
- export default mock;
91
- `
92
- };
93
- }
94
- function loadMockEdgeModule(encodedPayload) {
95
- let payload;
96
- try {
97
- payload = JSON.parse(fromBase64Url(encodedPayload));
98
- } catch {
99
- payload = { exports: [] };
100
- }
101
- const names = Array.isArray(payload.exports) ? payload.exports.filter(
102
- (n) => typeof n === "string" && isValidExportName(n)
103
- ) : [];
104
- const runtimeId = typeof payload.runtimeId === "string" && payload.runtimeId.length > 0 ? payload.runtimeId : MOCK_MODULE_ID;
105
- const exportLines = names.map((n) => `export const ${n} = mock.${n};`);
106
- return {
107
- code: `
108
- import mock from ${JSON.stringify(runtimeId)};
109
- ${exportLines.join("\n")}
110
- export default mock;
111
- `
112
- };
113
- }
114
- function loadMockRuntimeModule(encodedPayload) {
115
- let payload;
116
- try {
117
- payload = JSON.parse(fromBase64Url(encodedPayload));
118
- } catch {
119
- payload = {};
120
- }
121
- const mode = payload.mode === "warn" || payload.mode === "off" ? payload.mode : "error";
122
- const meta = {
123
- env: String(payload.env ?? ""),
124
- importer: String(payload.importer ?? ""),
125
- specifier: String(payload.specifier ?? ""),
126
- trace: Array.isArray(payload.trace) ? payload.trace : []
127
- };
128
- return {
129
- code: `
130
- const __meta = ${JSON.stringify(meta)};
131
- const __mode = ${JSON.stringify(mode)};
43
+ function generateMockCode(diagnostics) {
44
+ const fnName = diagnostics ? "__createMock" : "createMock";
45
+ const hasDiag = !!diagnostics;
46
+ const preamble = hasDiag ? `const __meta = ${JSON.stringify(diagnostics.meta)};
47
+ const __mode = ${JSON.stringify(diagnostics.mode)};
132
48
 
133
49
  const __seen = new Set();
134
50
  function __report(action, accessPath) {
@@ -147,7 +63,7 @@ function __report(action, accessPath) {
147
63
  'Importer: ' + __meta.importer + '\\n' +
148
64
  'Access: ' + accessPath + ' (' + action + ')' +
149
65
  traceLines +
150
- '\\n\\nFix: Remove server-only imports from client code. Use createServerFn().handler(() => ...) to call server logic from the client via RPC, or move the import into a .server.ts file. To disable these runtime diagnostics, set importProtection.mockAccess: "off".';
66
+ '\\n\\n' + ${JSON.stringify(RUNTIME_SUGGESTION_TEXT)};
151
67
 
152
68
  const err = new Error(msg);
153
69
  if (__mode === 'warn') {
@@ -156,22 +72,8 @@ function __report(action, accessPath) {
156
72
  console.error(err);
157
73
  }
158
74
  }
159
-
160
- function __createMock(name) {
161
- const fn = function () {};
162
- fn.prototype.name = name;
163
- const children = Object.create(null);
164
-
165
- const proxy = new Proxy(fn, {
166
- get(_target, prop) {
167
- if (prop === '__esModule') return true;
168
- if (prop === 'default') return proxy;
169
- if (prop === 'caller') return null;
170
- if (prop === 'then') return (f) => Promise.resolve(f(proxy));
171
- if (prop === 'catch') return () => Promise.resolve(proxy);
172
- if (prop === 'finally') return (f) => { f(); return Promise.resolve(proxy); };
173
-
174
- // Trigger a runtime diagnostic for primitive conversions.
75
+ ` : "";
76
+ const diagGetTraps = hasDiag ? `
175
77
  if (prop === Symbol.toPrimitive) {
176
78
  return () => {
177
79
  __report('toPrimitive', name);
@@ -183,38 +85,104 @@ function __createMock(name) {
183
85
  __report(String(prop), name);
184
86
  return '[import-protection mock]';
185
87
  };
186
- }
187
-
88
+ }` : "";
89
+ const applyBody = hasDiag ? `__report('call', name + '()');
90
+ return ${fnName}(name + '()');` : `return ${fnName}(name + '()');`;
91
+ const constructBody = hasDiag ? `__report('construct', 'new ' + name);
92
+ return ${fnName}('new ' + name);` : `return ${fnName}('new ' + name);`;
93
+ const setTrap = hasDiag ? `
94
+ set(_target, prop) {
95
+ __report('set', name + '.' + String(prop));
96
+ return true;
97
+ },` : "";
98
+ return `
99
+ ${preamble}function ${fnName}(name) {
100
+ const fn = function () {};
101
+ fn.prototype.name = name;
102
+ const children = Object.create(null);
103
+ const proxy = new Proxy(fn, {
104
+ get(_target, prop) {
105
+ if (prop === '__esModule') return true;
106
+ if (prop === 'default') return proxy;
107
+ if (prop === 'caller') return null;
108
+ if (prop === 'then') return (f) => Promise.resolve(f(proxy));
109
+ if (prop === 'catch') return () => Promise.resolve(proxy);
110
+ if (prop === 'finally') return (f) => { f(); return Promise.resolve(proxy); };${diagGetTraps}
188
111
  if (typeof prop === 'symbol') return undefined;
189
112
  if (!(prop in children)) {
190
- children[prop] = __createMock(name + '.' + prop);
113
+ children[prop] = ${fnName}(name + '.' + prop);
191
114
  }
192
115
  return children[prop];
193
116
  },
194
117
  apply() {
195
- __report('call', name + '()');
196
- return __createMock(name + '()');
118
+ ${applyBody}
197
119
  },
198
120
  construct() {
199
- __report('construct', 'new ' + name);
200
- return __createMock('new ' + name);
201
- },
202
- set(_target, prop) {
203
- __report('set', name + '.' + String(prop));
204
- return true;
205
- },
121
+ ${constructBody}
122
+ },${setTrap}
206
123
  });
207
-
208
124
  return proxy;
209
125
  }
210
-
211
- const mock = __createMock('mock');
126
+ const mock = ${fnName}('mock');
127
+ export default mock;
128
+ `;
129
+ }
130
+ function loadSilentMockModule() {
131
+ return { syntheticNamedExports: true, code: generateMockCode() };
132
+ }
133
+ function loadMockEdgeModule(encodedPayload) {
134
+ let payload;
135
+ try {
136
+ payload = JSON.parse(fromBase64Url(encodedPayload));
137
+ } catch {
138
+ payload = { exports: [] };
139
+ }
140
+ const names = Array.isArray(payload.exports) ? payload.exports.filter(
141
+ (n) => typeof n === "string" && n.length > 0 && n !== "default"
142
+ ) : [];
143
+ const runtimeId = typeof payload.runtimeId === "string" && payload.runtimeId.length > 0 ? payload.runtimeId : MOCK_MODULE_ID;
144
+ const exportLines = [];
145
+ const stringExports = [];
146
+ for (let i = 0; i < names.length; i++) {
147
+ const n = names[i];
148
+ if (isValidExportName(n)) {
149
+ exportLines.push(`export const ${n} = mock.${n};`);
150
+ } else {
151
+ const alias = `__tss_str_${i}`;
152
+ exportLines.push(`const ${alias} = mock[${JSON.stringify(n)}];`);
153
+ stringExports.push({ alias, name: n });
154
+ }
155
+ }
156
+ if (stringExports.length > 0) {
157
+ const reexports = stringExports.map((s) => `${s.alias} as ${JSON.stringify(s.name)}`).join(", ");
158
+ exportLines.push(`export { ${reexports} };`);
159
+ }
160
+ return {
161
+ code: `import mock from ${JSON.stringify(runtimeId)};
162
+ ${exportLines.join("\n")}
212
163
  export default mock;
213
164
  `
214
165
  };
215
166
  }
167
+ function loadMockRuntimeModule(encodedPayload) {
168
+ let payload;
169
+ try {
170
+ payload = JSON.parse(fromBase64Url(encodedPayload));
171
+ } catch {
172
+ payload = {};
173
+ }
174
+ const mode = payload.mode === "warn" || payload.mode === "off" ? payload.mode : "error";
175
+ const meta = {
176
+ env: String(payload.env ?? ""),
177
+ importer: String(payload.importer ?? ""),
178
+ specifier: String(payload.specifier ?? ""),
179
+ trace: Array.isArray(payload.trace) ? payload.trace : []
180
+ };
181
+ return { code: generateMockCode({ meta, mode }) };
182
+ }
183
+ const MARKER_MODULE_RESULT = { code: "export {}" };
216
184
  function loadMarkerModule() {
217
- return { code: "export {}" };
185
+ return MARKER_MODULE_RESULT;
218
186
  }
219
187
  export {
220
188
  MARKER_PREFIX,
@@ -225,6 +193,7 @@ export {
225
193
  RESOLVED_MOCK_EDGE_PREFIX,
226
194
  RESOLVED_MOCK_MODULE_ID,
227
195
  RESOLVED_MOCK_RUNTIME_PREFIX,
196
+ RUNTIME_SUGGESTION_TEXT,
228
197
  loadMarkerModule,
229
198
  loadMockEdgeModule,
230
199
  loadMockRuntimeModule,