@tanstack/start-plugin-core 1.160.2 → 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.
- package/dist/esm/dev-server-plugin/plugin.js +1 -1
- package/dist/esm/dev-server-plugin/plugin.js.map +1 -1
- package/dist/esm/import-protection-plugin/defaults.d.ts +17 -0
- package/dist/esm/import-protection-plugin/defaults.js +36 -0
- package/dist/esm/import-protection-plugin/defaults.js.map +1 -0
- package/dist/esm/import-protection-plugin/matchers.d.ts +13 -0
- package/dist/esm/import-protection-plugin/matchers.js +31 -0
- package/dist/esm/import-protection-plugin/matchers.js.map +1 -0
- package/dist/esm/import-protection-plugin/plugin.d.ts +16 -0
- package/dist/esm/import-protection-plugin/plugin.js +699 -0
- package/dist/esm/import-protection-plugin/plugin.js.map +1 -0
- package/dist/esm/import-protection-plugin/postCompileUsage.d.ts +11 -0
- package/dist/esm/import-protection-plugin/postCompileUsage.js +177 -0
- package/dist/esm/import-protection-plugin/postCompileUsage.js.map +1 -0
- package/dist/esm/import-protection-plugin/rewriteDeniedImports.d.ts +27 -0
- package/dist/esm/import-protection-plugin/rewriteDeniedImports.js +51 -0
- package/dist/esm/import-protection-plugin/rewriteDeniedImports.js.map +1 -0
- package/dist/esm/import-protection-plugin/sourceLocation.d.ts +132 -0
- package/dist/esm/import-protection-plugin/sourceLocation.js +255 -0
- package/dist/esm/import-protection-plugin/sourceLocation.js.map +1 -0
- package/dist/esm/import-protection-plugin/trace.d.ts +67 -0
- package/dist/esm/import-protection-plugin/trace.js +204 -0
- package/dist/esm/import-protection-plugin/trace.js.map +1 -0
- package/dist/esm/import-protection-plugin/utils.d.ts +8 -0
- package/dist/esm/import-protection-plugin/utils.js +29 -0
- package/dist/esm/import-protection-plugin/utils.js.map +1 -0
- package/dist/esm/import-protection-plugin/virtualModules.d.ts +25 -0
- package/dist/esm/import-protection-plugin/virtualModules.js +235 -0
- package/dist/esm/import-protection-plugin/virtualModules.js.map +1 -0
- package/dist/esm/plugin.js +7 -0
- package/dist/esm/plugin.js.map +1 -1
- package/dist/esm/prerender.js +3 -3
- package/dist/esm/prerender.js.map +1 -1
- package/dist/esm/schema.d.ts +260 -0
- package/dist/esm/schema.js +35 -1
- package/dist/esm/schema.js.map +1 -1
- package/dist/esm/start-compiler-plugin/compiler.js +5 -1
- package/dist/esm/start-compiler-plugin/compiler.js.map +1 -1
- package/dist/esm/start-compiler-plugin/handleCreateServerFn.js +2 -2
- package/dist/esm/start-compiler-plugin/handleCreateServerFn.js.map +1 -1
- package/dist/esm/start-compiler-plugin/plugin.js.map +1 -1
- package/dist/esm/start-router-plugin/plugin.js +5 -5
- package/dist/esm/start-router-plugin/plugin.js.map +1 -1
- package/package.json +6 -3
- package/src/dev-server-plugin/plugin.ts +1 -1
- package/src/import-protection-plugin/defaults.ts +56 -0
- package/src/import-protection-plugin/matchers.ts +48 -0
- package/src/import-protection-plugin/plugin.ts +1173 -0
- package/src/import-protection-plugin/postCompileUsage.ts +266 -0
- package/src/import-protection-plugin/rewriteDeniedImports.ts +255 -0
- package/src/import-protection-plugin/sourceLocation.ts +524 -0
- package/src/import-protection-plugin/trace.ts +296 -0
- package/src/import-protection-plugin/utils.ts +32 -0
- package/src/import-protection-plugin/virtualModules.ts +300 -0
- package/src/plugin.ts +7 -0
- package/src/schema.ts +58 -0
- package/src/start-compiler-plugin/compiler.ts +12 -1
- 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({
|