Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/core/deno/debug.ts
3584 views
1
/*
2
* debug.ts
3
*
4
* Copyright (C) 2024 Posit Software, PBC
5
*
6
* Debugging utilities.
7
*/
8
9
import * as colors from "fmt/colors";
10
11
type StackEntry = {
12
pos: string;
13
name: string;
14
line: string;
15
col: string;
16
};
17
const compareEntry = (prev: StackEntry, next: StackEntry): boolean => {
18
return prev.pos === next.pos && prev.line === next.line &&
19
prev.col === next.col;
20
};
21
let previousStack: StackEntry[] = [];
22
23
// returns the length of the common prefix of the two stacks
24
const compareStacks = (prev: StackEntry[], next: StackEntry[]): number => {
25
prev = prev.toReversed();
26
next = next.toReversed();
27
let i = 0;
28
while (i < prev.length && i < next.length && compareEntry(prev[i], next[i])) {
29
i++;
30
}
31
return i;
32
};
33
34
export const getStackAsArray = (
35
format?: "json" | "raw" | "ansi",
36
offset?: number,
37
) => {
38
let rawStack = (new Error().stack ?? "").split("\n").slice(offset ?? 2);
39
// now we heuristically try to match the first entry of the stack trace (last in the stack)
40
// to our expectations of quarto.ts being the entry point.
41
// This will only happen in dev builds and when
42
//
43
// export QUARTO_DENO_V8_OPTIONS=--stack-trace-limit=100
44
//
45
// is set.
46
const m = rawStack[rawStack.length - 1].match(
47
/^.*at async (.*)src\/quarto.ts:\d+:\d+$/,
48
);
49
if (!m) {
50
console.log(
51
"Could not find quarto.ts in stack trace, is QUARTO_DENO_V8_OPTIONS set with a sufficiently-large stack size?",
52
);
53
}
54
55
// filter out eventLoopTick
56
rawStack = rawStack.filter((s) =>
57
!(s.match(/eventLoopTick/) && s.match(/core\/01_core.js/))
58
);
59
if (m && (typeof format !== "undefined") && (format !== "raw")) {
60
const pathPrefix = m[1];
61
// first, trim all the path prefixes
62
rawStack = rawStack.map((s) => s.replace(pathPrefix, ""));
63
// then, entries can be async or not, and be in the main entry point or not.
64
65
// main entry point async entries look like: "at async src/quarto.ts:170:5"
66
// main entry point sync entries look like: "at src/quarto.ts:170:5"
67
// other async entries look like: "at async render (src/command/render/render-shared.ts:112:22)"
68
// other sync entries look like: "at render (src/command/render/render-shared.ts:112:22)"
69
70
// we want them all to start with the source file and line number in parentheses
71
const entries: StackEntry[] = rawStack.map((s) => {
72
// main entry point? (no parentheses)
73
const m1 = s.match(/^.*at (async )?(src\/quarto.ts):(\d+):(\d+)$/);
74
if (m1) {
75
return {
76
pos: m1[2],
77
name: `<main>`,
78
line: m1[3],
79
// if async, move the column to the start of the actual function name
80
col: m1[4] + (m1[1] ? 6 : 0),
81
};
82
}
83
// other stack entry
84
// (with parentheses)
85
const m2 = s.match(/^.*at (async )?(.*) \((src\/.+):(\d+):(\d+)\)$/) ||
86
// without parentheses - async and name will be empty
87
s.match(/^.*at (async )?(.*)(src\/.+):(\d+):(\d+)$/);
88
if (m2) {
89
return {
90
pos: m2[3],
91
name: `${m2[2]}`,
92
line: m2[4],
93
// if async, move the column to the start of the actual function name
94
col: m2[5] + (m2[1] ? 6 : 0),
95
};
96
}
97
// links to deno's core?
98
// FIXME these will generate bad links in vscode
99
const m3 = s.match(
100
/^.*at (async )?(.*) \(ext:(core\/.+):(\d+):(\d+)*\)$/,
101
);
102
if (m3) {
103
return {
104
pos: m3[3],
105
name: `${m3[2]}`,
106
line: m3[4],
107
// if async, move the column to the start of the actual function name
108
col: m3[5] + (m3[1] ? 6 : 0),
109
};
110
}
111
112
// at async Command.execute (https://deno.land/x/[email protected]/command/command.ts:1948:7)
113
const m4 = s.match(
114
/^.*at (async )?(.*) \((http.+):(\d+):(\d+)*\)$/,
115
);
116
if (m4) {
117
return {
118
pos: m4[3],
119
name: `${m4[2]}`,
120
line: m4[4],
121
// if async, move the column to the start of the actual function name
122
col: m4[5] + (m4[1] ? 6 : 0),
123
};
124
}
125
126
// at Array.map (<anonymous>)
127
const m5 = s.match(
128
/^.*at (.*)\(<anonymous>\)$/,
129
);
130
if (m5) {
131
return {
132
pos: "",
133
name: `${m5[1]}`,
134
line: "",
135
col: "",
136
};
137
}
138
throw new Error(`Unexpected stack entry: ${s}`);
139
});
140
141
if (format === "json") {
142
return entries;
143
}
144
const maxPosLength = Math.max(...entries.map((e) => e.pos.length));
145
const maxLineLength = Math.max(
146
...entries.map((e) => String(e.line).length),
147
);
148
const maxColLength = Math.max(...entries.map((e) => String(e.col).length));
149
// this one only works in super fancy terminal emulators and vscode, :shrug:
150
// https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
151
// now we format them all
152
const accentColor = colors.gray;
153
const commonPrefixLength = compareStacks(previousStack, entries);
154
rawStack = entries.map((e, i) => {
155
const linkedPos = `\x1b]8;;${
156
pathPrefix.replace("file://", "vscode://file")
157
}${e.pos}:${e.line}:${e.col}\x1b\\${e.pos}\x1b]8;;\x1b\\`;
158
const srcPadding = " ".repeat(maxPosLength - e.pos.length);
159
const linePadding = " ".repeat(maxLineLength - String(e.line).length);
160
const colPadding = " ".repeat(maxColLength - String(e.col).length);
161
162
const isFirstChange = i === entries.length - commonPrefixLength - 1;
163
if (!isFirstChange) {
164
return `${srcPadding}${
165
accentColor(
166
linkedPos + ":" + linePadding + e.line + colPadding + ":" + e.col +
167
": " + e.name,
168
)
169
}`;
170
}
171
return `${srcPadding}${linkedPos}${
172
accentColor(":")
173
}${linePadding}${e.line}${accentColor(":")}${colPadding}${e.col}${
174
accentColor(":")
175
} ${colors.yellow(e.name)}`;
176
});
177
previousStack = entries;
178
}
179
return rawStack;
180
};
181
182
export const getStack = (format?: "json" | "raw" | "ansi", offset?: number) => {
183
return "Stack:\n" +
184
getStackAsArray(format, offset ? offset + 1 : 3).join("\n");
185
};
186
187
// use debugPrint instead of console.log so it's easy to find stray print statements
188
// on our codebase
189
//
190
// deno-lint-ignore no-explicit-any
191
export const debugPrint = (...data: any[]) => {
192
console.log(...data);
193
};
194
195
export const debugLogWithStack = async (...data: unknown[]) => {
196
const payload = {
197
payload: data,
198
stack: getStackAsArray(),
199
timestamp: new Date().toISOString(),
200
};
201
await Deno.writeTextFile(
202
"/tmp/stack-debug.json",
203
JSON.stringify(payload) + "\n",
204
{
205
append: true,
206
},
207
);
208
};
209
210