@forge-kit/plugin-source-map-prase 0.0.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.
@@ -0,0 +1,128 @@
1
+ .trace-chain-root {
2
+ flex: 1;
3
+ min-height: 0;
4
+ height: 100%;
5
+ overflow: auto;
6
+ padding: 4px;
7
+ -webkit-app-region: no-drag;
8
+
9
+ .trace-chain-item {
10
+ margin-bottom: 10px;
11
+
12
+ &[data-dragging="true"] {
13
+ opacity: 0.85;
14
+ }
15
+ }
16
+
17
+ .trace-chain-empty {
18
+ min-height: 180px;
19
+ border: 1px dashed var(--gray-6);
20
+ border-radius: var(--radius-3);
21
+ color: var(--gray-11);
22
+ font-size: 13px;
23
+ }
24
+
25
+ .trace-chain-node {
26
+ padding: 10px;
27
+ border-radius: var(--radius-3);
28
+ border: 1px solid var(--gray-6);
29
+ background:
30
+ linear-gradient(160deg, color-mix(in srgb, var(--gray-3) 90%, transparent), color-mix(in srgb, var(--gray-2) 90%, transparent));
31
+ }
32
+
33
+ .trace-chain-drag-handle {
34
+ min-width: 46px;
35
+ height: 24px;
36
+ border-radius: 999px;
37
+ border: 1px solid var(--gray-7);
38
+ background: var(--gray-4);
39
+ color: var(--gray-12);
40
+ font-size: 12px;
41
+ font-weight: 600;
42
+ user-select: none;
43
+ cursor: grab;
44
+
45
+ &:active {
46
+ cursor: grabbing;
47
+ }
48
+ }
49
+
50
+ .trace-chain-content {
51
+ overflow: hidden;
52
+ width: 100%;
53
+ }
54
+
55
+ .trace-chain-main-title {
56
+ display: block;
57
+ font-size: 13px;
58
+ font-weight: 700;
59
+ color: var(--gray-12);
60
+ line-height: 1.35;
61
+ word-break: break-all;
62
+ user-select: text;
63
+ cursor: text;
64
+ }
65
+
66
+ .trace-chain-header {
67
+ width: 100%;
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
+
77
+ .trace-chain-row[data-row-type="output"] {
78
+ margin-top: 2px;
79
+ padding-top: 8px;
80
+ border-top: 1px dashed color-mix(in srgb, var(--gray-6) 78%, transparent);
81
+ }
82
+
83
+ .trace-chain-label {
84
+ min-width: 30px;
85
+ font-size: 12px;
86
+ color: var(--gray-10);
87
+ user-select: none;
88
+ margin-top: 1px;
89
+ }
90
+
91
+ .trace-chain-detail {
92
+ min-width: 0;
93
+ }
94
+
95
+ .trace-chain-output-head {
96
+ }
97
+
98
+ .trace-chain-file {
99
+ font-size: 13px;
100
+ font-weight: 600;
101
+ color: var(--gray-12);
102
+ white-space: normal;
103
+ overflow-wrap: anywhere;
104
+ word-break: break-all;
105
+ user-select: text;
106
+ cursor: text;
107
+ }
108
+
109
+ .trace-chain-position {
110
+ font-size: 12px;
111
+ color: var(--gray-11);
112
+ white-space: normal;
113
+ overflow-wrap: anywhere;
114
+ user-select: text;
115
+ cursor: text;
116
+ }
117
+
118
+ .trace-chain-error {
119
+ font-size: 12px;
120
+ color: #ff7b7b;
121
+ }
122
+
123
+ .trace-chain-pending {
124
+ font-size: 12px;
125
+ color: var(--gray-10);
126
+ }
127
+
128
+ }
@@ -0,0 +1,151 @@
1
+ import * as React from "react";
2
+ import {Box, Button, Flex, Text} from "@forge-kit/component";
3
+ import {DndContext, PointerSensor, closestCenter, useSensor, useSensors, type DragEndEvent} from "@dnd-kit/core";
4
+ import {SortableContext, arrayMove, useSortable, verticalListSortingStrategy} from "@dnd-kit/sortable";
5
+ import {CSS} from "@dnd-kit/utilities";
6
+ import "./index.less";
7
+
8
+ interface TraceChainNode {
9
+ fileName: string
10
+ lineNumber: number
11
+ columnNumber: number
12
+ }
13
+
14
+ interface TraceChainHop {
15
+ input: TraceChainNode
16
+ output?: TraceChainNode
17
+ mapFileName?: string
18
+ }
19
+
20
+
21
+ interface TraceChainHopRowProps {
22
+ type: "input" | "output"
23
+ node?: TraceChainNode
24
+ }
25
+
26
+ const TraceChainHopRow: React.FC<TraceChainHopRowProps> = (props) => {
27
+ const {type, node} = props
28
+ const isInput = type === "input"
29
+ const label = isInput ? "输入" : "输出"
30
+ const fileName = node?.fileName || (isInput ? "-" : "未命中映射")
31
+ const position = node
32
+ ? `line ${node.lineNumber}, column ${node.columnNumber}`
33
+ : "line -, column -"
34
+
35
+ return (
36
+ <Box className="trace-chain-row" data-row-type={type}>
37
+ <Box className="trace-chain-label">{label}</Box>
38
+ <Flex className="trace-chain-detail" direction="column" gap="1">
39
+ <Flex className="trace-chain-output-head" align="start" justify="start" gap="2">
40
+ <Box className="trace-chain-file">{fileName}</Box>
41
+ </Flex>
42
+ <Box className="trace-chain-position">{position}</Box>
43
+ </Flex>
44
+ </Box>
45
+ )
46
+ }
47
+
48
+ interface SortableTraceChainItemProps {
49
+ slot: TraceChainSlot
50
+ hop?: TraceChainHop
51
+ index: number
52
+ onDelete?: (slotId: string) => void
53
+ }
54
+
55
+ const SortableTraceChainItem: React.FC<SortableTraceChainItemProps> = (props) => {
56
+ const {slot, hop, index, onDelete} = props
57
+ const {id, error} = slot
58
+
59
+ const {
60
+ attributes,
61
+ listeners,
62
+ setNodeRef,
63
+ transform,
64
+ transition,
65
+ isDragging,
66
+ } = useSortable({id})
67
+
68
+ const style: React.CSSProperties = {
69
+ transform: CSS.Transform.toString(transform),
70
+ transition,
71
+ }
72
+
73
+ return (
74
+ <Box ref={setNodeRef} style={style} className="trace-chain-item" data-dragging={isDragging ? "true" : "false"}>
75
+ <Flex className="trace-chain-node" gap="2">
76
+ <Flex className="trace-chain-drag-handle" align="center" justify="center" {...attributes} {...listeners}>#{index + 1}</Flex>
77
+ <Flex className="trace-chain-content" direction="column" gap="2">
78
+ <Flex className="trace-chain-header" justify="between" align="start" gap="2">
79
+ <Text className="trace-chain-main-title">{slot.mapFileName || "(未命名 map 文件)"}</Text>
80
+ <Button size="1" variant="outline" onClick={() => onDelete?.(id)}>删除</Button>
81
+ </Flex>
82
+ {error && <Text className="trace-chain-error">{error}</Text>}
83
+ {hop && (
84
+ <React.Fragment>
85
+ <TraceChainHopRow type="input" node={hop.input}/>
86
+ <TraceChainHopRow type="output" node={hop.output}/>
87
+ </React.Fragment>
88
+ )}
89
+ {!hop && <Text className="trace-chain-pending">等待解析结果</Text>}
90
+ </Flex>
91
+ </Flex>
92
+ </Box>
93
+ )
94
+ }
95
+
96
+
97
+ interface TraceChainSlot {
98
+ id: string
99
+ mapFileName: string
100
+ error?: string
101
+ }
102
+
103
+ interface TraceChainProps {
104
+ slots?: TraceChainSlot[]
105
+ data?: TraceChainHop[]
106
+ onReorder?: (nextIds: string[]) => void
107
+ onDelete?: (slotId: string) => void
108
+ className?: string
109
+ }
110
+
111
+
112
+ export const TraceChain: React.FC<TraceChainProps> = (props) => {
113
+ const {slots = [], data = [], onReorder, onDelete, className} = props
114
+ const sensors = useSensors(useSensor(PointerSensor, {activationConstraint: {distance: 6}}))
115
+
116
+ if (slots.length === 0) {
117
+ return (
118
+ <Box className={className || "trace-chain-root"}>
119
+ <Flex className="trace-chain-empty" align="center" justify="center">暂无解析链路,上传 map 并点击“解析链路”。</Flex>
120
+ </Box>
121
+ )
122
+ }
123
+
124
+ const handleDragEnd = (event: DragEndEvent) => {
125
+ const {active, over} = event
126
+ if (!over || active.id === over.id) return
127
+ const oldIndex = slots.findIndex((item) => item.id === String(active.id))
128
+ const newIndex = slots.findIndex((item) => item.id === String(over.id))
129
+ if (oldIndex < 0 || newIndex < 0) return
130
+ const nextSlots = arrayMove(slots, oldIndex, newIndex)
131
+ onReorder?.(nextSlots.map((item) => item.id))
132
+ }
133
+
134
+ return (
135
+ <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
136
+ <SortableContext items={slots.map((item) => item.id)} strategy={verticalListSortingStrategy}>
137
+ <Box className={className || "trace-chain-root"}>
138
+ {slots.map((slot, index) => (
139
+ <SortableTraceChainItem
140
+ key={slot.id}
141
+ slot={slot}
142
+ hop={data[index]}
143
+ index={index}
144
+ onDelete={onDelete}
145
+ />
146
+ ))}
147
+ </Box>
148
+ </SortableContext>
149
+ </DndContext>
150
+ )
151
+ }
package/src/main.tsx ADDED
@@ -0,0 +1,29 @@
1
+ import {StrictMode} from 'react'
2
+ import ReactDOM from "react-dom/client";
3
+ import {definePlugin} from '@forge-kit/types'
4
+ import {App} from '@/App.tsx'
5
+
6
+ let rootInstance: ReturnType<typeof ReactDOM.createRoot> | null = null;
7
+
8
+ const plugin = definePlugin({
9
+ bundleId: "com.forge-kit.plugin.source-map-prase",
10
+ name: 'Source Map 解析工具',
11
+ icon: `${import.meta.env.BASE_URL}icon.svg`,
12
+ description: "Source Map 解析工具",
13
+ mount: (container) => {
14
+ rootInstance = ReactDOM.createRoot(container);
15
+ rootInstance.render(
16
+ <StrictMode>
17
+ <App/>
18
+ </StrictMode>,
19
+ )
20
+ },
21
+ unmount: () => {
22
+ if (!rootInstance) return
23
+ rootInstance.unmount();
24
+ rootInstance = null;
25
+ }
26
+ });
27
+ if (import.meta.env.DEV) plugin.mount(document.getElementById('root')!)
28
+
29
+ export default plugin
@@ -0,0 +1 @@
1
+ export const noop = ()=>{}
@@ -0,0 +1,2 @@
1
+ export const MAX_TRACE_DEPTH = 20
2
+ export const DEFAULT_CONTEXT_LINE_RADIUS = 3
@@ -0,0 +1,17 @@
1
+ export const normalizePath = (input: string) => input.replace(/\\/g, "/").trim()
2
+
3
+ export const getBaseName = (input: string) => {
4
+ const normalized = normalizePath(input)
5
+ const tokens = normalized.split("/")
6
+ return tokens[tokens.length - 1] || normalized
7
+ }
8
+
9
+ export const stripMapExt = (input: string) => input.endsWith(".map") ? input.slice(0, -4) : input
10
+
11
+ export const joinPath = (...parts: string[]) => {
12
+ return parts
13
+ .map((part) => normalizePath(part))
14
+ .filter(Boolean)
15
+ .join("/")
16
+ .replace(/\/{2,}/g, "/")
17
+ }
@@ -0,0 +1,38 @@
1
+ import type {SourceMapRecord, SourceMapRegistry} from "./types";
2
+ import {getBaseName, normalizePath} from "./path";
3
+
4
+ export const registerSourceMap = (registry: SourceMapRegistry, key: string, sourceMapRecord: SourceMapRecord) => {
5
+ const normalized = normalizePath(key)
6
+ if (!normalized) return registry
7
+
8
+ const nextRegistry: SourceMapRegistry = {
9
+ ...registry,
10
+ [normalized]: sourceMapRecord,
11
+ }
12
+
13
+ const baseName = getBaseName(normalized)
14
+ if (!nextRegistry[baseName]) {
15
+ nextRegistry[baseName] = sourceMapRecord
16
+ }
17
+
18
+ return nextRegistry
19
+ }
20
+
21
+ export const listSourceMapRecords = (sourceMaps: SourceMapRegistry): SourceMapRecord[] => {
22
+ const map = new Map<string, SourceMapRecord>()
23
+ Object.values(sourceMaps).forEach((record) => {
24
+ if (!map.has(record.mapFileName)) {
25
+ map.set(record.mapFileName, record)
26
+ }
27
+ })
28
+ return Array.from(map.values())
29
+ }
30
+
31
+ export const getSourceMapByFileName = (sourceMaps: SourceMapRegistry, fileName: string) => {
32
+ const normalized = normalizePath(fileName)
33
+ return sourceMaps[normalized] ?? sourceMaps[getBaseName(normalized)]
34
+ }
35
+
36
+ export const getSourceMapByMapFileName = (sourceMaps: SourceMapRegistry, mapFileName: string) => {
37
+ return listSourceMapRecords(sourceMaps).find((item) => item.mapFileName === mapFileName)
38
+ }
@@ -0,0 +1,37 @@
1
+ import type {RawSourceMap} from "source-map-js";
2
+
3
+ export interface TraceLocation {
4
+ fileName: string
5
+ lineNumber: number
6
+ columnNumber: number
7
+ }
8
+
9
+ export interface ResolveTraceOptions {
10
+ maxDepth?: number
11
+ }
12
+
13
+ export interface TraceHop {
14
+ input: TraceLocation
15
+ output?: TraceLocation
16
+ mapFileName?: string
17
+ }
18
+
19
+ export interface SourceMapRecord {
20
+ rawSourceMap: RawSourceMap
21
+ mapFileName: string
22
+ }
23
+
24
+ export interface ResolvedSourceMeta {
25
+ sourceFile: string
26
+ lineNumber: number
27
+ columnNumber: number
28
+ }
29
+
30
+ export interface ChainMapSlot {
31
+ id: string
32
+ mapFileName: string
33
+ sourceMapRecord?: SourceMapRecord
34
+ error?: string
35
+ }
36
+
37
+ export type SourceMapRegistry = Record<string, SourceMapRecord>
@@ -0,0 +1,59 @@
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 createChainSlotFromFile = async (file: File): Promise<ChainMapSlot> => {
6
+ const slotId = `${file.name}-${Math.random().toString(36).slice(2, 8)}`
7
+
8
+ try {
9
+ const fileText = await file.text()
10
+ const rawSourceMap = JSON.parse(fileText)
11
+
12
+ if (!rawSourceMap || typeof rawSourceMap !== "object" || typeof rawSourceMap.mappings !== "string") {
13
+ throw new Error("invalid source-map json")
14
+ }
15
+
16
+ return {
17
+ id: slotId,
18
+ mapFileName: file.name,
19
+ sourceMapRecord: {rawSourceMap, mapFileName: file.name},
20
+ }
21
+ } catch (error) {
22
+ const message = error instanceof Error ? error.message : "unknown error"
23
+ return {
24
+ id: slotId,
25
+ mapFileName: file.name,
26
+ error: `${file.name}: ${message}`,
27
+ }
28
+ }
29
+ }
30
+
31
+ export const buildRegistryFromChainSlots = (slots: ChainMapSlot[]) => {
32
+ let registry: SourceMapRegistry = {}
33
+ const orderedMapFileNames: string[] = []
34
+
35
+ slots.forEach((slot) => {
36
+ const {sourceMapRecord: record} = slot
37
+ if (!record) return
38
+
39
+ orderedMapFileNames.push(record.mapFileName)
40
+ registry = registerSourceMap(registry, record.mapFileName, record)
41
+ registry = registerSourceMap(registry, stripMapExt(record.mapFileName), record)
42
+
43
+ if (record.rawSourceMap.file) {
44
+ registry = registerSourceMap(registry, record.rawSourceMap.file, record)
45
+ }
46
+ })
47
+
48
+ return {registry, orderedMapFileNames}
49
+ }
50
+
51
+ export const getAutoEntryFileNameFromSlots = (slots: ChainMapSlot[]) => {
52
+ const firstValidSlot = slots.find((slot) => slot.sourceMapRecord)
53
+ if (!firstValidSlot?.sourceMapRecord) return ""
54
+
55
+ const mappedFile = firstValidSlot.sourceMapRecord.rawSourceMap.file
56
+ if (mappedFile) return normalizePath(mappedFile)
57
+
58
+ return stripMapExt(firstValidSlot.sourceMapRecord.mapFileName)
59
+ }
@@ -0,0 +1,75 @@
1
+ import {getBaseName, joinPath, normalizePath} from "../base/path";
2
+ import {listSourceMapRecords} from "../base/registry";
3
+ import {toDisplayLocation} from "./view-model";
4
+ import type {SourceMapRecord, SourceMapRegistry, TraceHop, TraceLocation} from "../base/types";
5
+
6
+ interface SourceCodeMatch {
7
+ code: string
8
+ sourceFile: string
9
+ location: TraceLocation
10
+ }
11
+
12
+ const findSourceCodeMatch = (hop: TraceHop, sourceMapRecord: SourceMapRecord): SourceCodeMatch | null => {
13
+ if (!hop.output) return null
14
+
15
+ const {sources = [], sourcesContent = [], sourceRoot = ""} = sourceMapRecord.rawSourceMap
16
+ const outputFile = normalizePath(hop.output.fileName)
17
+ const outputBaseName = getBaseName(outputFile)
18
+
19
+ const sourceIndex = sources.findIndex((source) => {
20
+ const normalizedSource = normalizePath(source)
21
+ const sourceWithRoot = joinPath(sourceRoot, normalizedSource)
22
+
23
+ return (
24
+ normalizedSource === outputFile
25
+ || sourceWithRoot === outputFile
26
+ || outputFile.endsWith(`/${normalizedSource}`)
27
+ || outputFile.endsWith(`/${sourceWithRoot}`)
28
+ || getBaseName(normalizedSource) === outputBaseName
29
+ || getBaseName(sourceWithRoot) === outputBaseName
30
+ )
31
+ })
32
+
33
+ if (sourceIndex < 0) return null
34
+
35
+ const code = sourcesContent[sourceIndex]
36
+ if (!code) return null
37
+
38
+ return {
39
+ code,
40
+ sourceFile: sources[sourceIndex],
41
+ location: toDisplayLocation(hop.output),
42
+ }
43
+ }
44
+
45
+ export const resolveSourceCodeFromTrace = (traceHops: TraceHop[], sourceMaps: SourceMapRegistry) => {
46
+ const sourceMapRecords = listSourceMapRecords(sourceMaps)
47
+
48
+ for (let index = traceHops.length - 1; index >= 0; index--) {
49
+ const hop = traceHops[index]
50
+ if (!hop.output || !hop.mapFileName) continue
51
+
52
+ const sourceMapRecord = sourceMapRecords.find((item) => item.mapFileName === hop.mapFileName)
53
+ if (!sourceMapRecord) continue
54
+
55
+ const match = findSourceCodeMatch(hop, sourceMapRecord)
56
+ if (!match) continue
57
+
58
+ return {
59
+ code: match.code,
60
+ sourceFile: match.sourceFile,
61
+ lineNumber: match.location.lineNumber,
62
+ columnNumber: match.location.columnNumber,
63
+ }
64
+ }
65
+
66
+ return null
67
+ }
68
+
69
+ export const getLastResolvedOutput = (traceHops: TraceHop[]) => {
70
+ for (let index = traceHops.length - 1; index >= 0; index--) {
71
+ const output = traceHops[index].output
72
+ if (output) return output
73
+ }
74
+ return null
75
+ }