Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 173 additions & 64 deletions packages/sandcastle/public/Sandcastle-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,167 @@
return value !== undefined;
}

/**
* Convert an object to a single line string only displaying the top level keys and values.
* This is meant as a compromise instead of displaying [object Object]
*
* This is handy for logging simple object values while also avoiding getting way out of hand
* when logging large complex objects that would create a massive string using JSON.stringify directly.
* This can still generate large strings for large objects like our Viewer but it's still shorter.
*
* @param {object} obj
* @returns {string}
*/
function simpleStringify(obj) {
if ("toString" in obj && obj.toString !== Object.prototype.toString) {
// Use customized toString functions if they're specified
// if it's the native function continue instead of getting [object Object]
return obj.toString();
}

const properties = Object.entries(obj);

if (obj.constructor.name !== "Object") {
// Iterate through the prototype's properties too to grab any extra getters
// which are common across CesiumJS classes
// https://stackoverflow.com/questions/60400066/how-to-enumerate-discover-getters-and-setters-in-javascript
const prototypeProperties = Object.entries(
Object.getOwnPropertyDescriptors(Reflect.getPrototypeOf(obj)),
);
properties.push(...prototypeProperties);
}

const keyValueStrings = properties.map(([key, value]) => {
let valueStr = value;
if (typeof value === "string") {
valueStr = `"${value}"`;
} else if (typeof value === "function") {
valueStr = functionString(value, true);
} else if (Array.isArray(value)) {
valueStr = arrayString(value);
}
return `${key}: ${valueStr}`;
});

const className =
obj.constructor.name !== "Object" ? `${obj.constructor.name} ` : "";
return `${className}{${keyValueStrings.join(", ")}}`;
}

function arrayString(arr) {
return `[${arr.join(", ")}]`;
}

function functionString(func, signatureOnly) {
const functionAsString = func.toString();
if (signatureOnly) {
const signaturePattern = /function.*\)/;
const functionSigMatch = functionAsString
.toString()
.match(signaturePattern);
return functionSigMatch
? `${functionSigMatch[0].replace("function", "ƒ")} {...}`
: "ƒ () {...}";
}

const lineTruncatePattern = /function.*(?:\n.*){0,4}/;
const linesTruncatedMatch = functionAsString.match(lineTruncatePattern);
if (linesTruncatedMatch === null) {
// unable to match and truncate by lines for some reason
return linesTruncatedMatch.toString();
}
let truncated = linesTruncatedMatch[0];
if (functionAsString.length > truncated.length) {
truncated += "\n...";
}
return truncated.replace("function", "ƒ");
}

function errorLineNumber(error) {
if (typeof error.stack !== "string") {
return;
}

// Look for error.stack, "bucket.html:line:char"
let lineNumber = -1;
const stack = error.stack;
let pos = stack.indexOf(bucket);
if (pos < 0) {
pos = stack.indexOf("<anonymous>");
}
if (pos >= 0) {
const lineStart = stack.indexOf(":", pos);
if (lineStart > pos) {
let lineEnd1 = stack.indexOf(":", lineStart + 1);
const lineEnd2 = stack.indexOf("\n", lineStart + 1);
if (
lineEnd2 > lineStart &&
(lineEnd2 < lineEnd1 || lineEnd1 < lineStart)
) {
lineEnd1 = lineEnd2;
}
if (lineEnd1 > lineStart) {
/*eslint-disable no-empty*/
try {
lineNumber = parseInt(stack.substring(lineStart + 1, lineEnd1), 10);
} catch (ex) {}
/*eslint-enable no-empty*/
}
}
}
return lineNumber;
}

/**
* Take a singular value and return the string representation for it.
* Handles multiple types with specific handling for arrays, objects and functions.
* Any value that is not specifically processed will get converted by `value.toString()`
*
* @param {any} value
* @returns {string}
*/
function print(value) {
if (value === null) {
return "null";
} else if (defined(value)) {
return value.toString();
}
return "undefined";
if (value === undefined) {
return "undefined";
}
if (Array.isArray(value)) {
// there's a small chance this recurssion gets out of hand for nested arrays
return arrayString(
value.map((value) => {
if (typeof value === "function") {
return functionString(value, true);
}
return print(value);
}),
);
}
if (typeof value.stack === "string") {
// assume this is an Error object and attempt to extract the line number
const lineNumber = errorLineNumber(value);
if (lineNumber !== undefined) {
return `${value.toString()} (on line ${lineNumber})`;
}
}
if (typeof value === "function") {
return functionString(value);
}
if (typeof value === "object") {
return simpleStringify(value);
}
return value.toString();
}

/**
* Combine any number of arguments into a single string converting them as needed.
*
* @param {any[]} args an array of any values, can be mixed types
* @returns {string}
*/
function combineArguments(args) {
return args.map(print).join(" ");
}

const originalClear = console.clear;
Expand All @@ -27,24 +181,24 @@
};

const originalLog = console.log;
console.log = function (d1) {
originalLog.apply(console, arguments);
console.log = function (...args) {
originalLog.apply(console, args);
window.parent.postMessage(
{
type: "consoleLog",
log: print(d1),
log: combineArguments(args),
},
"*",
);
};

const originalWarn = console.warn;
console.warn = function (d1) {
originalWarn.apply(console, arguments);
console.warn = function (...args) {
originalWarn.apply(console, args);
window.parent.postMessage(
{
type: "consoleWarn",
warn: defined(d1) ? d1.toString() : "undefined",
warn: combineArguments(args),
},
"*",
);
Expand All @@ -57,9 +211,9 @@
}

const originalError = console.error;
console.error = function (d1) {
originalError.apply(console, arguments);
if (!defined(d1)) {
console.error = function (...args) {
originalError.apply(console, args);
if (args.length === 0 || !defined(args[0])) {
window.parent.postMessage(
{
type: "consoleError",
Expand All @@ -70,58 +224,13 @@
return;
}

// Look for d1.stack, "bucket.html:line:char"
let lineNumber = -1;
const errorMsg = d1.toString();
if (typeof d1.stack === "string") {
const stack = d1.stack;
let pos = stack.indexOf(bucket);
if (pos < 0) {
pos = stack.indexOf("<anonymous>");
}
if (pos >= 0) {
const lineStart = stack.indexOf(":", pos);
if (lineStart > pos) {
let lineEnd1 = stack.indexOf(":", lineStart + 1);
const lineEnd2 = stack.indexOf("\n", lineStart + 1);
if (
lineEnd2 > lineStart &&
(lineEnd2 < lineEnd1 || lineEnd1 < lineStart)
) {
lineEnd1 = lineEnd2;
}
if (lineEnd1 > lineStart) {
/*eslint-disable no-empty*/
try {
lineNumber = parseInt(
stack.substring(lineStart + 1, lineEnd1),
10,
);
} catch (ex) {}
/*eslint-enable no-empty*/
}
}
}
}

if (lineNumber >= 0) {
window.parent.postMessage(
{
type: "consoleError",
error: errorMsg,
lineNumber: lineNumber,
},
"*",
);
} else {
window.parent.postMessage(
{
type: "consoleError",
error: errorMsg,
},
"*",
);
}
window.parent.postMessage(
{
type: "consoleError",
error: combineArguments(args),
},
"*",
);
};

window.onerror = function (errorMsg, url, lineNumber) {
Expand Down
15 changes: 4 additions & 11 deletions packages/sandcastle/src/Bucket.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,6 @@ function Bucket({
lastRunNumber.current = runNumber;
}, [code, html, runNumber]);

function scriptLineToEditorLine(line: number) {
// editor lines are zero-indexed, plus 3 lines of boilerplate
return line - 1;
}

useEffect(() => {
const messageHandler = function (e: MessageEvent<SandcastleMessage>) {
// The iframe (bucket.html) sends this message on load.
Expand All @@ -181,16 +176,14 @@ function Bucket({
} else if (e.data.type === "consoleError") {
// Console error messages from the iframe display in Sandcastle
let errorMsg = e.data.error;
let lineNumber = e.data.lineNumber;
const lineNumber = e.data.lineNumber;
if (lineNumber) {
errorMsg += " (on line ";
errorMsg += ` (on line ${lineNumber}`;

if (e.data.url) {
errorMsg += `${lineNumber} of ${e.data.url})`;
} else {
lineNumber = scriptLineToEditorLine(lineNumber);
errorMsg += `${lineNumber + 1})`;
errorMsg += ` of ${e.data.url}`;
}
errorMsg += ")";
}
appendConsole("error", errorMsg);
} else if (e.data.type === "consoleWarn") {
Expand Down