@tanstack/start-plugin-core 1.160.1 → 1.161.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 (58) hide show
  1. package/dist/esm/dev-server-plugin/plugin.js +1 -1
  2. package/dist/esm/dev-server-plugin/plugin.js.map +1 -1
  3. package/dist/esm/import-protection-plugin/defaults.d.ts +17 -0
  4. package/dist/esm/import-protection-plugin/defaults.js +36 -0
  5. package/dist/esm/import-protection-plugin/defaults.js.map +1 -0
  6. package/dist/esm/import-protection-plugin/matchers.d.ts +13 -0
  7. package/dist/esm/import-protection-plugin/matchers.js +31 -0
  8. package/dist/esm/import-protection-plugin/matchers.js.map +1 -0
  9. package/dist/esm/import-protection-plugin/plugin.d.ts +16 -0
  10. package/dist/esm/import-protection-plugin/plugin.js +699 -0
  11. package/dist/esm/import-protection-plugin/plugin.js.map +1 -0
  12. package/dist/esm/import-protection-plugin/postCompileUsage.d.ts +11 -0
  13. package/dist/esm/import-protection-plugin/postCompileUsage.js +177 -0
  14. package/dist/esm/import-protection-plugin/postCompileUsage.js.map +1 -0
  15. package/dist/esm/import-protection-plugin/rewriteDeniedImports.d.ts +27 -0
  16. package/dist/esm/import-protection-plugin/rewriteDeniedImports.js +51 -0
  17. package/dist/esm/import-protection-plugin/rewriteDeniedImports.js.map +1 -0
  18. package/dist/esm/import-protection-plugin/sourceLocation.d.ts +132 -0
  19. package/dist/esm/import-protection-plugin/sourceLocation.js +255 -0
  20. package/dist/esm/import-protection-plugin/sourceLocation.js.map +1 -0
  21. package/dist/esm/import-protection-plugin/trace.d.ts +67 -0
  22. package/dist/esm/import-protection-plugin/trace.js +204 -0
  23. package/dist/esm/import-protection-plugin/trace.js.map +1 -0
  24. package/dist/esm/import-protection-plugin/utils.d.ts +8 -0
  25. package/dist/esm/import-protection-plugin/utils.js +29 -0
  26. package/dist/esm/import-protection-plugin/utils.js.map +1 -0
  27. package/dist/esm/import-protection-plugin/virtualModules.d.ts +25 -0
  28. package/dist/esm/import-protection-plugin/virtualModules.js +235 -0
  29. package/dist/esm/import-protection-plugin/virtualModules.js.map +1 -0
  30. package/dist/esm/plugin.js +7 -0
  31. package/dist/esm/plugin.js.map +1 -1
  32. package/dist/esm/prerender.js +3 -3
  33. package/dist/esm/prerender.js.map +1 -1
  34. package/dist/esm/schema.d.ts +260 -0
  35. package/dist/esm/schema.js +35 -1
  36. package/dist/esm/schema.js.map +1 -1
  37. package/dist/esm/start-compiler-plugin/compiler.js +5 -1
  38. package/dist/esm/start-compiler-plugin/compiler.js.map +1 -1
  39. package/dist/esm/start-compiler-plugin/handleCreateServerFn.js +2 -2
  40. package/dist/esm/start-compiler-plugin/handleCreateServerFn.js.map +1 -1
  41. package/dist/esm/start-compiler-plugin/plugin.js.map +1 -1
  42. package/dist/esm/start-router-plugin/plugin.js +5 -5
  43. package/dist/esm/start-router-plugin/plugin.js.map +1 -1
  44. package/package.json +6 -3
  45. package/src/dev-server-plugin/plugin.ts +1 -1
  46. package/src/import-protection-plugin/defaults.ts +56 -0
  47. package/src/import-protection-plugin/matchers.ts +48 -0
  48. package/src/import-protection-plugin/plugin.ts +1173 -0
  49. package/src/import-protection-plugin/postCompileUsage.ts +266 -0
  50. package/src/import-protection-plugin/rewriteDeniedImports.ts +255 -0
  51. package/src/import-protection-plugin/sourceLocation.ts +524 -0
  52. package/src/import-protection-plugin/trace.ts +296 -0
  53. package/src/import-protection-plugin/utils.ts +32 -0
  54. package/src/import-protection-plugin/virtualModules.ts +300 -0
  55. package/src/plugin.ts +7 -0
  56. package/src/schema.ts +58 -0
  57. package/src/start-compiler-plugin/compiler.ts +12 -1
  58. package/src/start-compiler-plugin/plugin.ts +3 -3
@@ -0,0 +1,296 @@
1
+ import * as path from 'pathe'
2
+
3
+ export interface TraceEdge {
4
+ importer: string
5
+ specifier?: string
6
+ }
7
+
8
+ /**
9
+ * Per-environment reverse import graph.
10
+ * Maps a resolved module id to the set of modules that import it.
11
+ */
12
+ export class ImportGraph {
13
+ /**
14
+ * resolvedId -> Map<importer, specifier>
15
+ *
16
+ * We use a Map instead of a Set of objects so edges dedupe correctly.
17
+ */
18
+ readonly reverseEdges: Map<string, Map<string, string | undefined>> =
19
+ new Map()
20
+
21
+ /**
22
+ * Forward-edge index: importer -> Set<resolvedId>.
23
+ *
24
+ * Maintained alongside reverseEdges so that {@link invalidate} can remove
25
+ * all outgoing edges for a file in O(outgoing-edges) instead of scanning
26
+ * every reverse-edge map in the graph.
27
+ */
28
+ private readonly forwardEdges: Map<string, Set<string>> = new Map()
29
+
30
+ readonly entries: Set<string> = new Set()
31
+
32
+ addEdge(resolved: string, importer: string, specifier?: string): void {
33
+ let importers = this.reverseEdges.get(resolved)
34
+ if (!importers) {
35
+ importers = new Map()
36
+ this.reverseEdges.set(resolved, importers)
37
+ }
38
+ // Last writer wins; good enough for trace display.
39
+ importers.set(importer, specifier)
40
+
41
+ // Maintain forward index
42
+ let targets = this.forwardEdges.get(importer)
43
+ if (!targets) {
44
+ targets = new Set()
45
+ this.forwardEdges.set(importer, targets)
46
+ }
47
+ targets.add(resolved)
48
+ }
49
+
50
+ /** Convenience for tests/debugging. */
51
+ getEdges(resolved: string): Set<TraceEdge> | undefined {
52
+ const importers = this.reverseEdges.get(resolved)
53
+ if (!importers) return undefined
54
+ const out = new Set<TraceEdge>()
55
+ for (const [importer, specifier] of importers) {
56
+ out.add({ importer, specifier })
57
+ }
58
+ return out
59
+ }
60
+
61
+ addEntry(id: string): void {
62
+ this.entries.add(id)
63
+ }
64
+
65
+ clear(): void {
66
+ this.reverseEdges.clear()
67
+ this.forwardEdges.clear()
68
+ this.entries.clear()
69
+ }
70
+
71
+ invalidate(id: string): void {
72
+ // Remove all outgoing edges (id as importer) using the forward index.
73
+ const targets = this.forwardEdges.get(id)
74
+ if (targets) {
75
+ for (const resolved of targets) {
76
+ this.reverseEdges.get(resolved)?.delete(id)
77
+ }
78
+ this.forwardEdges.delete(id)
79
+ }
80
+ // Remove as a target (id as resolved module)
81
+ this.reverseEdges.delete(id)
82
+ }
83
+ }
84
+
85
+ export interface TraceStep {
86
+ file: string
87
+ specifier?: string
88
+ line?: number
89
+ column?: number
90
+ }
91
+
92
+ export interface Loc {
93
+ file?: string
94
+ line: number
95
+ column: number
96
+ }
97
+
98
+ /**
99
+ * BFS from a node upward through reverse edges to find the shortest
100
+ * path to an entry module.
101
+ */
102
+ export function buildTrace(
103
+ graph: ImportGraph,
104
+ startNode: string,
105
+ maxDepth: number = 20,
106
+ ): Array<TraceStep> {
107
+ // BFS upward (startNode -> importers -> ...)
108
+ const visited = new Set<string>([startNode])
109
+ const depthByNode = new Map<string, number>([[startNode, 0]])
110
+
111
+ // For any importer we visit, store the "down" link back toward startNode.
112
+ // importer --(specifier)--> next
113
+ const down = new Map<string, { next: string; specifier?: string }>()
114
+
115
+ const queue: Array<string> = [startNode]
116
+ let queueIndex = 0
117
+
118
+ let root: string | null = null
119
+
120
+ while (queueIndex < queue.length) {
121
+ const node = queue[queueIndex++]!
122
+ const depth = depthByNode.get(node) ?? 0
123
+ const importers = graph.reverseEdges.get(node)
124
+
125
+ if (node !== startNode) {
126
+ const isEntry =
127
+ graph.entries.has(node) || !importers || importers.size === 0
128
+ if (isEntry) {
129
+ root = node
130
+ break
131
+ }
132
+ }
133
+
134
+ if (depth >= maxDepth) {
135
+ continue
136
+ }
137
+
138
+ if (!importers || importers.size === 0) {
139
+ continue
140
+ }
141
+
142
+ for (const [importer, specifier] of importers) {
143
+ if (visited.has(importer)) continue
144
+ visited.add(importer)
145
+ depthByNode.set(importer, depth + 1)
146
+ down.set(importer, { next: node, specifier })
147
+ queue.push(importer)
148
+ }
149
+ }
150
+
151
+ // Best-effort: if we never found a root, just start from the original node.
152
+ if (!root) {
153
+ root = startNode
154
+ }
155
+
156
+ const trace: Array<TraceStep> = []
157
+ let current = root
158
+ for (let i = 0; i <= maxDepth + 1; i++) {
159
+ const link = down.get(current)
160
+ trace.push({ file: current, specifier: link?.specifier })
161
+ if (!link) break
162
+ current = link.next
163
+ }
164
+
165
+ return trace
166
+ }
167
+
168
+ export interface ViolationInfo {
169
+ env: string
170
+ envType: 'client' | 'server'
171
+ type: 'specifier' | 'file' | 'marker'
172
+ behavior: 'error' | 'mock'
173
+ pattern?: string | RegExp
174
+ specifier: string
175
+ importer: string
176
+ importerLoc?: Loc
177
+ resolved?: string
178
+ trace: Array<TraceStep>
179
+ message: string
180
+ /** Vitest-style code snippet showing the offending usage in the leaf module. */
181
+ snippet?: {
182
+ lines: Array<string>
183
+ highlightLine: number
184
+ location: string
185
+ }
186
+ }
187
+
188
+ export function formatViolation(info: ViolationInfo, root: string): string {
189
+ const rel = (p: string) => {
190
+ if (p.startsWith(root)) {
191
+ return path.relative(root, p)
192
+ }
193
+ return p
194
+ }
195
+
196
+ const relLoc = (p: string, loc?: Loc) => {
197
+ const r = rel(p)
198
+ const file = loc?.file ? rel(loc.file) : r
199
+ return loc ? `${file}:${loc.line}:${loc.column}` : r
200
+ }
201
+
202
+ const relTraceStep = (step: TraceStep): string => {
203
+ const file = rel(step.file)
204
+ if (step.line == null) return file
205
+ const col = step.column ?? 1
206
+ return `${file}:${step.line}:${col}`
207
+ }
208
+
209
+ const lines: Array<string> = []
210
+ lines.push(``)
211
+ lines.push(`[import-protection] Import denied in ${info.envType} environment`)
212
+ lines.push(``)
213
+
214
+ if (info.type === 'specifier') {
215
+ lines.push(` Denied by specifier pattern: ${String(info.pattern)}`)
216
+ } else if (info.type === 'file') {
217
+ lines.push(` Denied by file pattern: ${String(info.pattern)}`)
218
+ } else {
219
+ lines.push(
220
+ ` Denied by marker: module is restricted to the opposite environment`,
221
+ )
222
+ }
223
+
224
+ lines.push(` Importer: ${relLoc(info.importer, info.importerLoc)}`)
225
+ lines.push(` Import: "${rel(info.specifier)}"`)
226
+ if (info.resolved) {
227
+ lines.push(` Resolved: ${rel(info.resolved)}`)
228
+ }
229
+
230
+ if (info.trace.length > 0) {
231
+ lines.push(``)
232
+ lines.push(` Trace:`)
233
+ for (let i = 0; i < info.trace.length; i++) {
234
+ const step = info.trace[i]!
235
+ const isEntry = i === 0
236
+ const tag = isEntry ? ' (entry)' : ''
237
+ const spec = step.specifier ? ` (import "${rel(step.specifier)}")` : ''
238
+ lines.push(` ${i + 1}. ${relTraceStep(step)}${tag}${spec}`)
239
+ }
240
+ }
241
+
242
+ if (info.snippet) {
243
+ lines.push(``)
244
+ lines.push(` Code:`)
245
+ for (const snippetLine of info.snippet.lines) {
246
+ lines.push(snippetLine)
247
+ }
248
+ lines.push(``)
249
+ lines.push(` ${rel(info.snippet.location)}`)
250
+ }
251
+
252
+ lines.push(``)
253
+
254
+ // Add suggestions
255
+ if (info.envType === 'client') {
256
+ // Server-only code leaking into the client environment
257
+ lines.push(` Suggestions:`)
258
+ lines.push(
259
+ ` - Use createServerFn().handler(() => ...) to keep the logic on the server and call it from the client via an RPC bridge`,
260
+ )
261
+ lines.push(
262
+ ` - Use createServerOnlyFn(() => ...) to mark it as server-only (it will throw if accidentally called from the client)`,
263
+ )
264
+ lines.push(
265
+ ` - Use createIsomorphicFn().client(() => ...).server(() => ...) to provide separate client and server implementations`,
266
+ )
267
+ lines.push(
268
+ ` - Move the server-only import out of this file into a separate .server.ts module that is not imported by any client code`,
269
+ )
270
+ } else {
271
+ // Client-only code leaking into the server environment
272
+ const snippetText = info.snippet?.lines.join('\n') ?? ''
273
+ const looksLikeJsx =
274
+ /<[A-Z]/.test(snippetText) ||
275
+ (/\{.*\(.*\).*\}/.test(snippetText) && /</.test(snippetText))
276
+
277
+ lines.push(` Suggestions:`)
278
+ if (looksLikeJsx) {
279
+ lines.push(
280
+ ` - Wrap the JSX in <ClientOnly fallback={<Loading />}>...</ClientOnly> so it only renders in the browser after hydration`,
281
+ )
282
+ }
283
+ lines.push(
284
+ ` - Use createClientOnlyFn(() => ...) to mark it as client-only (returns undefined on the server)`,
285
+ )
286
+ lines.push(
287
+ ` - Use createIsomorphicFn().client(() => ...).server(() => ...) to provide separate client and server implementations`,
288
+ )
289
+ lines.push(
290
+ ` - Move the client-only import out of this file into a separate .client.ts module that is not imported by any server code`,
291
+ )
292
+ }
293
+
294
+ lines.push(``)
295
+ return lines.join('\n')
296
+ }
@@ -0,0 +1,32 @@
1
+ import { normalizePath } from 'vite'
2
+
3
+ export type Pattern = string | RegExp
4
+
5
+ export function dedupePatterns(patterns: Array<Pattern>): Array<Pattern> {
6
+ const out: Array<Pattern> = []
7
+ const seen = new Set<string>()
8
+ for (const p of patterns) {
9
+ const key = typeof p === 'string' ? `s:${p}` : `r:${p.toString()}`
10
+ if (seen.has(key)) continue
11
+ seen.add(key)
12
+ out.push(p)
13
+ }
14
+ return out
15
+ }
16
+
17
+ export function stripViteQuery(id: string): string {
18
+ const q = id.indexOf('?')
19
+ const h = id.indexOf('#')
20
+ if (q === -1 && h === -1) return id
21
+ if (q === -1) return id.slice(0, h)
22
+ if (h === -1) return id.slice(0, q)
23
+ return id.slice(0, Math.min(q, h))
24
+ }
25
+
26
+ /**
27
+ * Strip Vite query parameters and normalize the path in one step.
28
+ * Replaces the repeated `normalizePath(stripViteQuery(id))` pattern.
29
+ */
30
+ export function normalizeFilePath(id: string): string {
31
+ return normalizePath(stripViteQuery(id))
32
+ }
@@ -0,0 +1,300 @@
1
+ import { normalizePath } from 'vite'
2
+ import * as path from 'pathe'
3
+
4
+ import { resolveViteId } from '../utils'
5
+ import { VITE_ENVIRONMENT_NAMES } from '../constants'
6
+ import { isValidExportName } from './rewriteDeniedImports'
7
+ import type { ViolationInfo } from './trace'
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Virtual module ID constants
11
+ // ---------------------------------------------------------------------------
12
+
13
+ export const MOCK_MODULE_ID = 'tanstack-start-import-protection:mock'
14
+ export const RESOLVED_MOCK_MODULE_ID = resolveViteId(MOCK_MODULE_ID)
15
+
16
+ export const MOCK_EDGE_PREFIX = 'tanstack-start-import-protection:mock-edge:'
17
+ export const RESOLVED_MOCK_EDGE_PREFIX = resolveViteId(MOCK_EDGE_PREFIX)
18
+
19
+ // Dev-only runtime-diagnostic mock modules (used only by the client rewrite pass)
20
+ export const MOCK_RUNTIME_PREFIX =
21
+ 'tanstack-start-import-protection:mock-runtime:'
22
+ export const RESOLVED_MOCK_RUNTIME_PREFIX = resolveViteId(MOCK_RUNTIME_PREFIX)
23
+
24
+ export const MARKER_PREFIX = 'tanstack-start-import-protection:marker:'
25
+ export const RESOLVED_MARKER_PREFIX = resolveViteId(MARKER_PREFIX)
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Base64url helpers
29
+ // ---------------------------------------------------------------------------
30
+
31
+ function toBase64Url(input: string): string {
32
+ return Buffer.from(input, 'utf8').toString('base64url')
33
+ }
34
+
35
+ function fromBase64Url(input: string): string {
36
+ return Buffer.from(input, 'base64url').toString('utf8')
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Mock-runtime module helpers
41
+ // ---------------------------------------------------------------------------
42
+
43
+ export type MockAccessMode = 'error' | 'warn' | 'off'
44
+
45
+ function makeMockRuntimeModuleId(payload: {
46
+ env: string
47
+ importer: string
48
+ specifier: string
49
+ trace: Array<string>
50
+ mode: MockAccessMode
51
+ }): string {
52
+ return `${MOCK_RUNTIME_PREFIX}${toBase64Url(JSON.stringify(payload))}`
53
+ }
54
+
55
+ function stripTraceFormatting(
56
+ trace: Array<{ file: string; line?: number; column?: number }>,
57
+ root: string,
58
+ ): Array<string> {
59
+ // Keep this very small: runtime warning should show an actionable chain.
60
+ // Format: relativePath[:line:col]
61
+ const rel = (p: string) => {
62
+ if (p.startsWith(root)) return normalizePath(path.relative(root, p))
63
+ return p
64
+ }
65
+ return trace.map((s) => {
66
+ const file = rel(s.file)
67
+ if (s.line == null) return file
68
+ return `${file}:${s.line}:${s.column ?? 1}`
69
+ })
70
+ }
71
+
72
+ export function mockRuntimeModuleIdFromViolation(
73
+ info: ViolationInfo,
74
+ mode: MockAccessMode,
75
+ root: string,
76
+ ): string {
77
+ if (mode === 'off') return MOCK_MODULE_ID
78
+ // Only emit runtime diagnostics in dev and only on the client environment.
79
+ if (info.env !== VITE_ENVIRONMENT_NAMES.client) return MOCK_MODULE_ID
80
+ return makeMockRuntimeModuleId({
81
+ env: info.env,
82
+ importer: info.importer,
83
+ specifier: info.specifier,
84
+ trace: stripTraceFormatting(info.trace, root),
85
+ mode,
86
+ })
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Mock-edge module ID builder
91
+ // ---------------------------------------------------------------------------
92
+
93
+ export function makeMockEdgeModuleId(
94
+ exports: Array<string>,
95
+ source: string,
96
+ runtimeId: string,
97
+ ): string {
98
+ const payload = {
99
+ source,
100
+ exports,
101
+ runtimeId,
102
+ }
103
+ return `${MOCK_EDGE_PREFIX}${toBase64Url(JSON.stringify(payload))}`
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Load handler helpers — virtual module source code generators
108
+ // ---------------------------------------------------------------------------
109
+
110
+ export function loadSilentMockModule(): {
111
+ syntheticNamedExports: boolean
112
+ code: string
113
+ } {
114
+ return {
115
+ // syntheticNamedExports tells Rollup to derive named exports
116
+ // from the default export. Combined with the Proxy-based mock,
117
+ // this allows `import { anything } from 'mock'` to work.
118
+ syntheticNamedExports: true,
119
+ code: `
120
+ function createMock(name) {
121
+ const fn = function () {};
122
+ fn.prototype.name = name;
123
+ const children = Object.create(null);
124
+ const proxy = new Proxy(fn, {
125
+ get(target, prop) {
126
+ if (prop === '__esModule') return true;
127
+ if (prop === 'default') return proxy;
128
+ if (prop === 'caller') return null;
129
+ if (typeof prop === 'symbol') return undefined;
130
+ // Thenable support: prevent await from hanging
131
+ if (prop === 'then') return (fn) => Promise.resolve(fn(proxy));
132
+ if (prop === 'catch') return () => Promise.resolve(proxy);
133
+ if (prop === 'finally') return (fn) => { fn(); return Promise.resolve(proxy); };
134
+ // Memoize child proxies so mock.foo === mock.foo
135
+ if (!(prop in children)) {
136
+ children[prop] = createMock(name + '.' + prop);
137
+ }
138
+ return children[prop];
139
+ },
140
+ apply() {
141
+ return createMock(name + '()');
142
+ },
143
+ construct() {
144
+ return createMock('new ' + name);
145
+ },
146
+ });
147
+ return proxy;
148
+ }
149
+ const mock = createMock('mock');
150
+ export default mock;
151
+ `,
152
+ }
153
+ }
154
+
155
+ export function loadMockEdgeModule(encodedPayload: string): { code: string } {
156
+ let payload: { exports?: Array<string>; runtimeId?: string }
157
+ try {
158
+ payload = JSON.parse(fromBase64Url(encodedPayload)) as typeof payload
159
+ } catch {
160
+ payload = { exports: [] }
161
+ }
162
+ const names: Array<string> = Array.isArray(payload.exports)
163
+ ? payload.exports.filter(
164
+ (n): n is string => typeof n === 'string' && isValidExportName(n),
165
+ )
166
+ : []
167
+
168
+ const runtimeId: string =
169
+ typeof payload.runtimeId === 'string' && payload.runtimeId.length > 0
170
+ ? payload.runtimeId
171
+ : MOCK_MODULE_ID
172
+
173
+ const exportLines = names.map((n) => `export const ${n} = mock.${n};`)
174
+ return {
175
+ code: `
176
+ import mock from ${JSON.stringify(runtimeId)};
177
+ ${exportLines.join('\n')}
178
+ export default mock;
179
+ `,
180
+ }
181
+ }
182
+
183
+ export function loadMockRuntimeModule(encodedPayload: string): {
184
+ code: string
185
+ } {
186
+ let payload: {
187
+ mode?: string
188
+ env?: string
189
+ importer?: string
190
+ specifier?: string
191
+ trace?: Array<unknown>
192
+ }
193
+ try {
194
+ payload = JSON.parse(fromBase64Url(encodedPayload)) as typeof payload
195
+ } catch {
196
+ payload = {}
197
+ }
198
+
199
+ const mode: 'error' | 'warn' | 'off' =
200
+ payload.mode === 'warn' || payload.mode === 'off' ? payload.mode : 'error'
201
+
202
+ const meta = {
203
+ env: String(payload.env ?? ''),
204
+ importer: String(payload.importer ?? ''),
205
+ specifier: String(payload.specifier ?? ''),
206
+ trace: Array.isArray(payload.trace) ? payload.trace : [],
207
+ }
208
+
209
+ return {
210
+ code: `
211
+ const __meta = ${JSON.stringify(meta)};
212
+ const __mode = ${JSON.stringify(mode)};
213
+
214
+ const __seen = new Set();
215
+ function __report(action, accessPath) {
216
+ if (__mode === 'off') return;
217
+ const key = action + ':' + accessPath;
218
+ if (__seen.has(key)) return;
219
+ __seen.add(key);
220
+
221
+ const traceLines = Array.isArray(__meta.trace) && __meta.trace.length
222
+ ? "\\n\\nTrace:\\n" + __meta.trace.map((t, i) => ' ' + (i + 1) + '. ' + String(t)).join('\\n')
223
+ : '';
224
+
225
+ const msg =
226
+ '[import-protection] Mocked import used in dev client\\n\\n' +
227
+ 'Denied import: "' + __meta.specifier + '"\\n' +
228
+ 'Importer: ' + __meta.importer + '\\n' +
229
+ 'Access: ' + accessPath + ' (' + action + ')' +
230
+ traceLines +
231
+ '\\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".';
232
+
233
+ const err = new Error(msg);
234
+ if (__mode === 'warn') {
235
+ console.warn(err);
236
+ } else {
237
+ console.error(err);
238
+ }
239
+ }
240
+
241
+ function __createMock(name) {
242
+ const fn = function () {};
243
+ fn.prototype.name = name;
244
+ const children = Object.create(null);
245
+
246
+ const proxy = new Proxy(fn, {
247
+ get(_target, prop) {
248
+ if (prop === '__esModule') return true;
249
+ if (prop === 'default') return proxy;
250
+ if (prop === 'caller') return null;
251
+ if (prop === 'then') return (f) => Promise.resolve(f(proxy));
252
+ if (prop === 'catch') return () => Promise.resolve(proxy);
253
+ if (prop === 'finally') return (f) => { f(); return Promise.resolve(proxy); };
254
+
255
+ // Trigger a runtime diagnostic for primitive conversions.
256
+ if (prop === Symbol.toPrimitive) {
257
+ return () => {
258
+ __report('toPrimitive', name);
259
+ return '[import-protection mock]';
260
+ };
261
+ }
262
+ if (prop === 'toString' || prop === 'valueOf' || prop === 'toJSON') {
263
+ return () => {
264
+ __report(String(prop), name);
265
+ return '[import-protection mock]';
266
+ };
267
+ }
268
+
269
+ if (typeof prop === 'symbol') return undefined;
270
+ if (!(prop in children)) {
271
+ children[prop] = __createMock(name + '.' + prop);
272
+ }
273
+ return children[prop];
274
+ },
275
+ apply() {
276
+ __report('call', name + '()');
277
+ return __createMock(name + '()');
278
+ },
279
+ construct() {
280
+ __report('construct', 'new ' + name);
281
+ return __createMock('new ' + name);
282
+ },
283
+ set(_target, prop) {
284
+ __report('set', name + '.' + String(prop));
285
+ return true;
286
+ },
287
+ });
288
+
289
+ return proxy;
290
+ }
291
+
292
+ const mock = __createMock('mock');
293
+ export default mock;
294
+ `,
295
+ }
296
+ }
297
+
298
+ export function loadMarkerModule(): { code: string } {
299
+ return { code: 'export {}' }
300
+ }
package/src/plugin.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  } from './output-directory'
18
18
  import { postServerBuild } from './post-server-build'
19
19
  import { startCompilerPlugin } from './start-compiler-plugin/plugin'
20
+ import { importProtectionPlugin } from './import-protection-plugin/plugin'
20
21
  import type {
21
22
  GetConfigFn,
22
23
  ResolvedStartConfig,
@@ -376,6 +377,12 @@ export function TanStackStartVitePluginCore(
376
377
  generateFunctionId: startPluginOpts?.serverFns?.generateFunctionId,
377
378
  providerEnvName: serverFnProviderEnv,
378
379
  }),
380
+ importProtectionPlugin({
381
+ getConfig,
382
+ framework: corePluginOpts.framework,
383
+ environments,
384
+ providerEnvName: serverFnProviderEnv,
385
+ }),
379
386
  tanStackStartRouter(startPluginOpts, getConfig, corePluginOpts),
380
387
  loadEnvPlugin(),
381
388
  startManifestPlugin({