@forge-kit/plugin-source-map-prase 0.0.2 → 0.0.4
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/app.js +17 -19
- package/dist/node.js +2552 -0
- package/package.json +5 -3
- package/src/App.less +35 -26
- package/src/App.tsx +70 -23
- package/src/components/map-input-panel/index.less +71 -78
- package/src/components/map-input-panel/index.tsx +19 -20
- package/src/components/panel-card/index.less +16 -13
- package/src/components/panel-card/index.tsx +2 -2
- package/src/components/trace-chain/index.less +119 -120
- package/src/components/trace-chain/index.tsx +9 -7
- package/src/main.tsx +1 -1
- package/src/node/index.ts +6 -0
- package/src/{utils/source-map → node/trace/core}/base/registry.ts +14 -11
- package/src/{utils/source-map → node/trace/core}/base/types.ts +1 -0
- package/src/node/trace/core/domain/chain-slots.ts +33 -0
- package/src/{utils/source-map → node/trace/core}/domain/source-content.ts +15 -2
- package/src/node/trace/core/domain/trace-resolver.ts +179 -0
- package/src/node/trace/core/domain/view-model.ts +57 -0
- package/src/node/trace/resolve/context.ts +59 -0
- package/src/node/trace/resolve/index.ts +97 -0
- package/src/node/trace/resolve/input.ts +35 -0
- package/src/node/trace/resolve/snippet-limit.ts +35 -0
- package/src/node/trace/resolve/types.ts +37 -0
- package/src/node/trace/runner.ts +149 -0
- package/src/shared/trace-common.ts +104 -0
- package/src/shared/trace-contract.ts +29 -0
- package/src/types.ts +11 -0
- package/src/utils/trace-ui/index.ts +12 -0
- package/src/utils/trace-ui/state.ts +81 -0
- package/src/utils/source-map/domain/chain-slots.ts +0 -59
- package/src/utils/source-map/domain/trace-resolver.ts +0 -165
- package/src/utils/source-map/domain/view-model.ts +0 -20
- package/src/utils/source-map/facade/source-map-utils.ts +0 -212
- package/src/utils/source-map/index.ts +0 -18
- /package/src/{utils/source-map → node/trace/core}/base/constants.ts +0 -0
- /package/src/{utils/source-map → node/trace/core}/base/path.ts +0 -0
|
@@ -1,130 +1,129 @@
|
|
|
1
1
|
.trace-panel-card {
|
|
2
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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"
|
|
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=
|
|
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=
|
|
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=
|
|
140
|
+
<Box className="trace-chain-root">
|
|
140
141
|
{slots.map((slot, index) => {
|
|
141
|
-
return <SortableTraceChainItem key={slot.id} slot={slot} hop={data[index]}
|
|
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/
|
|
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/helper";
|
|
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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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 (
|
|
25
|
-
|
|
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
|
}
|
|
@@ -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 = (
|
|
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 =
|
|
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
|
+
}
|