@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.
- 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,524 @@
|
|
|
1
|
+
import { SourceMapConsumer } from 'source-map'
|
|
2
|
+
import * as path from 'pathe'
|
|
3
|
+
|
|
4
|
+
import { normalizeFilePath } from './utils'
|
|
5
|
+
import type { Loc } from './trace'
|
|
6
|
+
import type { RawSourceMap } from 'source-map'
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Source-map type compatible with both Rollup's SourceMap and source-map's
|
|
10
|
+
// RawSourceMap. We define our own structural type so that the value returned
|
|
11
|
+
// by `getCombinedSourcemap()` (version: number) flows seamlessly into
|
|
12
|
+
// `SourceMapConsumer` (version: string) without requiring a cast.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Minimal source-map shape used throughout the import-protection plugin.
|
|
17
|
+
*
|
|
18
|
+
* Structurally compatible with both Rollup's `SourceMap` (version: number)
|
|
19
|
+
* and the `source-map` package's `RawSourceMap` (version: string).
|
|
20
|
+
*/
|
|
21
|
+
export interface SourceMapLike {
|
|
22
|
+
file?: string
|
|
23
|
+
sourceRoot?: string
|
|
24
|
+
version: number | string
|
|
25
|
+
sources: Array<string>
|
|
26
|
+
names: Array<string>
|
|
27
|
+
sourcesContent?: Array<string | null>
|
|
28
|
+
mappings: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Transform result provider (replaces ctx.load() which doesn't work in dev)
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A cached transform result for a single module.
|
|
37
|
+
*
|
|
38
|
+
* - `code` – fully-transformed source (after all plugins).
|
|
39
|
+
* - `map` – composed sourcemap (chains back to the original file).
|
|
40
|
+
* - `originalCode` – the untransformed source, extracted from the
|
|
41
|
+
* sourcemap's `sourcesContent[0]` during the transform
|
|
42
|
+
* hook. Used by {@link buildCodeSnippet} so we never
|
|
43
|
+
* have to re-derive it via a flaky `sourceContentFor`
|
|
44
|
+
* lookup at display time.
|
|
45
|
+
*/
|
|
46
|
+
export interface TransformResult {
|
|
47
|
+
code: string
|
|
48
|
+
map: SourceMapLike | undefined
|
|
49
|
+
originalCode: string | undefined
|
|
50
|
+
/** Precomputed line index for `code` (index → line/col). */
|
|
51
|
+
lineIndex?: LineIndex
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Provides the transformed code and composed sourcemap for a module.
|
|
56
|
+
*
|
|
57
|
+
* During `resolveId`, Vite's `this.load()` does NOT return code/map in dev
|
|
58
|
+
* mode (the ModuleInfo proxy throws on `.code` access). Even in build mode,
|
|
59
|
+
* Rollup's `ModuleInfo` has `.code` but not `.map`.
|
|
60
|
+
*
|
|
61
|
+
* Instead, we populate this cache from a late-running transform hook that
|
|
62
|
+
* stores `{ code, map, originalCode }` for every module as it passes through
|
|
63
|
+
* the pipeline. By the time `resolveId` fires for an import, the importer
|
|
64
|
+
* has already been fully transformed, so the cache always has the data we
|
|
65
|
+
* need.
|
|
66
|
+
*
|
|
67
|
+
* The `id` parameter is the **raw** module ID (may include Vite query
|
|
68
|
+
* parameters like `?tsr-split=component`). Implementations should look up
|
|
69
|
+
* with the full ID first, then fall back to the query-stripped path so that
|
|
70
|
+
* virtual-module variants are resolved correctly without losing the base-file
|
|
71
|
+
* fallback.
|
|
72
|
+
*/
|
|
73
|
+
export interface TransformResultProvider {
|
|
74
|
+
getTransformResult: (id: string) => TransformResult | undefined
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Index → line/column conversion
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
export type LineIndex = {
|
|
82
|
+
offsets: Array<number>
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function buildLineIndex(code: string): LineIndex {
|
|
86
|
+
const offsets: Array<number> = [0]
|
|
87
|
+
for (let i = 0; i < code.length; i++) {
|
|
88
|
+
if (code.charCodeAt(i) === 10) {
|
|
89
|
+
offsets.push(i + 1)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { offsets }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function upperBound(values: Array<number>, x: number): number {
|
|
96
|
+
let lo = 0
|
|
97
|
+
let hi = values.length
|
|
98
|
+
while (lo < hi) {
|
|
99
|
+
const mid = (lo + hi) >> 1
|
|
100
|
+
if (values[mid]! <= x) lo = mid + 1
|
|
101
|
+
else hi = mid
|
|
102
|
+
}
|
|
103
|
+
return lo
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function indexToLineColWithIndex(
|
|
107
|
+
lineIndex: LineIndex,
|
|
108
|
+
idx: number,
|
|
109
|
+
): { line: number; column0: number } {
|
|
110
|
+
let line = 1
|
|
111
|
+
|
|
112
|
+
const offsets = lineIndex.offsets
|
|
113
|
+
const ub = upperBound(offsets, idx)
|
|
114
|
+
const lineIdx = Math.max(0, ub - 1)
|
|
115
|
+
line = lineIdx + 1
|
|
116
|
+
|
|
117
|
+
const lineStart = offsets[lineIdx] ?? 0
|
|
118
|
+
return { line, column0: Math.max(0, idx - lineStart) }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Pick the best original source from sourcesContent
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
function suffixSegmentScore(a: string, b: string): number {
|
|
126
|
+
const aSeg = a.split('/').filter(Boolean)
|
|
127
|
+
const bSeg = b.split('/').filter(Boolean)
|
|
128
|
+
let score = 0
|
|
129
|
+
for (
|
|
130
|
+
let i = aSeg.length - 1, j = bSeg.length - 1;
|
|
131
|
+
i >= 0 && j >= 0;
|
|
132
|
+
i--, j--
|
|
133
|
+
) {
|
|
134
|
+
if (aSeg[i] !== bSeg[j]) break
|
|
135
|
+
score++
|
|
136
|
+
}
|
|
137
|
+
return score
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function normalizeSourceCandidate(
|
|
141
|
+
source: string,
|
|
142
|
+
root: string,
|
|
143
|
+
sourceRoot: string | undefined,
|
|
144
|
+
): string {
|
|
145
|
+
// Prefer resolving relative source paths against root/sourceRoot when present.
|
|
146
|
+
if (!source) return ''
|
|
147
|
+
if (path.isAbsolute(source)) return normalizeFilePath(source)
|
|
148
|
+
const base = sourceRoot ? path.resolve(root, sourceRoot) : root
|
|
149
|
+
return normalizeFilePath(path.resolve(base, source))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Pick the most-likely original source text for `importerFile`.
|
|
154
|
+
*
|
|
155
|
+
* Sourcemaps can contain multiple sources (composed maps), so `sourcesContent[0]`
|
|
156
|
+
* is not guaranteed to represent the importer.
|
|
157
|
+
*/
|
|
158
|
+
export function pickOriginalCodeFromSourcesContent(
|
|
159
|
+
map: SourceMapLike | undefined,
|
|
160
|
+
importerFile: string,
|
|
161
|
+
root: string,
|
|
162
|
+
): string | undefined {
|
|
163
|
+
if (!map?.sourcesContent || map.sources.length === 0) {
|
|
164
|
+
return undefined
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const file = normalizeFilePath(importerFile)
|
|
168
|
+
const sourceRoot = map.sourceRoot
|
|
169
|
+
|
|
170
|
+
let bestIdx = -1
|
|
171
|
+
let bestScore = -1
|
|
172
|
+
|
|
173
|
+
for (let i = 0; i < map.sources.length; i++) {
|
|
174
|
+
const content = map.sourcesContent[i]
|
|
175
|
+
if (typeof content !== 'string') continue
|
|
176
|
+
|
|
177
|
+
const src = map.sources[i] ?? ''
|
|
178
|
+
|
|
179
|
+
// Exact match via raw normalized source.
|
|
180
|
+
const normalizedSrc = normalizeFilePath(src)
|
|
181
|
+
if (normalizedSrc === file) {
|
|
182
|
+
return content
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Exact match via resolved absolute candidate.
|
|
186
|
+
const resolved = normalizeSourceCandidate(src, root, sourceRoot)
|
|
187
|
+
if (resolved === file) {
|
|
188
|
+
return content
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const score = Math.max(
|
|
192
|
+
suffixSegmentScore(normalizedSrc, file),
|
|
193
|
+
suffixSegmentScore(resolved, file),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if (score > bestScore) {
|
|
197
|
+
bestScore = score
|
|
198
|
+
bestIdx = i
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Require at least a basename match; otherwise fall back to index 0.
|
|
203
|
+
if (bestIdx !== -1 && bestScore >= 1) {
|
|
204
|
+
const best = map.sourcesContent[bestIdx]
|
|
205
|
+
return typeof best === 'string' ? best : undefined
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const fallback = map.sourcesContent[0]
|
|
209
|
+
return typeof fallback === 'string' ? fallback : undefined
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Sourcemap: generated → original mapping
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
export async function mapGeneratedToOriginal(
|
|
217
|
+
map: SourceMapLike | undefined,
|
|
218
|
+
generated: { line: number; column0: number },
|
|
219
|
+
fallbackFile: string,
|
|
220
|
+
): Promise<Loc> {
|
|
221
|
+
const fallback: Loc = {
|
|
222
|
+
file: fallbackFile,
|
|
223
|
+
line: generated.line,
|
|
224
|
+
column: generated.column0 + 1,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!map) {
|
|
228
|
+
return fallback
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const consumer = await getSourceMapConsumer(map)
|
|
232
|
+
if (!consumer) return fallback
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const orig = consumer.originalPositionFor({
|
|
236
|
+
line: generated.line,
|
|
237
|
+
column: generated.column0,
|
|
238
|
+
})
|
|
239
|
+
if (orig.line != null && orig.column != null) {
|
|
240
|
+
return {
|
|
241
|
+
file: orig.source ? normalizeFilePath(orig.source) : fallbackFile,
|
|
242
|
+
line: orig.line,
|
|
243
|
+
column: orig.column + 1,
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} catch {
|
|
247
|
+
// Invalid or malformed sourcemap — fall through to fallback.
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return fallback
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Cache SourceMapConsumer per sourcemap object.
|
|
254
|
+
const consumerCache = new WeakMap<object, Promise<SourceMapConsumer | null>>()
|
|
255
|
+
|
|
256
|
+
async function getSourceMapConsumer(
|
|
257
|
+
map: SourceMapLike,
|
|
258
|
+
): Promise<SourceMapConsumer | null> {
|
|
259
|
+
// WeakMap requires an object key; SourceMapLike should be an object in all
|
|
260
|
+
// real cases, but guard anyway.
|
|
261
|
+
// (TypeScript already guarantees `map` is an object here.)
|
|
262
|
+
|
|
263
|
+
const cached = consumerCache.get(map)
|
|
264
|
+
if (cached) return cached
|
|
265
|
+
|
|
266
|
+
const promise = (async () => {
|
|
267
|
+
try {
|
|
268
|
+
return await new SourceMapConsumer(map as unknown as RawSourceMap)
|
|
269
|
+
} catch {
|
|
270
|
+
return null
|
|
271
|
+
}
|
|
272
|
+
})()
|
|
273
|
+
|
|
274
|
+
consumerCache.set(map, promise)
|
|
275
|
+
return promise
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
// Import specifier search (regex-based, no AST needed)
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
export function findFirstImportSpecifierIndex(
|
|
283
|
+
code: string,
|
|
284
|
+
source: string,
|
|
285
|
+
): number {
|
|
286
|
+
const escaped = source.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
287
|
+
|
|
288
|
+
const patterns: Array<RegExp> = [
|
|
289
|
+
// import 'x'
|
|
290
|
+
new RegExp(`\\bimport\\s+(['"])${escaped}\\1`),
|
|
291
|
+
// import ... from 'x' / export ... from 'x'
|
|
292
|
+
new RegExp(`\\bfrom\\s+(['"])${escaped}\\1`),
|
|
293
|
+
// import('x')
|
|
294
|
+
new RegExp(`\\bimport\\s*\\(\\s*(['"])${escaped}\\1\\s*\\)`),
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
let best = -1
|
|
298
|
+
for (const re of patterns) {
|
|
299
|
+
const m = re.exec(code)
|
|
300
|
+
if (!m) continue
|
|
301
|
+
const idx = m.index + m[0].indexOf(source)
|
|
302
|
+
if (idx === -1) continue
|
|
303
|
+
if (best === -1 || idx < best) best = idx
|
|
304
|
+
}
|
|
305
|
+
return best
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// High-level location finders (use transform result cache, no disk reads)
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Find the location of an import statement in a transformed module.
|
|
314
|
+
*
|
|
315
|
+
* Looks up the module's transformed code + composed sourcemap from the
|
|
316
|
+
* {@link TransformResultProvider}, finds the import specifier in the
|
|
317
|
+
* transformed code, and maps back to the original source via the sourcemap.
|
|
318
|
+
*
|
|
319
|
+
* Results are cached in `importLocCache`.
|
|
320
|
+
*/
|
|
321
|
+
export async function findImportStatementLocationFromTransformed(
|
|
322
|
+
provider: TransformResultProvider,
|
|
323
|
+
importerId: string,
|
|
324
|
+
source: string,
|
|
325
|
+
importLocCache: Map<
|
|
326
|
+
string,
|
|
327
|
+
{ file?: string; line: number; column: number } | null
|
|
328
|
+
>,
|
|
329
|
+
): Promise<Loc | undefined> {
|
|
330
|
+
const importerFile = normalizeFilePath(importerId)
|
|
331
|
+
const cacheKey = `${importerFile}::${source}`
|
|
332
|
+
if (importLocCache.has(cacheKey)) {
|
|
333
|
+
return importLocCache.get(cacheKey) || undefined
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
// Pass the raw importerId so the provider can look up the exact virtual
|
|
338
|
+
// module variant (e.g. ?tsr-split=component) before falling back to the
|
|
339
|
+
// base file path.
|
|
340
|
+
const res = provider.getTransformResult(importerId)
|
|
341
|
+
if (!res) {
|
|
342
|
+
importLocCache.set(cacheKey, null)
|
|
343
|
+
return undefined
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const { code, map } = res
|
|
347
|
+
if (typeof code !== 'string') {
|
|
348
|
+
importLocCache.set(cacheKey, null)
|
|
349
|
+
return undefined
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const lineIndex = res.lineIndex ?? buildLineIndex(code)
|
|
353
|
+
|
|
354
|
+
const idx = findFirstImportSpecifierIndex(code, source)
|
|
355
|
+
if (idx === -1) {
|
|
356
|
+
importLocCache.set(cacheKey, null)
|
|
357
|
+
return undefined
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const generated = indexToLineColWithIndex(lineIndex, idx)
|
|
361
|
+
const loc = await mapGeneratedToOriginal(map, generated, importerFile)
|
|
362
|
+
importLocCache.set(cacheKey, loc)
|
|
363
|
+
return loc
|
|
364
|
+
} catch {
|
|
365
|
+
importLocCache.set(cacheKey, null)
|
|
366
|
+
return undefined
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Find the first post-compile usage location for a denied import specifier.
|
|
372
|
+
*
|
|
373
|
+
* Best-effort: looks up the module's transformed output from the
|
|
374
|
+
* {@link TransformResultProvider}, finds the first non-import usage of
|
|
375
|
+
* an imported binding, and maps back to original source via sourcemap.
|
|
376
|
+
*/
|
|
377
|
+
export async function findPostCompileUsageLocation(
|
|
378
|
+
provider: TransformResultProvider,
|
|
379
|
+
importerId: string,
|
|
380
|
+
source: string,
|
|
381
|
+
findPostCompileUsagePos: (
|
|
382
|
+
code: string,
|
|
383
|
+
source: string,
|
|
384
|
+
) => { line: number; column0: number } | undefined,
|
|
385
|
+
): Promise<Loc | undefined> {
|
|
386
|
+
try {
|
|
387
|
+
const importerFile = normalizeFilePath(importerId)
|
|
388
|
+
// Pass the raw importerId so the provider can look up the exact virtual
|
|
389
|
+
// module variant (e.g. ?tsr-split=component) before falling back to the
|
|
390
|
+
// base file path.
|
|
391
|
+
const res = provider.getTransformResult(importerId)
|
|
392
|
+
if (!res) return undefined
|
|
393
|
+
const { code, map } = res
|
|
394
|
+
if (typeof code !== 'string') return undefined
|
|
395
|
+
|
|
396
|
+
// Ensure we have a line index ready for any downstream mapping.
|
|
397
|
+
// (We don't currently need it here, but keeping it hot improves locality
|
|
398
|
+
// for callers that also need import-statement mapping.)
|
|
399
|
+
if (!res.lineIndex) {
|
|
400
|
+
res.lineIndex = buildLineIndex(code)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const pos = findPostCompileUsagePos(code, source)
|
|
404
|
+
if (!pos) return undefined
|
|
405
|
+
|
|
406
|
+
return await mapGeneratedToOriginal(map, pos, importerFile)
|
|
407
|
+
} catch {
|
|
408
|
+
return undefined
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Annotate each trace hop with the location of the import that created the
|
|
414
|
+
* edge (file:line:col). Skips steps that already have a location.
|
|
415
|
+
*/
|
|
416
|
+
export async function addTraceImportLocations(
|
|
417
|
+
provider: TransformResultProvider,
|
|
418
|
+
trace: Array<{
|
|
419
|
+
file: string
|
|
420
|
+
specifier?: string
|
|
421
|
+
line?: number
|
|
422
|
+
column?: number
|
|
423
|
+
}>,
|
|
424
|
+
importLocCache: Map<
|
|
425
|
+
string,
|
|
426
|
+
{ file?: string; line: number; column: number } | null
|
|
427
|
+
>,
|
|
428
|
+
): Promise<void> {
|
|
429
|
+
for (const step of trace) {
|
|
430
|
+
if (!step.specifier) continue
|
|
431
|
+
if (step.line != null && step.column != null) continue
|
|
432
|
+
const loc = await findImportStatementLocationFromTransformed(
|
|
433
|
+
provider,
|
|
434
|
+
step.file,
|
|
435
|
+
step.specifier,
|
|
436
|
+
importLocCache,
|
|
437
|
+
)
|
|
438
|
+
if (!loc) continue
|
|
439
|
+
step.line = loc.line
|
|
440
|
+
step.column = loc.column
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ---------------------------------------------------------------------------
|
|
445
|
+
// Code snippet extraction (vitest-style context around a location)
|
|
446
|
+
// ---------------------------------------------------------------------------
|
|
447
|
+
|
|
448
|
+
export interface CodeSnippet {
|
|
449
|
+
/** Source lines with line numbers, e.g. `[" 6 | import { getSecret } from './secret.server'", ...]` */
|
|
450
|
+
lines: Array<string>
|
|
451
|
+
/** The highlighted line (1-indexed original line number) */
|
|
452
|
+
highlightLine: number
|
|
453
|
+
/** Clickable file:line reference */
|
|
454
|
+
location: string
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Build a vitest-style code snippet showing the lines surrounding a location.
|
|
459
|
+
*
|
|
460
|
+
* Uses the `originalCode` stored in the transform result cache (extracted from
|
|
461
|
+
* `sourcesContent[0]` of the composed sourcemap at transform time). This is
|
|
462
|
+
* reliable regardless of how the sourcemap names its sources.
|
|
463
|
+
*
|
|
464
|
+
* Falls back to the transformed code only when `originalCode` is unavailable
|
|
465
|
+
* (e.g. a virtual module with no sourcemap).
|
|
466
|
+
*
|
|
467
|
+
* @param contextLines Number of lines to show above/below the target line (default 2).
|
|
468
|
+
*/
|
|
469
|
+
export function buildCodeSnippet(
|
|
470
|
+
provider: TransformResultProvider,
|
|
471
|
+
moduleId: string,
|
|
472
|
+
loc: Loc,
|
|
473
|
+
contextLines: number = 2,
|
|
474
|
+
): CodeSnippet | undefined {
|
|
475
|
+
try {
|
|
476
|
+
const importerFile = normalizeFilePath(moduleId)
|
|
477
|
+
// Pass the raw moduleId so the provider can look up the exact virtual
|
|
478
|
+
// module variant (e.g. ?tsr-split=component) before falling back to the
|
|
479
|
+
// base file path.
|
|
480
|
+
const res = provider.getTransformResult(moduleId)
|
|
481
|
+
if (!res) return undefined
|
|
482
|
+
|
|
483
|
+
const { code: transformedCode, originalCode } = res
|
|
484
|
+
if (typeof transformedCode !== 'string') return undefined
|
|
485
|
+
|
|
486
|
+
// Prefer the original source that was captured at transform time from the
|
|
487
|
+
// sourcemap's sourcesContent. This avoids the source-name-mismatch
|
|
488
|
+
// problem that plagued the old sourceContentFor()-based lookup.
|
|
489
|
+
const sourceCode = originalCode ?? transformedCode
|
|
490
|
+
|
|
491
|
+
const allLines = sourceCode.split(/\r?\n/)
|
|
492
|
+
const targetLine = loc.line // 1-indexed
|
|
493
|
+
const targetCol = loc.column // 1-indexed
|
|
494
|
+
|
|
495
|
+
if (targetLine < 1 || targetLine > allLines.length) return undefined
|
|
496
|
+
|
|
497
|
+
const startLine = Math.max(1, targetLine - contextLines)
|
|
498
|
+
const endLine = Math.min(allLines.length, targetLine + contextLines)
|
|
499
|
+
const gutterWidth = String(endLine).length
|
|
500
|
+
|
|
501
|
+
const sourceFile = loc.file ?? importerFile
|
|
502
|
+
const snippetLines: Array<string> = []
|
|
503
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
504
|
+
const lineContent = allLines[i - 1] ?? ''
|
|
505
|
+
const lineNum = String(i).padStart(gutterWidth, ' ')
|
|
506
|
+
const marker = i === targetLine ? '>' : ' '
|
|
507
|
+
snippetLines.push(` ${marker} ${lineNum} | ${lineContent}`)
|
|
508
|
+
|
|
509
|
+
// Add column pointer on the target line
|
|
510
|
+
if (i === targetLine && targetCol > 0) {
|
|
511
|
+
const padding = ' '.repeat(targetCol - 1)
|
|
512
|
+
snippetLines.push(` ${' '.repeat(gutterWidth)} | ${padding}^`)
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
lines: snippetLines,
|
|
518
|
+
highlightLine: targetLine,
|
|
519
|
+
location: `${sourceFile}:${targetLine}:${targetCol}`,
|
|
520
|
+
}
|
|
521
|
+
} catch {
|
|
522
|
+
return undefined
|
|
523
|
+
}
|
|
524
|
+
}
|