@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.
- package/dist/esm/import-protection-plugin/defaults.d.ts +6 -4
- package/dist/esm/import-protection-plugin/defaults.js +3 -12
- package/dist/esm/import-protection-plugin/defaults.js.map +1 -1
- package/dist/esm/import-protection-plugin/plugin.d.ts +1 -1
- package/dist/esm/import-protection-plugin/plugin.js +488 -257
- package/dist/esm/import-protection-plugin/plugin.js.map +1 -1
- package/dist/esm/import-protection-plugin/postCompileUsage.d.ts +4 -2
- package/dist/esm/import-protection-plugin/postCompileUsage.js +31 -150
- package/dist/esm/import-protection-plugin/postCompileUsage.js.map +1 -1
- package/dist/esm/import-protection-plugin/rewriteDeniedImports.js +13 -9
- package/dist/esm/import-protection-plugin/rewriteDeniedImports.js.map +1 -1
- package/dist/esm/import-protection-plugin/sourceLocation.d.ts +32 -66
- package/dist/esm/import-protection-plugin/sourceLocation.js +129 -56
- package/dist/esm/import-protection-plugin/sourceLocation.js.map +1 -1
- package/dist/esm/import-protection-plugin/trace.d.ts +10 -0
- package/dist/esm/import-protection-plugin/trace.js +30 -44
- package/dist/esm/import-protection-plugin/trace.js.map +1 -1
- package/dist/esm/import-protection-plugin/utils.d.ts +8 -4
- package/dist/esm/import-protection-plugin/utils.js +43 -1
- package/dist/esm/import-protection-plugin/utils.js.map +1 -1
- package/dist/esm/import-protection-plugin/virtualModules.d.ts +7 -1
- package/dist/esm/import-protection-plugin/virtualModules.js +104 -135
- package/dist/esm/import-protection-plugin/virtualModules.js.map +1 -1
- package/package.json +2 -2
- package/src/import-protection-plugin/defaults.ts +8 -19
- package/src/import-protection-plugin/plugin.ts +776 -433
- package/src/import-protection-plugin/postCompileUsage.ts +57 -229
- package/src/import-protection-plugin/rewriteDeniedImports.ts +34 -42
- package/src/import-protection-plugin/sourceLocation.ts +184 -185
- package/src/import-protection-plugin/trace.ts +38 -49
- package/src/import-protection-plugin/utils.ts +62 -1
- package/src/import-protection-plugin/virtualModules.ts +163 -177
|
@@ -1,22 +1,15 @@
|
|
|
1
1
|
import { SourceMapConsumer } from 'source-map'
|
|
2
2
|
import * as path from 'pathe'
|
|
3
3
|
|
|
4
|
-
import { normalizeFilePath } from './utils'
|
|
4
|
+
import { escapeRegExp, getOrCreate, normalizeFilePath } from './utils'
|
|
5
5
|
import type { Loc } from './trace'
|
|
6
6
|
import type { RawSourceMap } from 'source-map'
|
|
7
7
|
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
8
|
// Source-map type compatible with both Rollup's SourceMap and source-map's
|
|
10
|
-
// RawSourceMap.
|
|
11
|
-
// by `getCombinedSourcemap()` (version: number) flows seamlessly into
|
|
12
|
-
// `SourceMapConsumer` (version: string) without requiring a cast.
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
9
|
+
// RawSourceMap. Structural type avoids version: number vs string mismatch.
|
|
14
10
|
|
|
15
11
|
/**
|
|
16
12
|
* 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
13
|
*/
|
|
21
14
|
export interface SourceMapLike {
|
|
22
15
|
file?: string
|
|
@@ -28,21 +21,7 @@ export interface SourceMapLike {
|
|
|
28
21
|
mappings: string
|
|
29
22
|
}
|
|
30
23
|
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
24
|
// 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
25
|
export interface TransformResult {
|
|
47
26
|
code: string
|
|
48
27
|
map: SourceMapLike | undefined
|
|
@@ -54,29 +33,14 @@ export interface TransformResult {
|
|
|
54
33
|
/**
|
|
55
34
|
* Provides the transformed code and composed sourcemap for a module.
|
|
56
35
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
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.
|
|
36
|
+
* Populated from a late-running transform hook. By the time `resolveId`
|
|
37
|
+
* fires for an import, the importer has already been fully transformed.
|
|
72
38
|
*/
|
|
73
39
|
export interface TransformResultProvider {
|
|
74
40
|
getTransformResult: (id: string) => TransformResult | undefined
|
|
75
41
|
}
|
|
76
42
|
|
|
77
|
-
// ---------------------------------------------------------------------------
|
|
78
43
|
// Index → line/column conversion
|
|
79
|
-
// ---------------------------------------------------------------------------
|
|
80
44
|
|
|
81
45
|
export type LineIndex = {
|
|
82
46
|
offsets: Array<number>
|
|
@@ -107,53 +71,18 @@ function indexToLineColWithIndex(
|
|
|
107
71
|
lineIndex: LineIndex,
|
|
108
72
|
idx: number,
|
|
109
73
|
): { line: number; column0: number } {
|
|
110
|
-
let line = 1
|
|
111
|
-
|
|
112
74
|
const offsets = lineIndex.offsets
|
|
113
75
|
const ub = upperBound(offsets, idx)
|
|
114
76
|
const lineIdx = Math.max(0, ub - 1)
|
|
115
|
-
line = lineIdx + 1
|
|
77
|
+
const line = lineIdx + 1
|
|
116
78
|
|
|
117
79
|
const lineStart = offsets[lineIdx] ?? 0
|
|
118
80
|
return { line, column0: Math.max(0, idx - lineStart) }
|
|
119
81
|
}
|
|
120
82
|
|
|
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
83
|
/**
|
|
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.
|
|
84
|
+
* Pick the most-likely original source text for `importerFile` from
|
|
85
|
+
* a sourcemap that may contain multiple sources.
|
|
157
86
|
*/
|
|
158
87
|
export function pickOriginalCodeFromSourcesContent(
|
|
159
88
|
map: SourceMapLike | undefined,
|
|
@@ -166,6 +95,9 @@ export function pickOriginalCodeFromSourcesContent(
|
|
|
166
95
|
|
|
167
96
|
const file = normalizeFilePath(importerFile)
|
|
168
97
|
const sourceRoot = map.sourceRoot
|
|
98
|
+
const fileSeg = file.split('/').filter(Boolean)
|
|
99
|
+
|
|
100
|
+
const resolveBase = sourceRoot ? path.resolve(root, sourceRoot) : root
|
|
169
101
|
|
|
170
102
|
let bestIdx = -1
|
|
171
103
|
let bestScore = -1
|
|
@@ -176,21 +108,32 @@ export function pickOriginalCodeFromSourcesContent(
|
|
|
176
108
|
|
|
177
109
|
const src = map.sources[i] ?? ''
|
|
178
110
|
|
|
179
|
-
// Exact match via raw normalized source.
|
|
180
111
|
const normalizedSrc = normalizeFilePath(src)
|
|
181
112
|
if (normalizedSrc === file) {
|
|
182
113
|
return content
|
|
183
114
|
}
|
|
184
115
|
|
|
185
|
-
|
|
186
|
-
|
|
116
|
+
let resolved: string
|
|
117
|
+
if (!src) {
|
|
118
|
+
resolved = ''
|
|
119
|
+
} else if (path.isAbsolute(src)) {
|
|
120
|
+
resolved = normalizeFilePath(src)
|
|
121
|
+
} else {
|
|
122
|
+
resolved = normalizeFilePath(path.resolve(resolveBase, src))
|
|
123
|
+
}
|
|
187
124
|
if (resolved === file) {
|
|
188
125
|
return content
|
|
189
126
|
}
|
|
190
127
|
|
|
128
|
+
// Count matching path segments from the end.
|
|
129
|
+
const normalizedSrcSeg = normalizedSrc.split('/').filter(Boolean)
|
|
130
|
+
const resolvedSeg =
|
|
131
|
+
resolved !== normalizedSrc
|
|
132
|
+
? resolved.split('/').filter(Boolean)
|
|
133
|
+
: normalizedSrcSeg
|
|
191
134
|
const score = Math.max(
|
|
192
|
-
|
|
193
|
-
|
|
135
|
+
segmentSuffixScore(normalizedSrcSeg, fileSeg),
|
|
136
|
+
segmentSuffixScore(resolvedSeg, fileSeg),
|
|
194
137
|
)
|
|
195
138
|
|
|
196
139
|
if (score > bestScore) {
|
|
@@ -199,21 +142,28 @@ export function pickOriginalCodeFromSourcesContent(
|
|
|
199
142
|
}
|
|
200
143
|
}
|
|
201
144
|
|
|
202
|
-
// Require at least a basename match; otherwise fall back to index 0.
|
|
203
145
|
if (bestIdx !== -1 && bestScore >= 1) {
|
|
204
|
-
|
|
205
|
-
return typeof best === 'string' ? best : undefined
|
|
146
|
+
return map.sourcesContent[bestIdx] ?? undefined
|
|
206
147
|
}
|
|
207
148
|
|
|
208
|
-
|
|
209
|
-
return typeof fallback === 'string' ? fallback : undefined
|
|
149
|
+
return map.sourcesContent[0] ?? undefined
|
|
210
150
|
}
|
|
211
151
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
152
|
+
/** Count matching path segments from the end of `aSeg` against `bSeg`. */
|
|
153
|
+
function segmentSuffixScore(aSeg: Array<string>, bSeg: Array<string>): number {
|
|
154
|
+
let score = 0
|
|
155
|
+
for (
|
|
156
|
+
let i = aSeg.length - 1, j = bSeg.length - 1;
|
|
157
|
+
i >= 0 && j >= 0;
|
|
158
|
+
i--, j--
|
|
159
|
+
) {
|
|
160
|
+
if (aSeg[i] !== bSeg[j]) break
|
|
161
|
+
score++
|
|
162
|
+
}
|
|
163
|
+
return score
|
|
164
|
+
}
|
|
215
165
|
|
|
216
|
-
|
|
166
|
+
async function mapGeneratedToOriginal(
|
|
217
167
|
map: SourceMapLike | undefined,
|
|
218
168
|
generated: { line: number; column0: number },
|
|
219
169
|
fallbackFile: string,
|
|
@@ -244,28 +194,32 @@ export async function mapGeneratedToOriginal(
|
|
|
244
194
|
}
|
|
245
195
|
}
|
|
246
196
|
} catch {
|
|
247
|
-
//
|
|
197
|
+
// Malformed sourcemap
|
|
248
198
|
}
|
|
249
199
|
|
|
250
200
|
return fallback
|
|
251
201
|
}
|
|
252
202
|
|
|
253
|
-
// Cache SourceMapConsumer per sourcemap object.
|
|
254
203
|
const consumerCache = new WeakMap<object, Promise<SourceMapConsumer | null>>()
|
|
255
204
|
|
|
205
|
+
function toRawSourceMap(map: SourceMapLike): RawSourceMap {
|
|
206
|
+
return {
|
|
207
|
+
...map,
|
|
208
|
+
file: map.file ?? '',
|
|
209
|
+
version: Number(map.version),
|
|
210
|
+
sourcesContent: map.sourcesContent?.map((s) => s ?? '') ?? [],
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
256
214
|
async function getSourceMapConsumer(
|
|
257
215
|
map: SourceMapLike,
|
|
258
216
|
): 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
217
|
const cached = consumerCache.get(map)
|
|
264
218
|
if (cached) return cached
|
|
265
219
|
|
|
266
220
|
const promise = (async () => {
|
|
267
221
|
try {
|
|
268
|
-
return await new SourceMapConsumer(map
|
|
222
|
+
return await new SourceMapConsumer(toRawSourceMap(map))
|
|
269
223
|
} catch {
|
|
270
224
|
return null
|
|
271
225
|
}
|
|
@@ -275,24 +229,66 @@ async function getSourceMapConsumer(
|
|
|
275
229
|
return promise
|
|
276
230
|
}
|
|
277
231
|
|
|
278
|
-
|
|
279
|
-
// Import specifier search (regex-based, no AST needed)
|
|
280
|
-
// ---------------------------------------------------------------------------
|
|
232
|
+
export type ImportLocEntry = { file?: string; line: number; column: number }
|
|
281
233
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
234
|
+
/**
|
|
235
|
+
* Cache for import statement locations with reverse index for O(1)
|
|
236
|
+
* invalidation by file. Keys: `${importerFile}::${source}`.
|
|
237
|
+
*/
|
|
238
|
+
export class ImportLocCache {
|
|
239
|
+
private cache = new Map<string, ImportLocEntry | null>()
|
|
240
|
+
private reverseIndex = new Map<string, Set<string>>()
|
|
241
|
+
|
|
242
|
+
has(key: string): boolean {
|
|
243
|
+
return this.cache.has(key)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
get(key: string): ImportLocEntry | null | undefined {
|
|
247
|
+
return this.cache.get(key)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
set(key: string, value: ImportLocEntry | null): void {
|
|
251
|
+
this.cache.set(key, value)
|
|
252
|
+
const file = key.slice(0, key.indexOf('::'))
|
|
253
|
+
getOrCreate(this.reverseIndex, file, () => new Set()).add(key)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
clear(): void {
|
|
257
|
+
this.cache.clear()
|
|
258
|
+
this.reverseIndex.clear()
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Remove all cache entries where the importer matches `file`. */
|
|
262
|
+
deleteByFile(file: string): void {
|
|
263
|
+
const keys = this.reverseIndex.get(file)
|
|
264
|
+
if (keys) {
|
|
265
|
+
for (const key of keys) {
|
|
266
|
+
this.cache.delete(key)
|
|
267
|
+
}
|
|
268
|
+
this.reverseIndex.delete(file)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Import specifier search (regex-based)
|
|
274
|
+
|
|
275
|
+
const importPatternCache = new Map<string, Array<RegExp>>()
|
|
276
|
+
|
|
277
|
+
export function clearImportPatternCache(): void {
|
|
278
|
+
importPatternCache.clear()
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function findFirstImportSpecifierIndex(code: string, source: string): number {
|
|
282
|
+
let patterns = importPatternCache.get(source)
|
|
283
|
+
if (!patterns) {
|
|
284
|
+
const escaped = escapeRegExp(source)
|
|
285
|
+
patterns = [
|
|
286
|
+
new RegExp(`\\bimport\\s+(['"])${escaped}\\1`),
|
|
287
|
+
new RegExp(`\\bfrom\\s+(['"])${escaped}\\1`),
|
|
288
|
+
new RegExp(`\\bimport\\s*\\(\\s*(['"])${escaped}\\1\\s*\\)`),
|
|
289
|
+
]
|
|
290
|
+
importPatternCache.set(source, patterns)
|
|
291
|
+
}
|
|
296
292
|
|
|
297
293
|
let best = -1
|
|
298
294
|
for (const re of patterns) {
|
|
@@ -305,38 +301,24 @@ export function findFirstImportSpecifierIndex(
|
|
|
305
301
|
return best
|
|
306
302
|
}
|
|
307
303
|
|
|
308
|
-
// ---------------------------------------------------------------------------
|
|
309
|
-
// High-level location finders (use transform result cache, no disk reads)
|
|
310
|
-
// ---------------------------------------------------------------------------
|
|
311
|
-
|
|
312
304
|
/**
|
|
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
|
-
*
|
|
305
|
+
* Find the location of an import statement in a transformed module
|
|
306
|
+
* by searching the post-transform code and mapping back via sourcemap.
|
|
319
307
|
* Results are cached in `importLocCache`.
|
|
320
308
|
*/
|
|
321
309
|
export async function findImportStatementLocationFromTransformed(
|
|
322
310
|
provider: TransformResultProvider,
|
|
323
311
|
importerId: string,
|
|
324
312
|
source: string,
|
|
325
|
-
importLocCache:
|
|
326
|
-
string,
|
|
327
|
-
{ file?: string; line: number; column: number } | null
|
|
328
|
-
>,
|
|
313
|
+
importLocCache: ImportLocCache,
|
|
329
314
|
): Promise<Loc | undefined> {
|
|
330
315
|
const importerFile = normalizeFilePath(importerId)
|
|
331
316
|
const cacheKey = `${importerFile}::${source}`
|
|
332
317
|
if (importLocCache.has(cacheKey)) {
|
|
333
|
-
return importLocCache.get(cacheKey)
|
|
318
|
+
return importLocCache.get(cacheKey) ?? undefined
|
|
334
319
|
}
|
|
335
320
|
|
|
336
321
|
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
322
|
const res = provider.getTransformResult(importerId)
|
|
341
323
|
if (!res) {
|
|
342
324
|
importLocCache.set(cacheKey, null)
|
|
@@ -344,10 +326,6 @@ export async function findImportStatementLocationFromTransformed(
|
|
|
344
326
|
}
|
|
345
327
|
|
|
346
328
|
const { code, map } = res
|
|
347
|
-
if (typeof code !== 'string') {
|
|
348
|
-
importLocCache.set(cacheKey, null)
|
|
349
|
-
return undefined
|
|
350
|
-
}
|
|
351
329
|
|
|
352
330
|
const lineIndex = res.lineIndex ?? buildLineIndex(code)
|
|
353
331
|
|
|
@@ -369,10 +347,8 @@ export async function findImportStatementLocationFromTransformed(
|
|
|
369
347
|
|
|
370
348
|
/**
|
|
371
349
|
* Find the first post-compile usage location for a denied import specifier.
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
* {@link TransformResultProvider}, finds the first non-import usage of
|
|
375
|
-
* an imported binding, and maps back to original source via sourcemap.
|
|
350
|
+
* Best-effort: searches transformed code for non-import uses of imported
|
|
351
|
+
* bindings and maps back to original source via sourcemap.
|
|
376
352
|
*/
|
|
377
353
|
export async function findPostCompileUsageLocation(
|
|
378
354
|
provider: TransformResultProvider,
|
|
@@ -385,17 +361,10 @@ export async function findPostCompileUsageLocation(
|
|
|
385
361
|
): Promise<Loc | undefined> {
|
|
386
362
|
try {
|
|
387
363
|
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
364
|
const res = provider.getTransformResult(importerId)
|
|
392
365
|
if (!res) return undefined
|
|
393
366
|
const { code, map } = res
|
|
394
|
-
if (typeof code !== 'string') return undefined
|
|
395
367
|
|
|
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
368
|
if (!res.lineIndex) {
|
|
400
369
|
res.lineIndex = buildLineIndex(code)
|
|
401
370
|
}
|
|
@@ -421,10 +390,7 @@ export async function addTraceImportLocations(
|
|
|
421
390
|
line?: number
|
|
422
391
|
column?: number
|
|
423
392
|
}>,
|
|
424
|
-
importLocCache:
|
|
425
|
-
string,
|
|
426
|
-
{ file?: string; line: number; column: number } | null
|
|
427
|
-
>,
|
|
393
|
+
importLocCache: ImportLocCache,
|
|
428
394
|
): Promise<void> {
|
|
429
395
|
for (const step of trace) {
|
|
430
396
|
if (!step.specifier) continue
|
|
@@ -441,9 +407,7 @@ export async function addTraceImportLocations(
|
|
|
441
407
|
}
|
|
442
408
|
}
|
|
443
409
|
|
|
444
|
-
// ---------------------------------------------------------------------------
|
|
445
410
|
// Code snippet extraction (vitest-style context around a location)
|
|
446
|
-
// ---------------------------------------------------------------------------
|
|
447
411
|
|
|
448
412
|
export interface CodeSnippet {
|
|
449
413
|
/** Source lines with line numbers, e.g. `[" 6 | import { getSecret } from './secret.server'", ...]` */
|
|
@@ -455,16 +419,10 @@ export interface CodeSnippet {
|
|
|
455
419
|
}
|
|
456
420
|
|
|
457
421
|
/**
|
|
458
|
-
* Build a vitest-style code snippet showing
|
|
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).
|
|
422
|
+
* Build a vitest-style code snippet showing lines surrounding a location.
|
|
466
423
|
*
|
|
467
|
-
*
|
|
424
|
+
* Prefers `originalCode` from the sourcemap's sourcesContent; falls back
|
|
425
|
+
* to transformed code when unavailable.
|
|
468
426
|
*/
|
|
469
427
|
export function buildCodeSnippet(
|
|
470
428
|
provider: TransformResultProvider,
|
|
@@ -474,40 +432,81 @@ export function buildCodeSnippet(
|
|
|
474
432
|
): CodeSnippet | undefined {
|
|
475
433
|
try {
|
|
476
434
|
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
435
|
const res = provider.getTransformResult(moduleId)
|
|
481
436
|
if (!res) return undefined
|
|
482
437
|
|
|
483
438
|
const { code: transformedCode, originalCode } = res
|
|
484
|
-
if (typeof transformedCode !== 'string') return undefined
|
|
485
439
|
|
|
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
440
|
const sourceCode = originalCode ?? transformedCode
|
|
490
|
-
|
|
491
|
-
const allLines = sourceCode.split(/\r?\n/)
|
|
492
441
|
const targetLine = loc.line // 1-indexed
|
|
493
442
|
const targetCol = loc.column // 1-indexed
|
|
494
443
|
|
|
495
|
-
if (targetLine < 1
|
|
444
|
+
if (targetLine < 1) return undefined
|
|
445
|
+
|
|
446
|
+
const wantStart = Math.max(1, targetLine - contextLines)
|
|
447
|
+
const wantEnd = targetLine + contextLines
|
|
448
|
+
|
|
449
|
+
// Advance to wantStart
|
|
450
|
+
let lineNum = 1
|
|
451
|
+
let pos = 0
|
|
452
|
+
while (lineNum < wantStart && pos < sourceCode.length) {
|
|
453
|
+
const ch = sourceCode.charCodeAt(pos)
|
|
454
|
+
if (ch === 10) {
|
|
455
|
+
lineNum++
|
|
456
|
+
} else if (ch === 13) {
|
|
457
|
+
lineNum++
|
|
458
|
+
if (
|
|
459
|
+
pos + 1 < sourceCode.length &&
|
|
460
|
+
sourceCode.charCodeAt(pos + 1) === 10
|
|
461
|
+
)
|
|
462
|
+
pos++
|
|
463
|
+
}
|
|
464
|
+
pos++
|
|
465
|
+
}
|
|
466
|
+
if (lineNum < wantStart) return undefined
|
|
467
|
+
|
|
468
|
+
const lines: Array<string> = []
|
|
469
|
+
let curLine = wantStart
|
|
470
|
+
while (curLine <= wantEnd && pos <= sourceCode.length) {
|
|
471
|
+
// Find end of current line
|
|
472
|
+
let eol = pos
|
|
473
|
+
while (eol < sourceCode.length) {
|
|
474
|
+
const ch = sourceCode.charCodeAt(eol)
|
|
475
|
+
if (ch === 10 || ch === 13) break
|
|
476
|
+
eol++
|
|
477
|
+
}
|
|
478
|
+
lines.push(sourceCode.slice(pos, eol))
|
|
479
|
+
curLine++
|
|
480
|
+
if (eol < sourceCode.length) {
|
|
481
|
+
if (
|
|
482
|
+
sourceCode.charCodeAt(eol) === 13 &&
|
|
483
|
+
eol + 1 < sourceCode.length &&
|
|
484
|
+
sourceCode.charCodeAt(eol + 1) === 10
|
|
485
|
+
) {
|
|
486
|
+
pos = eol + 2
|
|
487
|
+
} else {
|
|
488
|
+
pos = eol + 1
|
|
489
|
+
}
|
|
490
|
+
} else {
|
|
491
|
+
pos = eol + 1
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (targetLine > wantStart + lines.length - 1) return undefined
|
|
496
496
|
|
|
497
|
-
const
|
|
498
|
-
const
|
|
499
|
-
const gutterWidth = String(endLine).length
|
|
497
|
+
const actualEnd = wantStart + lines.length - 1
|
|
498
|
+
const gutterWidth = String(actualEnd).length
|
|
500
499
|
|
|
501
500
|
const sourceFile = loc.file ?? importerFile
|
|
502
501
|
const snippetLines: Array<string> = []
|
|
503
|
-
for (let i =
|
|
504
|
-
const
|
|
505
|
-
const
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
if (
|
|
502
|
+
for (let i = 0; i < lines.length; i++) {
|
|
503
|
+
const ln = wantStart + i
|
|
504
|
+
const lineContent = lines[i]!
|
|
505
|
+
const lineNumStr = String(ln).padStart(gutterWidth, ' ')
|
|
506
|
+
const marker = ln === targetLine ? '>' : ' '
|
|
507
|
+
snippetLines.push(` ${marker} ${lineNumStr} | ${lineContent}`)
|
|
508
|
+
|
|
509
|
+
if (ln === targetLine && targetCol > 0) {
|
|
511
510
|
const padding = ' '.repeat(targetCol - 1)
|
|
512
511
|
snippetLines.push(` ${' '.repeat(gutterWidth)} | ${padding}^`)
|
|
513
512
|
}
|