@tanstack/start-plugin-core 1.161.4 → 1.162.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/esm/import-protection-plugin/defaults.d.ts +6 -4
  2. package/dist/esm/import-protection-plugin/defaults.js +3 -12
  3. package/dist/esm/import-protection-plugin/defaults.js.map +1 -1
  4. package/dist/esm/import-protection-plugin/plugin.d.ts +1 -1
  5. package/dist/esm/import-protection-plugin/plugin.js +488 -257
  6. package/dist/esm/import-protection-plugin/plugin.js.map +1 -1
  7. package/dist/esm/import-protection-plugin/postCompileUsage.d.ts +4 -2
  8. package/dist/esm/import-protection-plugin/postCompileUsage.js +31 -150
  9. package/dist/esm/import-protection-plugin/postCompileUsage.js.map +1 -1
  10. package/dist/esm/import-protection-plugin/rewriteDeniedImports.js +13 -9
  11. package/dist/esm/import-protection-plugin/rewriteDeniedImports.js.map +1 -1
  12. package/dist/esm/import-protection-plugin/sourceLocation.d.ts +32 -66
  13. package/dist/esm/import-protection-plugin/sourceLocation.js +129 -56
  14. package/dist/esm/import-protection-plugin/sourceLocation.js.map +1 -1
  15. package/dist/esm/import-protection-plugin/trace.d.ts +10 -0
  16. package/dist/esm/import-protection-plugin/trace.js +30 -44
  17. package/dist/esm/import-protection-plugin/trace.js.map +1 -1
  18. package/dist/esm/import-protection-plugin/utils.d.ts +8 -4
  19. package/dist/esm/import-protection-plugin/utils.js +43 -1
  20. package/dist/esm/import-protection-plugin/utils.js.map +1 -1
  21. package/dist/esm/import-protection-plugin/virtualModules.d.ts +7 -1
  22. package/dist/esm/import-protection-plugin/virtualModules.js +104 -135
  23. package/dist/esm/import-protection-plugin/virtualModules.js.map +1 -1
  24. package/package.json +6 -6
  25. package/src/import-protection-plugin/defaults.ts +8 -19
  26. package/src/import-protection-plugin/plugin.ts +776 -433
  27. package/src/import-protection-plugin/postCompileUsage.ts +57 -229
  28. package/src/import-protection-plugin/rewriteDeniedImports.ts +34 -42
  29. package/src/import-protection-plugin/sourceLocation.ts +184 -185
  30. package/src/import-protection-plugin/trace.ts +38 -49
  31. package/src/import-protection-plugin/utils.ts +62 -1
  32. package/src/import-protection-plugin/virtualModules.ts +163 -177
@@ -1,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. 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
- // ---------------------------------------------------------------------------
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
- * 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.
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
- // Exact match via resolved absolute candidate.
186
- const resolved = normalizeSourceCandidate(src, root, sourceRoot)
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
- suffixSegmentScore(normalizedSrc, file),
193
- suffixSegmentScore(resolved, file),
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
- const best = map.sourcesContent[bestIdx]
205
- return typeof best === 'string' ? best : undefined
146
+ return map.sourcesContent[bestIdx] ?? undefined
206
147
  }
207
148
 
208
- const fallback = map.sourcesContent[0]
209
- return typeof fallback === 'string' ? fallback : undefined
149
+ return map.sourcesContent[0] ?? undefined
210
150
  }
211
151
 
212
- // ---------------------------------------------------------------------------
213
- // Sourcemap: generated original mapping
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
- export async function mapGeneratedToOriginal(
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
- // Invalid or malformed sourcemap — fall through to fallback.
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 as unknown as RawSourceMap)
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
- 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
- ]
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: Map<
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) || undefined
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
- * 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.
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: Map<
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 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).
422
+ * Build a vitest-style code snippet showing lines surrounding a location.
466
423
  *
467
- * @param contextLines Number of lines to show above/below the target line (default 2).
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 || targetLine > allLines.length) return undefined
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 startLine = Math.max(1, targetLine - contextLines)
498
- const endLine = Math.min(allLines.length, targetLine + contextLines)
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 = 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) {
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
  }