@forge-kit/plugin-source-map-prase 0.0.2 → 0.0.3

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 (37) hide show
  1. package/dist/app.js +3 -5
  2. package/dist/node.js +2556 -0
  3. package/package.json +5 -3
  4. package/src/App.less +35 -26
  5. package/src/App.tsx +72 -23
  6. package/src/components/map-input-panel/index.less +71 -78
  7. package/src/components/map-input-panel/index.tsx +19 -20
  8. package/src/components/panel-card/index.less +16 -13
  9. package/src/components/panel-card/index.tsx +2 -2
  10. package/src/components/trace-chain/index.less +119 -120
  11. package/src/components/trace-chain/index.tsx +9 -7
  12. package/src/main.tsx +1 -1
  13. package/src/node/index.ts +6 -0
  14. package/src/{utils/source-map → node/trace/core}/base/registry.ts +14 -11
  15. package/src/{utils/source-map → node/trace/core}/base/types.ts +1 -0
  16. package/src/node/trace/core/domain/chain-slots.ts +33 -0
  17. package/src/{utils/source-map → node/trace/core}/domain/source-content.ts +15 -2
  18. package/src/node/trace/core/domain/trace-resolver.ts +179 -0
  19. package/src/node/trace/core/domain/view-model.ts +57 -0
  20. package/src/node/trace/resolve/context.ts +59 -0
  21. package/src/node/trace/resolve/index.ts +97 -0
  22. package/src/node/trace/resolve/input.ts +35 -0
  23. package/src/node/trace/resolve/snippet-limit.ts +35 -0
  24. package/src/node/trace/resolve/types.ts +37 -0
  25. package/src/node/trace/runner.ts +149 -0
  26. package/src/shared/trace-common.ts +104 -0
  27. package/src/shared/trace-contract.ts +29 -0
  28. package/src/types.ts +19 -0
  29. package/src/utils/trace-ui/index.ts +12 -0
  30. package/src/utils/trace-ui/state.ts +81 -0
  31. package/src/utils/source-map/domain/chain-slots.ts +0 -59
  32. package/src/utils/source-map/domain/trace-resolver.ts +0 -165
  33. package/src/utils/source-map/domain/view-model.ts +0 -20
  34. package/src/utils/source-map/facade/source-map-utils.ts +0 -212
  35. package/src/utils/source-map/index.ts +0 -18
  36. /package/src/{utils/source-map → node/trace/core}/base/constants.ts +0 -0
  37. /package/src/{utils/source-map → node/trace/core}/base/path.ts +0 -0
@@ -1,130 +1,129 @@
1
1
  .trace-panel-card {
2
- flex: 1;
3
-
4
- .trace-chain-root {
2
+ .trace-panel-content {
5
3
  flex: 1;
6
- min-height: 0;
7
- height: 100%;
8
4
  overflow: auto;
9
- padding: 4px;
10
-
11
- .trace-chain-item {
12
- margin-bottom: 10px;
13
5
 
14
- &[data-dragging="true"] {
15
- opacity: 0.85;
6
+ .trace-chain-root {
7
+ flex: 1;
8
+ min-height: 0;
9
+ height: 100%;
10
+ overflow: auto;
11
+ padding: 4px;
12
+
13
+ .trace-chain-empty {
14
+ min-height: 180px;
15
+ border: 1px dashed var(--gray-6);
16
+ border-radius: var(--radius-3);
17
+ color: var(--gray-11);
18
+ font-size: 13px;
16
19
  }
17
- }
18
-
19
- .trace-chain-empty {
20
- min-height: 180px;
21
- border: 1px dashed var(--gray-6);
22
- border-radius: var(--radius-3);
23
- color: var(--gray-11);
24
- font-size: 13px;
25
- }
26
-
27
- .trace-chain-node {
28
- padding: 10px;
29
- border-radius: var(--radius-3);
30
- border: 1px solid var(--gray-6);
31
- background: linear-gradient(160deg, color-mix(in srgb, var(--gray-3) 90%, transparent), color-mix(in srgb, var(--gray-2) 90%, transparent));
32
- }
33
20
 
34
- .trace-chain-drag-handle {
35
- min-width: 46px;
36
- height: 24px;
37
- border-radius: 999px;
38
- border: 1px solid var(--gray-7);
39
- background: var(--gray-4);
40
- color: var(--gray-12);
41
- font-size: 12px;
42
- font-weight: 600;
43
- user-select: none;
44
- cursor: grab;
45
-
46
- &:active {
47
- cursor: grabbing;
21
+ .trace-chain-item {
22
+ margin-bottom: 10px;
23
+
24
+ &[data-dragging="true"] {
25
+ opacity: 0.85;
26
+ }
27
+
28
+ .trace-chain-node {
29
+ padding: 10px;
30
+ border-radius: var(--radius-3);
31
+ border: 1px solid var(--gray-6);
32
+ background: linear-gradient(160deg, color-mix(in srgb, var(--gray-3) 90%, transparent), color-mix(in srgb, var(--gray-2) 90%, transparent));
33
+
34
+ .trace-chain-drag-handle {
35
+ min-width: 46px;
36
+ height: 24px;
37
+ border-radius: 999px;
38
+ border: 1px solid var(--gray-7);
39
+ background: var(--gray-4);
40
+ color: var(--gray-12);
41
+ font-size: 12px;
42
+ font-weight: 600;
43
+ user-select: none;
44
+ cursor: grab;
45
+
46
+ &:active {
47
+ cursor: grabbing;
48
+ }
49
+ }
50
+
51
+ .trace-chain-content {
52
+ overflow: hidden;
53
+ width: 100%;
54
+
55
+ .trace-chain-header {
56
+ width: 100%;
57
+
58
+ .trace-chain-main-title {
59
+ display: block;
60
+ font-size: 13px;
61
+ font-weight: 700;
62
+ color: var(--gray-12);
63
+ line-height: 1.35;
64
+ word-break: break-all;
65
+ user-select: text;
66
+ cursor: text;
67
+ }
68
+ }
69
+
70
+ .trace-chain-row {
71
+ display: grid;
72
+ grid-template-columns: 34px minmax(0, 1fr);
73
+ gap: 6px;
74
+ padding: 2px 0;
75
+
76
+ &[data-row-type="output"] {
77
+ margin-top: 2px;
78
+ padding-top: 8px;
79
+ border-top: 1px dashed color-mix(in srgb, var(--gray-6) 78%, transparent);
80
+ }
81
+
82
+ .trace-chain-label {
83
+ min-width: 30px;
84
+ font-size: 12px;
85
+ color: var(--gray-10);
86
+ user-select: none;
87
+ margin-top: 1px;
88
+ }
89
+
90
+ .trace-chain-detail {
91
+ min-width: 0;
92
+
93
+ .trace-chain-file {
94
+ font-size: 13px;
95
+ font-weight: 600;
96
+ color: var(--gray-12);
97
+ white-space: normal;
98
+ overflow-wrap: anywhere;
99
+ word-break: break-all;
100
+ user-select: text;
101
+ cursor: text;
102
+ }
103
+
104
+ .trace-chain-position {
105
+ font-size: 12px;
106
+ color: var(--gray-11);
107
+ white-space: normal;
108
+ overflow-wrap: anywhere;
109
+ user-select: text;
110
+ cursor: text;
111
+ }
112
+ }
113
+ }
114
+
115
+ .trace-chain-error {
116
+ font-size: 12px;
117
+ color: #ff7b7b;
118
+ }
119
+
120
+ .trace-chain-pending {
121
+ font-size: 12px;
122
+ color: var(--gray-10);
123
+ }
124
+ }
125
+ }
48
126
  }
49
127
  }
50
-
51
- .trace-chain-content {
52
- overflow: hidden;
53
- width: 100%;
54
- }
55
-
56
- .trace-chain-main-title {
57
- display: block;
58
- font-size: 13px;
59
- font-weight: 700;
60
- color: var(--gray-12);
61
- line-height: 1.35;
62
- word-break: break-all;
63
- user-select: text;
64
- cursor: text;
65
- }
66
-
67
- .trace-chain-header {
68
- width: 100%;
69
- }
70
-
71
- .trace-chain-row {
72
- display: grid;
73
- grid-template-columns: 34px minmax(0, 1fr);
74
- gap: 6px;
75
- padding: 2px 0;
76
- }
77
-
78
- .trace-chain-row[data-row-type="output"] {
79
- margin-top: 2px;
80
- padding-top: 8px;
81
- border-top: 1px dashed color-mix(in srgb, var(--gray-6) 78%, transparent);
82
- }
83
-
84
- .trace-chain-label {
85
- min-width: 30px;
86
- font-size: 12px;
87
- color: var(--gray-10);
88
- user-select: none;
89
- margin-top: 1px;
90
- }
91
-
92
- .trace-chain-detail {
93
- min-width: 0;
94
- }
95
-
96
- .trace-chain-output-head {
97
- }
98
-
99
- .trace-chain-file {
100
- font-size: 13px;
101
- font-weight: 600;
102
- color: var(--gray-12);
103
- white-space: normal;
104
- overflow-wrap: anywhere;
105
- word-break: break-all;
106
- user-select: text;
107
- cursor: text;
108
- }
109
-
110
- .trace-chain-position {
111
- font-size: 12px;
112
- color: var(--gray-11);
113
- white-space: normal;
114
- overflow-wrap: anywhere;
115
- user-select: text;
116
- cursor: text;
117
- }
118
-
119
- .trace-chain-error {
120
- font-size: 12px;
121
- color: #ff7b7b;
122
- }
123
-
124
- .trace-chain-pending {
125
- font-size: 12px;
126
- color: var(--gray-10);
127
- }
128
-
129
128
  }
130
129
  }
@@ -5,6 +5,7 @@ import {SortableContext, arrayMove, useSortable, verticalListSortingStrategy} fr
5
5
  import {CSS} from "@dnd-kit/utilities";
6
6
  import {PanelCard} from "@/components/panel-card";
7
7
  import "./index.less";
8
+ import classNames from "classnames";
8
9
 
9
10
  interface TraceChainNode {
10
11
  fileName: string
@@ -74,7 +75,8 @@ const SortableTraceChainItem: React.FC<SortableTraceChainItemProps> = (props) =>
74
75
  return (
75
76
  <Box ref={setNodeRef} style={style} className="trace-chain-item" data-dragging={isDragging ? "true" : "false"}>
76
77
  <Flex className="trace-chain-node" gap="2">
77
- <Flex className="trace-chain-drag-handle" align="center" justify="center" {...attributes} {...listeners}>#{index + 1}</Flex>
78
+ <Flex className="trace-chain-drag-handle" align="center"
79
+ justify="center" {...attributes} {...listeners}>#{index + 1}</Flex>
78
80
  <Flex className="trace-chain-content" direction="column" gap="2">
79
81
  <Flex className="trace-chain-header" justify="between" align="start" gap="2">
80
82
  <Text className="trace-chain-main-title">{slot.mapFileName || "(未命名 map 文件)"}</Text>
@@ -125,20 +127,20 @@ export const TraceChain: React.FC<TraceChainProps> = (props) => {
125
127
  }
126
128
 
127
129
  return (
128
- <PanelCard title="TRACE CHAIN" className="trace-panel-card">
130
+ <PanelCard title="TRACE CHAIN" className={classNames('trace-panel-card', className)}>
129
131
  <Flex className="trace-panel-content">
130
132
  {Boolean(!slots.length) && (
131
- <Box className={className || "trace-chain-root"}>
132
- <Flex className="trace-chain-empty" align="center" justify="center">暂无解析链路,上传 map
133
- 并点击“解析链路”。</Flex>
133
+ <Box className="trace-chain-root">
134
+ <Flex className="trace-chain-empty" align="center" justify="center">暂无解析链路,上传 map 并点击“解析链路”。</Flex>
134
135
  </Box>
135
136
  )}
136
137
  {Boolean(slots.length) && (
137
138
  <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
138
139
  <SortableContext items={slots.map((item) => item.id)} strategy={verticalListSortingStrategy}>
139
- <Box className={className || "trace-chain-root"}>
140
+ <Box className="trace-chain-root">
140
141
  {slots.map((slot, index) => {
141
- return <SortableTraceChainItem key={slot.id} slot={slot} hop={data[index]} index={index} onDelete={onDelete}/>
142
+ return <SortableTraceChainItem key={slot.id} slot={slot} hop={data[index]}
143
+ index={index} onDelete={onDelete}/>
142
144
  })}
143
145
  </Box>
144
146
  </SortableContext>
package/src/main.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  import {StrictMode} from 'react'
2
2
  import ReactDOM from "react-dom/client";
3
- import {definePlugin} from '@forge-kit/types'
3
+ import {definePlugin} from '@forge-kit/helper'
4
4
  import {App} from '@/App.tsx'
5
5
 
6
6
  let rootInstance: ReturnType<typeof ReactDOM.createRoot> | null = null;
@@ -0,0 +1,6 @@
1
+ import {defineInvokeMethod, registerInvokeMethod} from "@forge-kit/node-sdk";
2
+ import {getSourceSnippet, profileResolveTracePhases, resolveTrace} from "./trace/resolve";
3
+
4
+ const invokeMethod = defineInvokeMethod({resolveTrace, getSourceSnippet, profileResolveTracePhases})
5
+
6
+ registerInvokeMethod(invokeMethod)
@@ -1,19 +1,23 @@
1
1
  import type {SourceMapRecord, SourceMapRegistry} from "./types";
2
2
  import {getBaseName, normalizePath} from "./path";
3
3
 
4
- export const registerSourceMap = (registry: SourceMapRegistry, key: string, sourceMapRecord: SourceMapRecord) => {
4
+ interface RegisterSourceMapInput {
5
+ registry: SourceMapRegistry
6
+ key: string
7
+ sourceMapRecord: SourceMapRecord
8
+ }
9
+
10
+ export const registerSourceMap = (input: RegisterSourceMapInput) => {
11
+ const {registry, key, sourceMapRecord} = input
5
12
  const normalized = normalizePath(key)
13
+
6
14
  if (!normalized) return registry
7
15
 
8
- const nextRegistry: SourceMapRegistry = {
9
- ...registry,
10
- [normalized]: sourceMapRecord,
11
- }
16
+ const nextRegistry: SourceMapRegistry = {...registry, [normalized]: sourceMapRecord}
12
17
 
13
18
  const baseName = getBaseName(normalized)
14
- if (!nextRegistry[baseName]) {
15
- nextRegistry[baseName] = sourceMapRecord
16
- }
19
+
20
+ if (!nextRegistry[baseName]) nextRegistry[baseName] = sourceMapRecord
17
21
 
18
22
  return nextRegistry
19
23
  }
@@ -21,9 +25,8 @@ export const registerSourceMap = (registry: SourceMapRegistry, key: string, sour
21
25
  export const listSourceMapRecords = (sourceMaps: SourceMapRegistry): SourceMapRecord[] => {
22
26
  const map = new Map<string, SourceMapRecord>()
23
27
  Object.values(sourceMaps).forEach((record) => {
24
- if (!map.has(record.mapFileName)) {
25
- map.set(record.mapFileName, record)
26
- }
28
+ if (map.has(record.mapFileName)) return
29
+ map.set(record.mapFileName, record)
27
30
  })
28
31
  return Array.from(map.values())
29
32
  }
@@ -30,6 +30,7 @@ export interface ResolvedSourceMeta {
30
30
  export interface ChainMapSlot {
31
31
  id: string
32
32
  mapFileName: string
33
+ mapFilePath?: string
33
34
  sourceMapRecord?: SourceMapRecord
34
35
  error?: string
35
36
  }
@@ -0,0 +1,33 @@
1
+ import {normalizePath, stripMapExt} from "../base/path";
2
+ import {registerSourceMap} from "../base/registry";
3
+ import type {ChainMapSlot, SourceMapRegistry} from "../base/types";
4
+
5
+ export const buildRegistryFromChainSlots = (slots: ChainMapSlot[]) => {
6
+ let registry: SourceMapRegistry = {}
7
+ const orderedMapFileNames: string[] = []
8
+
9
+ slots.forEach((slot) => {
10
+ const {sourceMapRecord: record} = slot
11
+ if (!record) return
12
+
13
+ orderedMapFileNames.push(record.mapFileName)
14
+ registry = registerSourceMap({registry, key: record.mapFileName, sourceMapRecord: record})
15
+ registry = registerSourceMap({registry, key: stripMapExt(record.mapFileName), sourceMapRecord: record})
16
+
17
+ if (record.rawSourceMap.file) {
18
+ registry = registerSourceMap({registry, key: record.rawSourceMap.file, sourceMapRecord: record})
19
+ }
20
+ })
21
+
22
+ return {registry, orderedMapFileNames}
23
+ }
24
+
25
+ export const getAutoEntryFileNameFromSlots = (slots: ChainMapSlot[]) => {
26
+ const firstValidSlot = slots.find((slot) => slot.sourceMapRecord)
27
+ if (!firstValidSlot?.sourceMapRecord) return ""
28
+
29
+ const mappedFile = firstValidSlot.sourceMapRecord.rawSourceMap.file
30
+ if (mappedFile) return normalizePath(mappedFile)
31
+
32
+ return stripMapExt(firstValidSlot.sourceMapRecord.mapFileName)
33
+ }
@@ -9,6 +9,11 @@ interface SourceCodeMatch {
9
9
  location: TraceLocation
10
10
  }
11
11
 
12
+ interface ResolveSourceCodeOptions {
13
+ onlyLastMap?: boolean
14
+ lastMapFileName?: string
15
+ }
16
+
12
17
  const findSourceCodeMatch = (hop: TraceHop, sourceMapRecord: SourceMapRecord): SourceCodeMatch | null => {
13
18
  if (!hop.output) return null
14
19
 
@@ -42,14 +47,22 @@ const findSourceCodeMatch = (hop: TraceHop, sourceMapRecord: SourceMapRecord): S
42
47
  }
43
48
  }
44
49
 
45
- export const resolveSourceCodeFromTrace = (traceHops: TraceHop[], sourceMaps: SourceMapRegistry) => {
50
+ export const resolveSourceCodeFromTrace = (
51
+ traceHops: TraceHop[],
52
+ sourceMaps: SourceMapRegistry,
53
+ options: ResolveSourceCodeOptions = {},
54
+ ) => {
46
55
  const sourceMapRecords = listSourceMapRecords(sourceMaps)
56
+ const sourceMapRecordByName = new Map(sourceMapRecords.map((record) => [record.mapFileName, record] as const))
57
+ const {onlyLastMap = false, lastMapFileName} = options
47
58
 
48
59
  for (let index = traceHops.length - 1; index >= 0; index--) {
49
60
  const hop = traceHops[index]
50
61
  if (!hop.output || !hop.mapFileName) continue
62
+ // 仅允许链路最后一个 map 返回 sourcesContent,控制传输体积和性能开销。
63
+ if (onlyLastMap && lastMapFileName && hop.mapFileName !== lastMapFileName) continue
51
64
 
52
- const sourceMapRecord = sourceMapRecords.find((item) => item.mapFileName === hop.mapFileName)
65
+ const sourceMapRecord = sourceMapRecordByName.get(hop.mapFileName)
53
66
  if (!sourceMapRecord) continue
54
67
 
55
68
  const match = findSourceCodeMatch(hop, sourceMapRecord)
@@ -0,0 +1,179 @@
1
+ import {SourceMapConsumer, type RawSourceMap} from "source-map-js";
2
+ import {MAX_TRACE_DEPTH} from "../base/constants";
3
+ import {getSourceMapByFileName, getSourceMapByMapFileName} from "../base/registry";
4
+ import type {ResolveTraceOptions, SourceMapRecord, SourceMapRegistry, TraceHop, TraceLocation} from "../base/types";
5
+
6
+ interface ResolvedTraceHop extends TraceHop {
7
+ output: TraceLocation
8
+ }
9
+
10
+ const toLocationCursor = (location: TraceLocation) => {
11
+ return `${location.fileName}:${location.lineNumber}:${location.columnNumber}`
12
+ }
13
+
14
+ const isSameLocation = (left: TraceLocation, right: TraceLocation) => {
15
+ return (
16
+ left.fileName === right.fileName
17
+ && left.lineNumber === right.lineNumber
18
+ && left.columnNumber === right.columnNumber
19
+ )
20
+ }
21
+
22
+ const resolveOriginalLocation = (
23
+ rawSourceMap: RawSourceMap,
24
+ position: { line: number; column: number },
25
+ ): TraceLocation | null => {
26
+ const consumer = new SourceMapConsumer(rawSourceMap)
27
+
28
+ try {
29
+ // 回退策略:
30
+ // 1) 优先按目标列号查询;
31
+ // 2) 再回退到列号 0 的行级查询;
32
+ // 同时组合 GLB/LUB 两种 bias,提高稀疏映射和压缩代码场景下的命中率。
33
+ const attempts = [
34
+ {line: position.line, column: position.column, bias: SourceMapConsumer.GREATEST_LOWER_BOUND},
35
+ {line: position.line, column: 0, bias: SourceMapConsumer.GREATEST_LOWER_BOUND},
36
+ {line: position.line, column: position.column, bias: SourceMapConsumer.LEAST_UPPER_BOUND},
37
+ {line: position.line, column: 0, bias: SourceMapConsumer.LEAST_UPPER_BOUND},
38
+ ]
39
+
40
+ for (const attempt of attempts) {
41
+ const found = consumer.originalPositionFor(attempt)
42
+ if (found.source && found.line != null && found.column != null) {
43
+ return {
44
+ fileName: found.source,
45
+ lineNumber: found.line,
46
+ columnNumber: found.column,
47
+ }
48
+ }
49
+ }
50
+ } finally {
51
+ const maybeDestroy = (consumer as { destroy?: () => void }).destroy
52
+ maybeDestroy?.()
53
+ }
54
+
55
+ return null
56
+ }
57
+
58
+ const resolveHopFromRecord = (sourceMapRecord: SourceMapRecord, input: TraceLocation): ResolvedTraceHop | null => {
59
+ try {
60
+ const {lineNumber: line, columnNumber: column} = input
61
+ const output = resolveOriginalLocation(sourceMapRecord.rawSourceMap, {line, column})
62
+ if (!output) return null
63
+ return {input, output, mapFileName: sourceMapRecord.mapFileName}
64
+ } catch {
65
+ return null
66
+ }
67
+ }
68
+
69
+ interface AppendUnresolvedHopInput {
70
+ traceHops: TraceHop[]
71
+ input: TraceLocation
72
+ mapFileName?: string
73
+ }
74
+
75
+ const appendUnresolvedHop = (input: AppendUnresolvedHopInput) => {
76
+ const {traceHops, input: location, mapFileName} = input
77
+ traceHops.push({input: location, mapFileName})
78
+ }
79
+
80
+ interface ResolveTraceWithOrderedMapsInput {
81
+ entry: TraceLocation
82
+ sourceMaps: SourceMapRegistry
83
+ orderedMapFileNames: string[]
84
+ maxDepth: number
85
+ }
86
+
87
+ const resolveTraceWithOrderedMaps = (input: ResolveTraceWithOrderedMapsInput) => {
88
+ const {entry, sourceMaps, orderedMapFileNames, maxDepth} = input
89
+ const traceHops: TraceHop[] = []
90
+ const visited = new Set<string>()
91
+ let current = entry
92
+
93
+ for (let depth = 0; depth < orderedMapFileNames.length && depth < maxDepth; depth++) {
94
+ const mapFileName = orderedMapFileNames[depth]
95
+ const cursor = toLocationCursor(current)
96
+
97
+ if (visited.has(cursor)) {
98
+ appendUnresolvedHop({traceHops, input: current, mapFileName})
99
+ continue
100
+ }
101
+ visited.add(cursor)
102
+
103
+ const sourceMapRecord = getSourceMapByMapFileName(sourceMaps, mapFileName)
104
+ if (!sourceMapRecord) {
105
+ appendUnresolvedHop({traceHops, input: current, mapFileName})
106
+ continue
107
+ }
108
+
109
+ const hop = resolveHopFromRecord(sourceMapRecord, current)
110
+ if (!hop) {
111
+ appendUnresolvedHop({traceHops, input: current, mapFileName: sourceMapRecord.mapFileName})
112
+ continue
113
+ }
114
+
115
+ traceHops.push(hop)
116
+ current = hop.output
117
+ }
118
+
119
+ return traceHops
120
+ }
121
+
122
+ interface ResolveTraceByFileLookupInput {
123
+ entry: TraceLocation
124
+ sourceMaps: SourceMapRegistry
125
+ maxDepth: number
126
+ }
127
+
128
+ const resolveTraceByFileLookup = (input: ResolveTraceByFileLookupInput) => {
129
+ const {entry, sourceMaps, maxDepth} = input
130
+ const traceHops: TraceHop[] = []
131
+ const visited = new Set<string>()
132
+ let current = entry
133
+
134
+ for (let depth = 0; depth < maxDepth; depth++) {
135
+ const cursor = toLocationCursor(current)
136
+
137
+ if (visited.has(cursor)) {
138
+ appendUnresolvedHop({traceHops, input: current})
139
+ break
140
+ }
141
+ visited.add(cursor)
142
+
143
+ const sourceMapRecord = getSourceMapByFileName(sourceMaps, current.fileName)
144
+ if (!sourceMapRecord) {
145
+ if (traceHops.length === 0) appendUnresolvedHop({traceHops, input: current})
146
+ break
147
+ }
148
+
149
+ const hop = resolveHopFromRecord(sourceMapRecord, current)
150
+ if (!hop) {
151
+ appendUnresolvedHop({traceHops, input: current, mapFileName: sourceMapRecord.mapFileName})
152
+ break
153
+ }
154
+
155
+ traceHops.push(hop)
156
+
157
+ if (isSameLocation(hop.output, current)) break
158
+
159
+ current = hop.output
160
+ }
161
+
162
+ return traceHops
163
+ }
164
+
165
+ interface ResolveTraceBySourceMapsInput {
166
+ entry: TraceLocation
167
+ sourceMaps: SourceMapRegistry
168
+ orderedMapFileNames?: string[]
169
+ options?: ResolveTraceOptions
170
+ }
171
+
172
+ export const resolveTraceBySourceMaps = async (input: ResolveTraceBySourceMapsInput): Promise<TraceHop[]> => {
173
+ const {entry, sourceMaps, orderedMapFileNames = [], options = {}} = input
174
+ const maxDepth = options.maxDepth ?? MAX_TRACE_DEPTH
175
+
176
+ if (orderedMapFileNames.length > 0) return resolveTraceWithOrderedMaps({entry, sourceMaps, orderedMapFileNames, maxDepth})
177
+
178
+ return resolveTraceByFileLookup({entry, sourceMaps, maxDepth})
179
+ }
@@ -0,0 +1,57 @@
1
+ import {DEFAULT_CONTEXT_LINE_RADIUS} from "../base/constants";
2
+ import type {TraceLocation} from "../base/types";
3
+
4
+ export const toDisplayLocation = (location: TraceLocation): TraceLocation => ({...location})
5
+
6
+ const MAX_EXCERPT_CHARS = 12000
7
+ const COLUMN_WINDOW_CHARS = 4000
8
+
9
+ const truncateLineByColumn = (line: string, targetColumn = 0) => {
10
+ if (line.length <= MAX_EXCERPT_CHARS) return line
11
+ const halfWindow = Math.floor(COLUMN_WINDOW_CHARS / 2)
12
+ const start = Math.max(0, targetColumn - halfWindow)
13
+ const end = Math.min(line.length, start + COLUMN_WINDOW_CHARS)
14
+ const head = start > 0 ? "…[truncated]…" : ""
15
+ const tail = end < line.length ? "…[truncated]…" : ""
16
+ return `${head}${line.slice(start, end)}${tail}`
17
+ }
18
+
19
+ const truncateExcerpt = (code: string) => {
20
+ if (code.length <= MAX_EXCERPT_CHARS) return code
21
+ return `${code.slice(0, MAX_EXCERPT_CHARS)}\n...[truncated ${code.length - MAX_EXCERPT_CHARS} chars]...`
22
+ }
23
+
24
+ interface CreateCodeExcerptInput {
25
+ fullCode: string
26
+ targetLine: number
27
+ radius?: number
28
+ targetColumn?: number
29
+ }
30
+
31
+ export const createCodeExcerpt = (input: CreateCodeExcerptInput) => {
32
+ const {
33
+ fullCode,
34
+ targetLine,
35
+ radius = DEFAULT_CONTEXT_LINE_RADIUS,
36
+ targetColumn = 0,
37
+ } = input
38
+ const lines = fullCode.split(/\r?\n/)
39
+ if (lines.length === 0) {
40
+ return {code: truncateExcerpt(fullCode), highlightLine: targetLine}
41
+ }
42
+
43
+ const safeTarget = Math.min(Math.max(targetLine, 1), lines.length)
44
+
45
+ // 压缩后的单行源码可能非常大,仅截取目标列附近窗口。
46
+ if (lines.length === 1) {
47
+ return {code: truncateLineByColumn(lines[0], targetColumn), highlightLine: 1}
48
+ }
49
+
50
+ const start = Math.max(1, safeTarget - radius)
51
+ const end = Math.min(lines.length, safeTarget + radius)
52
+
53
+ return {
54
+ code: truncateExcerpt(lines.slice(start - 1, end).join("\n")),
55
+ highlightLine: safeTarget - start + 1,
56
+ }
57
+ }