@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
@@ -0,0 +1,59 @@
1
+ import {readFile} from "node:fs/promises";
2
+ import {basename} from "node:path";
3
+ import {homedir} from "node:os";
4
+ import type {ChainMapSlot} from "@/node/trace/core/base/types";
5
+ import {
6
+ DEFAULT_CONTEXT_LINE_RADIUS,
7
+ buildChainStateFromSlots,
8
+ validateResolveInput,
9
+ } from "../runner";
10
+ import type {ResolveTaskContext, ResolveTraceInput} from "./types";
11
+
12
+ const createChainSlotFromPath = async (filePath: string): Promise<ChainMapSlot> => {
13
+ const resolvedPath = filePath.startsWith("~/") ? `${homedir()}/${filePath.slice(2)}` : filePath
14
+ const mapFileName = basename(resolvedPath)
15
+ const slotId = `${mapFileName}-${Math.random().toString(36).slice(2, 8)}`
16
+
17
+ try {
18
+ const fileText = await readFile(resolvedPath, "utf8")
19
+ const rawSourceMap = JSON.parse(fileText)
20
+ if (!rawSourceMap || typeof rawSourceMap !== "object" || typeof rawSourceMap.mappings !== "string") {
21
+ throw new Error("invalid source-map json")
22
+ }
23
+ return {
24
+ id: slotId,
25
+ mapFileName,
26
+ mapFilePath: resolvedPath,
27
+ sourceMapRecord: {rawSourceMap, mapFileName},
28
+ }
29
+ } catch (error) {
30
+ const message = error instanceof Error ? error.message : "unknown error"
31
+ return {
32
+ id: slotId,
33
+ mapFileName,
34
+ mapFilePath: resolvedPath,
35
+ error: `${mapFileName}: ${message}`,
36
+ }
37
+ }
38
+ }
39
+
40
+ export const createResolveContext = async (parsed: ResolveTraceInput): Promise<ResolveTaskContext> => {
41
+ // 单次请求内统一读取并校验 map 文件,避免重复 IO。
42
+ const chainSlots = await Promise.all(parsed.mapFilePaths.map(createChainSlotFromPath))
43
+ const chainState = buildChainStateFromSlots(chainSlots)
44
+ const validated = validateResolveInput({
45
+ entryFileName: parsed.entryFileName,
46
+ entryLine: parsed.entryLine,
47
+ entryColumn: parsed.entryColumn,
48
+ contextLineRadius: parsed.contextLineRadius || String(DEFAULT_CONTEXT_LINE_RADIUS),
49
+ mapCount: chainState.sourceMapFileNames.length,
50
+ })
51
+ if (!validated.ok) throw new Error(validated.message)
52
+
53
+ return {
54
+ entry: validated.entry,
55
+ contextLineRadius: validated.contextLineRadius,
56
+ sourceMaps: chainState.sourceMaps,
57
+ sourceMapFileNames: chainState.sourceMapFileNames,
58
+ }
59
+ }
@@ -0,0 +1,97 @@
1
+ import {performance} from "node:perf_hooks";
2
+ import {
3
+ buildTraceMetaOutput,
4
+ buildTraceSnippetOutput,
5
+ resolveTraceContextTask,
6
+ } from "../runner";
7
+ import {createResolveContext} from "./context";
8
+ import {
9
+ createValidationMetaOutput,
10
+ isInputValidationMessage,
11
+ parseResolveInput,
12
+ toFallbackEntry,
13
+ } from "./input";
14
+ import {
15
+ assertSnippetPayloadWithinLimit,
16
+ estimateSnippetPayloadBytes,
17
+ parseMaxSnippetPayloadBytes,
18
+ } from "./snippet-limit";
19
+ import type {ProfileResolveTraceOutput, TraceProfilePhase} from "./types";
20
+
21
+ const pushProfilePhase = (phases: TraceProfilePhase[], name: string, startedAt: number) => {
22
+ phases.push({name, durationMs: Number((performance.now() - startedAt).toFixed(2))})
23
+ }
24
+
25
+ export const resolveTrace = async (payloadText: string) => {
26
+ const parsed = parseResolveInput(payloadText)
27
+ try {
28
+ const resolveContext = await createResolveContext(parsed)
29
+ const resolved = await resolveTraceContextTask(resolveContext)
30
+ return buildTraceMetaOutput(resolved)
31
+ } catch (error) {
32
+ const message = error instanceof Error ? error.message : "unknown error"
33
+ if (isInputValidationMessage(message)) return createValidationMetaOutput(message)
34
+ return {
35
+ traceData: [{input: toFallbackEntry(parsed)}],
36
+ resolvedSourceMeta: null,
37
+ canFetchSourceSnippet: false,
38
+ message: `source-map trace failed: ${message}`,
39
+ }
40
+ }
41
+ }
42
+
43
+ export const getSourceSnippet = async (payloadText: string) => {
44
+ // 两阶段接口在无进程缓存模式下,第二阶段使用同一入参重新计算。
45
+ const parsed = parseResolveInput(payloadText)
46
+ const resolveContext = await createResolveContext(parsed)
47
+ const resolved = await resolveTraceContextTask(resolveContext)
48
+ const snippetOutput = buildTraceSnippetOutput({
49
+ contextLineRadius: resolveContext.contextLineRadius,
50
+ resolved,
51
+ })
52
+ assertSnippetPayloadWithinLimit(snippetOutput, parsed)
53
+ return snippetOutput
54
+ }
55
+
56
+ export const profileResolveTracePhases = async (payloadText: string): Promise<ProfileResolveTraceOutput> => {
57
+ const phases: TraceProfilePhase[] = []
58
+ const totalStart = performance.now()
59
+
60
+ const parseStart = performance.now()
61
+ const parsed = parseResolveInput(payloadText)
62
+ pushProfilePhase(phases, "parseResolveInput", parseStart)
63
+
64
+ const createContextStart = performance.now()
65
+ const resolveContext = await createResolveContext(parsed)
66
+ pushProfilePhase(phases, "createResolveContext", createContextStart)
67
+
68
+ const resolveTraceStart = performance.now()
69
+ const resolved = await resolveTraceContextTask(resolveContext)
70
+ pushProfilePhase(phases, "resolveTraceContextTask", resolveTraceStart)
71
+
72
+ const buildMetaStart = performance.now()
73
+ const metaOutput = buildTraceMetaOutput(resolved)
74
+ pushProfilePhase(phases, "buildTraceMetaOutput", buildMetaStart)
75
+
76
+ const buildSnippetStart = performance.now()
77
+ const snippetOutput = buildTraceSnippetOutput({
78
+ contextLineRadius: resolveContext.contextLineRadius,
79
+ resolved,
80
+ })
81
+ pushProfilePhase(phases, "buildTraceSnippetOutput", buildSnippetStart)
82
+
83
+ const estimateStart = performance.now()
84
+ const maxSnippetPayloadBytes = parseMaxSnippetPayloadBytes(parsed)
85
+ const snippetPayloadBytes = estimateSnippetPayloadBytes(snippetOutput)
86
+ pushProfilePhase(phases, "estimateSnippetPayloadBytes", estimateStart)
87
+
88
+ return {
89
+ phases,
90
+ totalMs: Number((performance.now() - totalStart).toFixed(2)),
91
+ traceHopCount: metaOutput.traceData.length,
92
+ resolvedSourceMetaExists: Boolean(metaOutput.resolvedSourceMeta),
93
+ snippetPayloadBytes,
94
+ maxSnippetPayloadBytes,
95
+ snippetPayloadWouldExceedLimit: snippetPayloadBytes > maxSnippetPayloadBytes,
96
+ }
97
+ }
@@ -0,0 +1,35 @@
1
+ import {DEFAULT_CONTEXT_LINE_RADIUS, type TraceMetaOutputState} from "../runner";
2
+ import type {ResolveTraceInput} from "./types";
3
+
4
+ const INPUT_VALIDATION_PATTERNS = ["请输入", "必须是", "未加载"]
5
+
6
+ export const parseResolveInput = (payloadText: string): ResolveTraceInput => {
7
+ const parsed = JSON.parse(payloadText) as ResolveTraceInput
8
+ if (!parsed || typeof parsed !== "object") throw new Error("invalid resolveTrace payload")
9
+ if (!parsed.entryFileName) throw new Error("missing argument: entryFileName")
10
+ if (!parsed.entryLine) throw new Error("missing argument: entryLine")
11
+ if (!parsed.entryColumn) throw new Error("missing argument: entryColumn")
12
+ if (!Array.isArray(parsed.mapFilePaths) || parsed.mapFilePaths.length === 0) {
13
+ throw new Error("missing argument: mapFilePaths")
14
+ }
15
+ return {
16
+ ...parsed,
17
+ contextLineRadius: parsed.contextLineRadius || String(DEFAULT_CONTEXT_LINE_RADIUS),
18
+ }
19
+ }
20
+
21
+ export const isInputValidationMessage = (message: string) =>
22
+ INPUT_VALIDATION_PATTERNS.some((pattern) => message.includes(pattern))
23
+
24
+ export const toFallbackEntry = (parsed: ResolveTraceInput) => ({
25
+ fileName: parsed.entryFileName,
26
+ lineNumber: Number(parsed.entryLine) || 1,
27
+ columnNumber: Number(parsed.entryColumn) || 0,
28
+ })
29
+
30
+ export const createValidationMetaOutput = (message: string): TraceMetaOutputState => ({
31
+ traceData: [],
32
+ resolvedSourceMeta: null,
33
+ canFetchSourceSnippet: false,
34
+ message,
35
+ })
@@ -0,0 +1,35 @@
1
+ import type {TraceSnippetState} from "@/shared/trace-common";
2
+ import type {ResolveTraceInput} from "./types";
3
+
4
+ export const DEFAULT_MAX_SNIPPET_PAYLOAD_BYTES = 32 * 1024
5
+ export const SNIPPET_PAYLOAD_EXCEEDED_ERROR = "SOURCE_SNIPPET_PAYLOAD_EXCEEDED: 返回体超出限制"
6
+ const SNIPPET_PAYLOAD_OVERHEAD_BYTES = 512
7
+
8
+ export const parseMaxSnippetPayloadBytes = (input: ResolveTraceInput) => {
9
+ if (!input.maxSnippetPayloadBytes?.trim()) return DEFAULT_MAX_SNIPPET_PAYLOAD_BYTES
10
+ const parsed = Number(input.maxSnippetPayloadBytes)
11
+ if (!Number.isInteger(parsed) || parsed <= 0) throw new Error("maxSnippetPayloadBytes 必须是大于 0 的整数。")
12
+ return parsed
13
+ }
14
+
15
+ export const estimateSnippetPayloadBytes = (snippet: TraceSnippetState) => {
16
+ const codeBytes = Buffer.byteLength(snippet.traceCode, "utf8")
17
+ const sourceFileBytes = snippet.resolvedSourceMeta?.sourceFile
18
+ ? Buffer.byteLength(snippet.resolvedSourceMeta.sourceFile, "utf8")
19
+ : 0
20
+ const highlightBytes = snippet.traceHighlightLines.reduce((total, line) => {
21
+ return total + Buffer.byteLength(String(line), "utf8") + 1
22
+ }, 0)
23
+ return codeBytes + sourceFileBytes + highlightBytes + SNIPPET_PAYLOAD_OVERHEAD_BYTES
24
+ }
25
+
26
+ export const assertSnippetPayloadWithinLimit = (
27
+ snippet: TraceSnippetState,
28
+ input: ResolveTraceInput,
29
+ ) => {
30
+ const maxSnippetPayloadBytes = parseMaxSnippetPayloadBytes(input)
31
+ const payloadBytes = estimateSnippetPayloadBytes(snippet)
32
+ if (payloadBytes > maxSnippetPayloadBytes) throw new Error(
33
+ `${SNIPPET_PAYLOAD_EXCEEDED_ERROR} (${payloadBytes} bytes > ${maxSnippetPayloadBytes} bytes)`,
34
+ )
35
+ }
@@ -0,0 +1,37 @@
1
+ import type {SourceMapRegistry, TraceHop} from "@/node/trace/core/base/types";
2
+ import type {TraceLocation} from "@/shared/trace-common";
3
+ import type {ResolveTraceInput} from "@/shared/trace-contract";
4
+ export type {ResolveTraceInput};
5
+
6
+ export interface ResolveTaskContext {
7
+ entry: TraceLocation
8
+ contextLineRadius: number
9
+ sourceMaps: SourceMapRegistry
10
+ sourceMapFileNames: string[]
11
+ }
12
+
13
+ export interface ResolvedTraceContext {
14
+ traceChain: TraceHop[]
15
+ sourceCode: {
16
+ code: string
17
+ sourceFile: string
18
+ lineNumber: number
19
+ columnNumber: number
20
+ } | null
21
+ lastResolved: TraceLocation | null
22
+ }
23
+
24
+ export interface TraceProfilePhase {
25
+ name: string
26
+ durationMs: number
27
+ }
28
+
29
+ export interface ProfileResolveTraceOutput {
30
+ phases: TraceProfilePhase[]
31
+ totalMs: number
32
+ traceHopCount: number
33
+ resolvedSourceMetaExists: boolean
34
+ snippetPayloadBytes: number
35
+ maxSnippetPayloadBytes: number
36
+ snippetPayloadWouldExceedLimit: boolean
37
+ }
@@ -0,0 +1,149 @@
1
+ import {MAX_TRACE_DEPTH} from "@/node/trace/core/base/constants";
2
+ import {buildRegistryFromChainSlots, getAutoEntryFileNameFromSlots} from "@/node/trace/core/domain/chain-slots";
3
+ import {resolveSourceCodeFromTrace, getLastResolvedOutput} from "@/node/trace/core/domain/source-content";
4
+ import {resolveTraceBySourceMaps} from "@/node/trace/core/domain/trace-resolver";
5
+ import {toDisplayLocation, createCodeExcerpt} from "@/node/trace/core/domain/view-model";
6
+ import type {
7
+ ChainMapSlot,
8
+ SourceMapRegistry,
9
+ TraceHop,
10
+ } from "@/node/trace/core/base/types";
11
+ import {
12
+ DEFAULT_CONTEXT_LINE_RADIUS,
13
+ createInitialOutputState,
14
+ patchOutputMessage,
15
+ validateResolveInput,
16
+ } from "@/shared/trace-common";
17
+ import type {
18
+ TraceMetaOutputState as SharedTraceMetaOutputState,
19
+ TraceOutputState as SharedTraceOutputState,
20
+ TraceSnippetState,
21
+ } from "@/shared/trace-common";
22
+ import type {ResolvedTraceContext, ResolveTaskContext} from "./resolve/types";
23
+
24
+ export type TraceOutputState = SharedTraceOutputState<TraceHop>
25
+ export type TraceMetaOutputState = SharedTraceMetaOutputState<TraceHop>
26
+
27
+ export interface SourceMapChainState {
28
+ sourceMaps: SourceMapRegistry
29
+ sourceMapFileNames: string[]
30
+ chainMapSlots: ChainMapSlot[]
31
+ entryFileName: string
32
+ }
33
+
34
+ interface BuildTraceSnippetOutputInput {
35
+ contextLineRadius: number
36
+ resolved: ResolvedTraceContext
37
+ }
38
+
39
+ export const buildChainStateFromSlots = (slots: ChainMapSlot[]): SourceMapChainState => {
40
+ const {registry, orderedMapFileNames} = buildRegistryFromChainSlots(slots)
41
+ return {
42
+ sourceMaps: registry,
43
+ sourceMapFileNames: orderedMapFileNames,
44
+ chainMapSlots: slots,
45
+ entryFileName: getAutoEntryFileNameFromSlots(slots),
46
+ }
47
+ }
48
+
49
+ export const resolveTraceContextTask = async (context: ResolveTaskContext): Promise<ResolvedTraceContext> => {
50
+ const traceChain = await resolveTraceBySourceMaps({
51
+ entry: context.entry,
52
+ sourceMaps: context.sourceMaps,
53
+ orderedMapFileNames: context.sourceMapFileNames,
54
+ options: {maxDepth: MAX_TRACE_DEPTH},
55
+ })
56
+
57
+ const sourceCode = resolveSourceCodeFromTrace(traceChain, context.sourceMaps, {
58
+ onlyLastMap: true,
59
+ lastMapFileName: context.sourceMapFileNames[context.sourceMapFileNames.length - 1],
60
+ })
61
+
62
+ return {
63
+ traceChain,
64
+ sourceCode,
65
+ lastResolved: getLastResolvedOutput(traceChain),
66
+ }
67
+ }
68
+
69
+ export const buildTraceMetaOutput = (resolved: ResolvedTraceContext): TraceMetaOutputState => {
70
+ if (resolved.sourceCode) {
71
+ // 第一阶段只返回元信息;大段源码由第二阶段接口按需获取。
72
+ return {
73
+ traceData: resolved.traceChain,
74
+ resolvedSourceMeta: {
75
+ sourceFile: resolved.sourceCode.sourceFile,
76
+ lineNumber: resolved.sourceCode.lineNumber,
77
+ columnNumber: resolved.sourceCode.columnNumber,
78
+ },
79
+ canFetchSourceSnippet: true,
80
+ message: "已定位到源码位置,开始加载源码片段。",
81
+ }
82
+ }
83
+
84
+ if (resolved.lastResolved) {
85
+ const display = toDisplayLocation(resolved.lastResolved)
86
+ return {
87
+ traceData: resolved.traceChain,
88
+ resolvedSourceMeta: {
89
+ sourceFile: display.fileName,
90
+ lineNumber: display.lineNumber,
91
+ columnNumber: display.columnNumber,
92
+ },
93
+ canFetchSourceSnippet: false,
94
+ message: "当前 map 不包含 sourcesContent,无法直接渲染源码内容。",
95
+ }
96
+ }
97
+
98
+ return {
99
+ traceData: resolved.traceChain,
100
+ resolvedSourceMeta: null,
101
+ canFetchSourceSnippet: false,
102
+ message: "未命中映射,未定位到源码位置。",
103
+ }
104
+ }
105
+
106
+ export const buildTraceSnippetOutput = (input: BuildTraceSnippetOutputInput): TraceSnippetState => {
107
+ const {contextLineRadius, resolved} = input
108
+ if (resolved.sourceCode) {
109
+ const excerpt = createCodeExcerpt({
110
+ fullCode: resolved.sourceCode.code,
111
+ targetLine: resolved.sourceCode.lineNumber,
112
+ radius: contextLineRadius,
113
+ targetColumn: resolved.sourceCode.columnNumber,
114
+ })
115
+ return {
116
+ traceCode: excerpt.code,
117
+ resolvedSourceMeta: {
118
+ sourceFile: resolved.sourceCode.sourceFile,
119
+ lineNumber: resolved.sourceCode.lineNumber,
120
+ columnNumber: resolved.sourceCode.columnNumber,
121
+ },
122
+ traceHighlightLines: [excerpt.highlightLine],
123
+ }
124
+ }
125
+
126
+ if (resolved.lastResolved) {
127
+ const display = toDisplayLocation(resolved.lastResolved)
128
+ return {
129
+ traceCode: [
130
+ `已定位到源码位置:${display.fileName}:${display.lineNumber}:${display.columnNumber}`,
131
+ "当前 map 不包含 sourcesContent,无法直接渲染源码内容。",
132
+ ].join("\n"),
133
+ resolvedSourceMeta: {
134
+ sourceFile: display.fileName,
135
+ lineNumber: display.lineNumber,
136
+ columnNumber: display.columnNumber,
137
+ },
138
+ traceHighlightLines: [],
139
+ }
140
+ }
141
+
142
+ return {
143
+ traceCode: "未命中映射,未定位到源码位置。",
144
+ resolvedSourceMeta: null,
145
+ traceHighlightLines: [],
146
+ }
147
+ }
148
+
149
+ export {DEFAULT_CONTEXT_LINE_RADIUS, createInitialOutputState, patchOutputMessage, validateResolveInput}
@@ -0,0 +1,104 @@
1
+ export const DEFAULT_CONTEXT_LINE_RADIUS = 3
2
+
3
+ export interface TraceLocation {
4
+ fileName: string
5
+ lineNumber: number
6
+ columnNumber: number
7
+ }
8
+
9
+ export interface ResolvedSourceMeta {
10
+ sourceFile: string
11
+ lineNumber: number
12
+ columnNumber: number
13
+ }
14
+
15
+ export interface TraceSnippetState {
16
+ traceCode: string
17
+ resolvedSourceMeta: ResolvedSourceMeta | null
18
+ traceHighlightLines: number[]
19
+ }
20
+
21
+ export interface TraceMetaOutputState<TTraceHop = unknown> {
22
+ traceData: TTraceHop[]
23
+ resolvedSourceMeta: ResolvedSourceMeta | null
24
+ canFetchSourceSnippet: boolean
25
+ message: string
26
+ }
27
+
28
+ export interface ResolveInputConfig {
29
+ entryFileName: string
30
+ entryLine: string
31
+ entryColumn: string
32
+ contextLineRadius: string
33
+ mapCount: number
34
+ }
35
+
36
+ export type ResolveInputValidation =
37
+ | { ok: true; entry: TraceLocation; contextLineRadius: number }
38
+ | { ok: false; message: string }
39
+
40
+ export interface TraceOutputState<TTraceHop = unknown> {
41
+ traceData: TTraceHop[]
42
+ traceCode: string
43
+ resolvedSourceMeta: ResolvedSourceMeta | null
44
+ traceHighlightLines: number[]
45
+ }
46
+
47
+ export const createInitialOutputState = <TTraceHop = unknown>(): TraceOutputState<TTraceHop> => {
48
+ return {
49
+ traceData: [],
50
+ traceCode: "请先上传一个或多个 .map 文件。",
51
+ resolvedSourceMeta: null,
52
+ traceHighlightLines: [],
53
+ }
54
+ }
55
+
56
+ export const patchOutputMessage = <TTraceHop = unknown>(
57
+ prev: TraceOutputState<TTraceHop>,
58
+ message: string,
59
+ ): TraceOutputState<TTraceHop> => {
60
+ return {
61
+ ...prev,
62
+ traceCode: message,
63
+ traceHighlightLines: [],
64
+ }
65
+ }
66
+
67
+ export const validateResolveInput = (config: ResolveInputConfig): ResolveInputValidation => {
68
+ const parsedLine = Number(config.entryLine)
69
+ const parsedColumn = Number(config.entryColumn)
70
+ const parsedContextLineRadius = Number(config.contextLineRadius)
71
+ const normalizedFileName = config.entryFileName.replace(/\\/g, "/").trim()
72
+
73
+ if (!normalizedFileName) {
74
+ return {ok: false, message: "请输入入口文件名,例如 index.js"}
75
+ }
76
+ if (!config.entryLine.trim()) {
77
+ return {ok: false, message: "请输入入口行号(1-based)。"}
78
+ }
79
+ if (!config.entryColumn.trim()) {
80
+ return {ok: false, message: "请输入入口列号(0-based)。"}
81
+ }
82
+ if (!Number.isInteger(parsedLine) || parsedLine < 1) {
83
+ return {ok: false, message: "行号必须是大于等于 1 的整数。"}
84
+ }
85
+ if (!Number.isInteger(parsedColumn) || parsedColumn < 0) {
86
+ return {ok: false, message: "列号必须是大于等于 0 的整数。"}
87
+ }
88
+ if (!Number.isInteger(parsedContextLineRadius) || parsedContextLineRadius < 0) {
89
+ return {ok: false, message: "上下文行数必须是大于等于 0 的整数。"}
90
+ }
91
+ if (config.mapCount === 0) {
92
+ return {ok: false, message: "未加载可用 map 文件,请先上传。"}
93
+ }
94
+
95
+ return {
96
+ ok: true,
97
+ entry: {
98
+ fileName: normalizedFileName,
99
+ lineNumber: parsedLine,
100
+ columnNumber: parsedColumn,
101
+ },
102
+ contextLineRadius: parsedContextLineRadius,
103
+ }
104
+ }
@@ -0,0 +1,29 @@
1
+ import type {ResolvedSourceMeta, TraceLocation} from "@/shared/trace-common";
2
+
3
+ export interface ResolveTraceInput {
4
+ entryFileName: string
5
+ entryLine: string
6
+ entryColumn: string
7
+ contextLineRadius?: string
8
+ mapFilePaths: string[]
9
+ maxSnippetPayloadBytes?: string
10
+ }
11
+
12
+ export interface TraceHopPayload {
13
+ input: TraceLocation
14
+ output?: TraceLocation
15
+ mapFileName?: string
16
+ }
17
+
18
+ export interface ResolveTraceMetaOutput {
19
+ traceData: TraceHopPayload[]
20
+ resolvedSourceMeta: ResolvedSourceMeta | null
21
+ canFetchSourceSnippet: boolean
22
+ message: string
23
+ }
24
+
25
+ export interface ResolveTraceSnippetOutput {
26
+ traceCode: string
27
+ resolvedSourceMeta: ResolvedSourceMeta | null
28
+ traceHighlightLines: number[]
29
+ }
package/src/types.ts ADDED
@@ -0,0 +1,19 @@
1
+ export type {
2
+ ResolveTraceInput,
3
+ ResolveTraceMetaOutput,
4
+ ResolveTraceSnippetOutput,
5
+ } from "@/shared/trace-contract";
6
+
7
+ export interface MapInputConfig {
8
+ entryLine: string
9
+ entryColumn: string
10
+ contextLineRadius: string
11
+ }
12
+
13
+ export interface ForgeKitBridge {
14
+ applyNodeMethod?: (method: string, ...args: string[]) => Promise<unknown>
15
+ openFileDialog?: (options: {
16
+ multiple?: boolean
17
+ filters?: {name: string; extensions: string[]}[]
18
+ }) => Promise<string | string[] | null>
19
+ }
@@ -0,0 +1,12 @@
1
+ export {SourceMapUtils} from "./state"
2
+
3
+ export type {
4
+ TraceOutputState,
5
+ SourceMapChainState,
6
+ TraceDisplayHop,
7
+ ResolveInputConfig,
8
+ ChainMapSlot,
9
+ TraceLocation,
10
+ TraceHop,
11
+ ResolvedSourceMeta,
12
+ } from "./state"
@@ -0,0 +1,81 @@
1
+ import {
2
+ DEFAULT_CONTEXT_LINE_RADIUS,
3
+ createInitialOutputState,
4
+ patchOutputMessage,
5
+ validateResolveInput,
6
+ } from "@/shared/trace-common";
7
+ import type {
8
+ ResolveInputConfig,
9
+ ResolveInputValidation,
10
+ TraceLocation,
11
+ TraceOutputState as SharedTraceOutputState,
12
+ } from "@/shared/trace-common";
13
+ export type {ResolveInputConfig, ResolvedSourceMeta, TraceLocation} from "@/shared/trace-common";
14
+
15
+ export interface TraceHop {
16
+ input: TraceLocation
17
+ output?: TraceLocation
18
+ mapFileName?: string
19
+ }
20
+
21
+ export type TraceOutputState = SharedTraceOutputState<TraceHop>
22
+
23
+ export interface SourceMapChainState {
24
+ chainMapSlots: ChainMapSlot[]
25
+ entryFileName: string
26
+ }
27
+
28
+ export interface ChainMapSlot {
29
+ id: string
30
+ mapFileName: string
31
+ mapFilePath?: string
32
+ error?: string
33
+ }
34
+
35
+ export interface TraceDisplayHop {
36
+ input: TraceLocation
37
+ output?: TraceLocation
38
+ mapFileName?: string
39
+ }
40
+
41
+ export class SourceMapUtils {
42
+ static readonly DEFAULT_CONTEXT_LINE_RADIUS = DEFAULT_CONTEXT_LINE_RADIUS
43
+
44
+ static createInitialOutputState(): TraceOutputState {
45
+ return createInitialOutputState<TraceHop>()
46
+ }
47
+
48
+ static patchOutputMessage(prev: TraceOutputState, message: string): TraceOutputState {
49
+ return patchOutputMessage(prev, message)
50
+ }
51
+
52
+ static removeChainSlot(slots: ChainMapSlot[], slotId: string): ChainMapSlot[] {
53
+ return slots.filter((item) => item.id !== slotId)
54
+ }
55
+
56
+ static reorderChainSlots(slots: ChainMapSlot[], nextIds: string[]): ChainMapSlot[] {
57
+ const idOrder = new Map(nextIds.map((id, index) => [id, index]))
58
+ return [...slots].sort((a, b) => {
59
+ const aIndex = idOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER
60
+ const bIndex = idOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER
61
+ return aIndex - bIndex
62
+ })
63
+ }
64
+
65
+ static mapTraceForDisplay(traceData: TraceHop[]): TraceDisplayHop[] {
66
+ return traceData.map((item) => {
67
+ let output: TraceLocation | undefined
68
+ if (item.output) output = {...item.output}
69
+ return {
70
+ input: {...item.input},
71
+ output,
72
+ mapFileName: item.mapFileName,
73
+ }
74
+ })
75
+ }
76
+
77
+ static validateResolveInput(config: ResolveInputConfig): ResolveInputValidation {
78
+ return validateResolveInput(config)
79
+ }
80
+
81
+ }