Skip to content

Commit bcc5ea3

Browse files
authored
Merge pull request #13013 from CesiumGS/clear-console
Add option to clear the console in Sandcastle
2 parents 278174f + d4582b5 commit bcc5ea3

File tree

6 files changed

+258
-90
lines changed

6 files changed

+258
-90
lines changed

packages/sandcastle/public/Sandcastle-client.js

Lines changed: 184 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,199 @@
66
return value !== undefined;
77
}
88

9+
/**
10+
* Convert an object to a single line string only displaying the top level keys and values.
11+
* This is meant as a compromise instead of displaying [object Object]
12+
*
13+
* This is handy for logging simple object values while also avoiding getting way out of hand
14+
* when logging large complex objects that would create a massive string using JSON.stringify directly.
15+
* This can still generate large strings for large objects like our Viewer but it's still shorter.
16+
*
17+
* @param {object} obj
18+
* @returns {string}
19+
*/
20+
function simpleStringify(obj) {
21+
if ("toString" in obj && obj.toString !== Object.prototype.toString) {
22+
// Use customized toString functions if they're specified
23+
// if it's the native function continue instead of getting [object Object]
24+
return obj.toString();
25+
}
26+
27+
const properties = Object.entries(obj);
28+
29+
if (obj.constructor.name !== "Object") {
30+
// Iterate through the prototype's properties too to grab any extra getters
31+
// which are common across CesiumJS classes
32+
// https://stackoverflow.com/questions/60400066/how-to-enumerate-discover-getters-and-setters-in-javascript
33+
const prototypeProperties = Object.entries(
34+
Object.getOwnPropertyDescriptors(Reflect.getPrototypeOf(obj)),
35+
);
36+
properties.push(...prototypeProperties);
37+
}
38+
39+
const keyValueStrings = properties.map(([key, value]) => {
40+
let valueStr = value;
41+
if (typeof value === "string") {
42+
valueStr = `"${value}"`;
43+
} else if (typeof value === "function") {
44+
valueStr = functionString(value, true);
45+
} else if (Array.isArray(value)) {
46+
valueStr = arrayString(value);
47+
}
48+
return `${key}: ${valueStr}`;
49+
});
50+
51+
const className =
52+
obj.constructor.name !== "Object" ? `${obj.constructor.name} ` : "";
53+
return `${className}{${keyValueStrings.join(", ")}}`;
54+
}
55+
56+
function arrayString(arr) {
57+
return `[${arr.join(", ")}]`;
58+
}
59+
60+
function functionString(func, signatureOnly) {
61+
const functionAsString = func.toString();
62+
if (signatureOnly) {
63+
const signaturePattern = /function.*\)/;
64+
const functionSigMatch = functionAsString
65+
.toString()
66+
.match(signaturePattern);
67+
return functionSigMatch
68+
? `${functionSigMatch[0].replace("function", "ƒ")} {...}`
69+
: "ƒ () {...}";
70+
}
71+
72+
const lineTruncatePattern = /function.*(?:\n.*){0,4}/;
73+
const linesTruncatedMatch = functionAsString.match(lineTruncatePattern);
74+
if (linesTruncatedMatch === null) {
75+
// unable to match and truncate by lines for some reason
76+
return linesTruncatedMatch.toString();
77+
}
78+
let truncated = linesTruncatedMatch[0];
79+
if (functionAsString.length > truncated.length) {
80+
truncated += "\n...";
81+
}
82+
return truncated.replace("function", "ƒ");
83+
}
84+
85+
function errorLineNumber(error) {
86+
if (typeof error.stack !== "string") {
87+
return;
88+
}
89+
90+
// Look for error.stack, "bucket.html:line:char"
91+
let lineNumber = -1;
92+
const stack = error.stack;
93+
let pos = stack.indexOf(bucket);
94+
if (pos < 0) {
95+
pos = stack.indexOf("<anonymous>");
96+
}
97+
if (pos >= 0) {
98+
const lineStart = stack.indexOf(":", pos);
99+
if (lineStart > pos) {
100+
let lineEnd1 = stack.indexOf(":", lineStart + 1);
101+
const lineEnd2 = stack.indexOf("\n", lineStart + 1);
102+
if (
103+
lineEnd2 > lineStart &&
104+
(lineEnd2 < lineEnd1 || lineEnd1 < lineStart)
105+
) {
106+
lineEnd1 = lineEnd2;
107+
}
108+
if (lineEnd1 > lineStart) {
109+
/*eslint-disable no-empty*/
110+
try {
111+
lineNumber = parseInt(stack.substring(lineStart + 1, lineEnd1), 10);
112+
} catch (ex) {}
113+
/*eslint-enable no-empty*/
114+
}
115+
}
116+
}
117+
return lineNumber;
118+
}
119+
120+
/**
121+
* Take a singular value and return the string representation for it.
122+
* Handles multiple types with specific handling for arrays, objects and functions.
123+
* Any value that is not specifically processed will get converted by `value.toString()`
124+
*
125+
* @param {any} value
126+
* @returns {string}
127+
*/
9128
function print(value) {
10129
if (value === null) {
11130
return "null";
12-
} else if (defined(value)) {
13-
return value.toString();
14131
}
15-
return "undefined";
132+
if (value === undefined) {
133+
return "undefined";
134+
}
135+
if (Array.isArray(value)) {
136+
// there's a small chance this recurssion gets out of hand for nested arrays
137+
return arrayString(
138+
value.map((value) => {
139+
if (typeof value === "function") {
140+
return functionString(value, true);
141+
}
142+
return print(value);
143+
}),
144+
);
145+
}
146+
if (typeof value.stack === "string") {
147+
// assume this is an Error object and attempt to extract the line number
148+
const lineNumber = errorLineNumber(value);
149+
if (lineNumber !== undefined) {
150+
return `${value.toString()} (on line ${lineNumber})`;
151+
}
152+
}
153+
if (typeof value === "function") {
154+
return functionString(value);
155+
}
156+
if (typeof value === "object") {
157+
return simpleStringify(value);
158+
}
159+
return value.toString();
160+
}
161+
162+
/**
163+
* Combine any number of arguments into a single string converting them as needed.
164+
*
165+
* @param {any[]} args an array of any values, can be mixed types
166+
* @returns {string}
167+
*/
168+
function combineArguments(args) {
169+
return args.map(print).join(" ");
16170
}
17171

172+
const originalClear = console.clear;
173+
console.clear = function () {
174+
originalClear();
175+
window.parent.postMessage(
176+
{
177+
type: "consoleClear",
178+
},
179+
"*",
180+
);
181+
};
182+
18183
const originalLog = console.log;
19-
console.log = function (d1) {
20-
originalLog.apply(console, arguments);
184+
console.log = function (...args) {
185+
originalLog.apply(console, args);
21186
window.parent.postMessage(
22187
{
23188
type: "consoleLog",
24-
log: print(d1),
189+
log: combineArguments(args),
25190
},
26191
"*",
27192
);
28193
};
29194

30195
const originalWarn = console.warn;
31-
console.warn = function (d1) {
32-
originalWarn.apply(console, arguments);
196+
console.warn = function (...args) {
197+
originalWarn.apply(console, args);
33198
window.parent.postMessage(
34199
{
35200
type: "consoleWarn",
36-
warn: defined(d1) ? d1.toString() : "undefined",
201+
warn: combineArguments(args),
37202
},
38203
"*",
39204
);
@@ -46,9 +211,9 @@
46211
}
47212

48213
const originalError = console.error;
49-
console.error = function (d1) {
50-
originalError.apply(console, arguments);
51-
if (!defined(d1)) {
214+
console.error = function (...args) {
215+
originalError.apply(console, args);
216+
if (args.length === 0 || !defined(args[0])) {
52217
window.parent.postMessage(
53218
{
54219
type: "consoleError",
@@ -59,58 +224,13 @@
59224
return;
60225
}
61226

62-
// Look for d1.stack, "bucket.html:line:char"
63-
let lineNumber = -1;
64-
const errorMsg = d1.toString();
65-
if (typeof d1.stack === "string") {
66-
const stack = d1.stack;
67-
let pos = stack.indexOf(bucket);
68-
if (pos < 0) {
69-
pos = stack.indexOf("<anonymous>");
70-
}
71-
if (pos >= 0) {
72-
const lineStart = stack.indexOf(":", pos);
73-
if (lineStart > pos) {
74-
let lineEnd1 = stack.indexOf(":", lineStart + 1);
75-
const lineEnd2 = stack.indexOf("\n", lineStart + 1);
76-
if (
77-
lineEnd2 > lineStart &&
78-
(lineEnd2 < lineEnd1 || lineEnd1 < lineStart)
79-
) {
80-
lineEnd1 = lineEnd2;
81-
}
82-
if (lineEnd1 > lineStart) {
83-
/*eslint-disable no-empty*/
84-
try {
85-
lineNumber = parseInt(
86-
stack.substring(lineStart + 1, lineEnd1),
87-
10,
88-
);
89-
} catch (ex) {}
90-
/*eslint-enable no-empty*/
91-
}
92-
}
93-
}
94-
}
95-
96-
if (lineNumber >= 0) {
97-
window.parent.postMessage(
98-
{
99-
type: "consoleError",
100-
error: errorMsg,
101-
lineNumber: lineNumber,
102-
},
103-
"*",
104-
);
105-
} else {
106-
window.parent.postMessage(
107-
{
108-
type: "consoleError",
109-
error: errorMsg,
110-
},
111-
"*",
112-
);
113-
}
227+
window.parent.postMessage(
228+
{
229+
type: "consoleError",
230+
error: combineArguments(args),
231+
},
232+
"*",
233+
);
114234
};
115235

116236
window.onerror = function (errorMsg, url, lineNumber) {

packages/sandcastle/src/App.tsx

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ export type SandcastleAction =
196196
function App() {
197197
const { settings, updateSettings } = useContext(SettingsContext);
198198
const rightSideRef = useRef<RightSideRef>(null);
199-
const consoleCollapsedHeight = 26;
199+
const consoleCollapsedHeight = 33;
200200
const [consoleExpanded, setConsoleExpanded] = useState(false);
201201

202202
const isStartingWithCode = !!(window.location.search || window.location.hash);
@@ -319,14 +319,27 @@ function App() {
319319
[consoleExpanded],
320320
);
321321

322-
const resetConsole = useCallback(() => {
323-
if (codeState.runNumber > 0) {
324-
// the console should only be cleared by the Bucket when the viewer page
325-
// has actually reloaded and stopped sending console statements
326-
// otherwise some could bleed into the "next run"
327-
setConsoleMessages([]);
328-
}
329-
}, [codeState.runNumber]);
322+
const resetConsole = useCallback(
323+
({ showMessage = false } = {}) => {
324+
if (codeState.runNumber > 0) {
325+
// the console should only be cleared by the Bucket when the viewer page
326+
// has actually reloaded and stopped sending console statements
327+
// otherwise some could bleed into the "next run"
328+
if (showMessage) {
329+
setConsoleMessages([
330+
{
331+
id: crypto.randomUUID(),
332+
type: "special",
333+
message: "Console was cleared",
334+
},
335+
]);
336+
} else {
337+
setConsoleMessages([]);
338+
}
339+
}
340+
},
341+
[codeState.runNumber],
342+
);
330343

331344
function runSandcastle() {
332345
dispatch({ type: "runSandcastle" });
@@ -634,6 +647,7 @@ function App() {
634647
logs={consoleMessages}
635648
expanded={consoleExpanded}
636649
toggleExpanded={() => rightSideRef.current?.toggleExpanded()}
650+
resetConsole={resetConsole}
637651
/>
638652
</Allotment.Pane>
639653
</RightSideAllotment>

packages/sandcastle/src/Bucket.tsx

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ConsoleMessageType } from "./ConsoleMirror";
55

66
type SandcastleMessage =
77
| { type: "reload" }
8+
| { type: "consoleClear" }
89
| { type: "consoleLog"; log: string }
910
| { type: "consoleWarn"; warn: string }
1011
| { type: "consoleError"; error: string; lineNumber?: number; url?: string }
@@ -30,7 +31,7 @@ function Bucket({
3031
*/
3132
highlightLine: (lineNumber: number) => void;
3233
appendConsole: (type: ConsoleMessageType, message: string) => void;
33-
resetConsole: () => void;
34+
resetConsole: (options?: { showMessage?: boolean | undefined }) => void;
3435
}) {
3536
const bucket = useRef<HTMLIFrameElement>(null);
3637
const lastRunNumber = useRef<number>(Number.NEGATIVE_INFINITY);
@@ -150,11 +151,6 @@ function Bucket({
150151
lastRunNumber.current = runNumber;
151152
}, [code, html, runNumber]);
152153

153-
function scriptLineToEditorLine(line: number) {
154-
// editor lines are zero-indexed, plus 3 lines of boilerplate
155-
return line - 1;
156-
}
157-
158154
useEffect(() => {
159155
const messageHandler = function (e: MessageEvent<SandcastleMessage>) {
160156
// The iframe (bucket.html) sends this message on load.
@@ -172,22 +168,22 @@ function Bucket({
172168
// into the iframe, causing the demo to run there.
173169
activateBucketScripts(bucket.current, code, html);
174170
}
171+
} else if (e.data.type === "consoleClear") {
172+
resetConsole({ showMessage: true });
175173
} else if (e.data.type === "consoleLog") {
176174
// Console log messages from the iframe display in Sandcastle.
177175
appendConsole("log", e.data.log);
178176
} else if (e.data.type === "consoleError") {
179177
// Console error messages from the iframe display in Sandcastle
180178
let errorMsg = e.data.error;
181-
let lineNumber = e.data.lineNumber;
179+
const lineNumber = e.data.lineNumber;
182180
if (lineNumber) {
183-
errorMsg += " (on line ";
181+
errorMsg += ` (on line ${lineNumber}`;
184182

185183
if (e.data.url) {
186-
errorMsg += `${lineNumber} of ${e.data.url})`;
187-
} else {
188-
lineNumber = scriptLineToEditorLine(lineNumber);
189-
errorMsg += `${lineNumber + 1})`;
184+
errorMsg += ` of ${e.data.url}`;
190185
}
186+
errorMsg += ")";
191187
}
192188
appendConsole("error", errorMsg);
193189
} else if (e.data.type === "consoleWarn") {

0 commit comments

Comments
 (0)