Playground: doc explorer (#10183)
This commit is contained in:
1 parent
ab72a2c11c
commit
b65cb6c1f6
13 files changed
+217
-24
No files matched your search
@@ -0,0 +1,3 @@
|
|||||||
|
#### "Doc Explorer" mode for the Playground (#10183 by @thorn0)
|
||||||
|
|
||||||
|
Switch the `parser` option to the special `doc-explorer` value to play with Prettier's intermediate representation and see how it's printed with different options.
|
||||||
@@ -9,4 +9,6 @@ The basic idea is that the printer takes an AST and returns an intermediate repr
|
|||||||
|
|
||||||
This means that most of the logic of printing an AST involves generating an abstract representation of the output involving certain commands. For example, `["(", line, arg, line, ")"]` would represent a concatenation of opening parens, an argument, and closing parens. But if that doesn’t fit on one line, the printer can break where `line` is specified.
|
This means that most of the logic of printing an AST involves generating an abstract representation of the output involving certain commands. For example, `["(", line, arg, line, ")"]` would represent a concatenation of opening parens, an argument, and closing parens. But if that doesn’t fit on one line, the printer can break where `line` is specified.
|
||||||
|
|
||||||
|
The [Playground](https://prettier.io/playground) has a special mode for exploring how Prettier’s intermediate representation is printed. To get there, open the sidebar (the "Show options" button) and set the `parser` option to the special value `doc-explorer`.
|
||||||
|
|
||||||
More (rough) details can be found in [commands.md](https://github.com/prettier/prettier/blob/main/commands.md).
|
More (rough) details can be found in [commands.md](https://github.com/prettier/prettier/blob/main/commands.md).
|
||||||
@@ -4,3 +4,7 @@
|
|||||||
[build.environment]
|
[build.environment]
|
||||||
NODE_VERSION = "lts/*"
|
NODE_VERSION = "lts/*"
|
||||||
YARN_VERSION = "1.22.15"
|
YARN_VERSION = "1.22.15"
|
||||||
|
[[headers]]
|
||||||
|
for = "/worker.js"
|
||||||
|
[headers.values]
|
||||||
|
Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-eval'"
|
||||||
+1
-1
@@ -96,7 +96,7 @@ function parse(text, opts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* istanbul ignore next */
|
/* istanbul ignore next */
|
||||||
throw error.stack;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,10 +67,13 @@ class Playground extends React.Component {
|
|||||||
options.parser = "babel";
|
options.parser = "babel";
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = original.content || getCodeSample(options.parser);
|
const codeSample = getCodeSample(options.parser);
|
||||||
|
const content = original.content || codeSample;
|
||||||
|
const needsClickForFirstRun =
|
||||||
|
options.parser === "doc-explorer" && content !== codeSample;
|
||||||
const selection = {};
|
const selection = {};
|
||||||
|
|
||||||
this.state = { content, options, selection };
|
this.state = { content, options, selection, needsClickForFirstRun };
|
||||||
|
|
||||||
this.handleOptionValueChange = this.handleOptionValueChange.bind(this);
|
this.handleOptionValueChange = this.handleOptionValueChange.bind(this);
|
||||||
|
|
||||||
@@ -80,14 +83,10 @@ class Playground extends React.Component {
|
|||||||
this.setSelection = (selection) => this.setState({ selection });
|
this.setSelection = (selection) => this.setState({ selection });
|
||||||
this.setSelectionAsRange = () => {
|
this.setSelectionAsRange = () => {
|
||||||
const { selection, content, options } = this.state;
|
const { selection, content, options } = this.state;
|
||||||
const { head, anchor } = selection;
|
const [rangeStart, rangeEnd] = util.convertSelectionToRange(
|
||||||
const range = [head, anchor].map(
|
selection,
|
||||||
({ ch, line }) =>
|
content
|
||||||
content.split("\n").slice(0, line).join("\n").length +
|
|
||||||
ch +
|
|
||||||
(line ? 1 : 0)
|
|
||||||
);
|
);
|
||||||
const [rangeStart, rangeEnd] = range.sort((a, b) => a - b);
|
|
||||||
const updatedOptions = { ...options, rangeStart, rangeEnd };
|
const updatedOptions = { ...options, rangeStart, rangeEnd };
|
||||||
if (rangeStart === rangeEnd) {
|
if (rangeStart === rangeEnd) {
|
||||||
delete updatedOptions.rangeStart;
|
delete updatedOptions.rangeStart;
|
||||||
@@ -103,6 +102,8 @@ class Playground extends React.Component {
|
|||||||
this.rangeEndOption = props.availableOptions.find(
|
this.rangeEndOption = props.availableOptions.find(
|
||||||
(opt) => opt.name === "rangeEnd"
|
(opt) => opt.name === "rangeEnd"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.handleInputPanelFormat = this.handleInputPanelFormat.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(_, prevState) {
|
componentDidUpdate(_, prevState) {
|
||||||
@@ -131,6 +132,10 @@ class Playground extends React.Component {
|
|||||||
? getCodeSample(options.parser)
|
? getCodeSample(options.parser)
|
||||||
: state.content;
|
: state.content;
|
||||||
|
|
||||||
|
if (option.name === "parser") {
|
||||||
|
state.needsClickForFirstRun = false;
|
||||||
|
}
|
||||||
|
|
||||||
return { options, content };
|
return { options, content };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -158,6 +163,30 @@ class Playground extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleInputPanelFormat() {
|
||||||
|
if (this.state.options.parser !== "doc-explorer") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { content, selection } = this.state;
|
||||||
|
|
||||||
|
return this.props.worker
|
||||||
|
.format(content, {
|
||||||
|
parser: "__js_expression",
|
||||||
|
cursorOffset: util.convertSelectionToRange(selection, content)[0],
|
||||||
|
})
|
||||||
|
.then(({ error, formatted, cursorOffset }) => {
|
||||||
|
if (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: formatted,
|
||||||
|
cursor: util.convertOffsetToPosition(cursorOffset, formatted),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { worker, version } = this.props;
|
const { worker, version } = this.props;
|
||||||
const { content, options } = this.state;
|
const { content, options } = this.state;
|
||||||
@@ -171,6 +200,7 @@ class Playground extends React.Component {
|
|||||||
<EditorState>
|
<EditorState>
|
||||||
{(editorState) => (
|
{(editorState) => (
|
||||||
<PrettierFormat
|
<PrettierFormat
|
||||||
|
enabled={!this.state.needsClickForFirstRun}
|
||||||
worker={worker}
|
worker={worker}
|
||||||
code={content}
|
code={content}
|
||||||
options={options}
|
options={options}
|
||||||
@@ -259,9 +289,10 @@ class Playground extends React.Component {
|
|||||||
checked={editorState.showSecondFormat}
|
checked={editorState.showSecondFormat}
|
||||||
onChange={editorState.toggleSecondFormat}
|
onChange={editorState.toggleSecondFormat}
|
||||||
/>
|
/>
|
||||||
{editorState.showDoc && debug.doc && (
|
{editorState.showDoc && (
|
||||||
<ClipboardButton
|
<ClipboardButton
|
||||||
copy={() => this.getMarkdown({ doc: debug.doc })}
|
copy={() => this.getMarkdown({ doc: debug.doc })}
|
||||||
|
disabled={!debug.doc}
|
||||||
>
|
>
|
||||||
Copy doc
|
Copy doc
|
||||||
</ClipboardButton>
|
</ClipboardButton>
|
||||||
@@ -284,6 +315,7 @@ class Playground extends React.Component {
|
|||||||
overlayEnd={options.rangeEnd}
|
overlayEnd={options.rangeEnd}
|
||||||
onChange={this.setContent}
|
onChange={this.setContent}
|
||||||
onSelectionChange={this.setSelection}
|
onSelectionChange={this.setSelection}
|
||||||
|
onFormat={this.handleInputPanelFormat}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{editorState.showAst ? (
|
{editorState.showAst ? (
|
||||||
@@ -302,11 +334,36 @@ class Playground extends React.Component {
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{editorState.showOutput ? (
|
{editorState.showOutput ? (
|
||||||
<OutputPanel
|
this.state.needsClickForFirstRun ? (
|
||||||
mode={util.getCodemirrorMode(options.parser)}
|
<div className="editor disabled-output-panel">
|
||||||
value={formatted}
|
<div className="explanation">
|
||||||
ruler={options.printWidth}
|
<code>doc-explorer</code> involves running code
|
||||||
/>
|
provided by users.
|
||||||
|
</div>
|
||||||
|
<div className="explanation">
|
||||||
|
To stay on the safe side and prevent abuse, an
|
||||||
|
explicit user action is required when a direct
|
||||||
|
link to a <code>doc-explorer</code> playground is
|
||||||
|
opened.
|
||||||
|
</div>
|
||||||
|
<div className="explanation">
|
||||||
|
Click the button below to start the playground.
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
this.setState({ needsClickForFirstRun: false })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<OutputPanel
|
||||||
|
mode={util.getCodemirrorMode(options.parser)}
|
||||||
|
value={formatted}
|
||||||
|
ruler={options.printWidth}
|
||||||
|
/>
|
||||||
|
)
|
||||||
) : null}
|
) : null}
|
||||||
{editorState.showSecondFormat ? (
|
{editorState.showSecondFormat ? (
|
||||||
<OutputPanel
|
<OutputPanel
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export default class PrettierFormat extends React.Component {
|
|||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
for (const key of [
|
for (const key of [
|
||||||
|
"enabled",
|
||||||
"code",
|
"code",
|
||||||
"options",
|
"options",
|
||||||
"debugAst",
|
"debugAst",
|
||||||
@@ -28,6 +29,7 @@ export default class PrettierFormat extends React.Component {
|
|||||||
|
|
||||||
format() {
|
format() {
|
||||||
const {
|
const {
|
||||||
|
enabled,
|
||||||
worker,
|
worker,
|
||||||
code,
|
code,
|
||||||
options,
|
options,
|
||||||
@@ -37,6 +39,10 @@ export default class PrettierFormat extends React.Component {
|
|||||||
reformat,
|
reformat,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
worker
|
worker
|
||||||
.format(code, options, { ast, doc, comments, reformat })
|
.format(code, options, { ast, doc, comments, reformat })
|
||||||
.then((result) => this.setState(result));
|
.then((result) => this.setState(result));
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export default function WorkerApi(source) {
|
|||||||
getMetadata() {
|
getMetadata() {
|
||||||
return postMessage({ type: "meta" });
|
return postMessage({ type: "meta" });
|
||||||
},
|
},
|
||||||
format(code, options, debug) {
|
format(code, options, debug = {}) {
|
||||||
return postMessage({ type: "format", code, options, debug });
|
return postMessage({ type: "format", code, options, debug });
|
||||||
},
|
},
|
||||||
postMessage,
|
postMessage,
|
||||||
|
|||||||
@@ -308,6 +308,24 @@ export default function getCodeSamples(parser) {
|
|||||||
" </body>",
|
" </body>",
|
||||||
"</HTML>",
|
"</HTML>",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
case "doc-explorer":
|
||||||
|
return [
|
||||||
|
"group([",
|
||||||
|
' "(*",',
|
||||||
|
" indent([",
|
||||||
|
" line,",
|
||||||
|
' "Prettier:",',
|
||||||
|
" line,",
|
||||||
|
' "Opinionated,",',
|
||||||
|
" line,",
|
||||||
|
' "Code,",',
|
||||||
|
" line,",
|
||||||
|
' "Formatter",',
|
||||||
|
" ]),",
|
||||||
|
" line,",
|
||||||
|
' "*)",',
|
||||||
|
"])",
|
||||||
|
].join("\n");
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ function getMarkdownSyntax(options) {
|
|||||||
case "acorn":
|
case "acorn":
|
||||||
case "espree":
|
case "espree":
|
||||||
case "meriyah":
|
case "meriyah":
|
||||||
|
case "doc-explorer":
|
||||||
return "jsx";
|
return "jsx";
|
||||||
case "babel-ts":
|
case "babel-ts":
|
||||||
case "typescript":
|
case "typescript":
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class CodeMirrorPanel extends React.Component {
|
|||||||
delete options.rulerColor;
|
delete options.rulerColor;
|
||||||
delete options.value;
|
delete options.value;
|
||||||
delete options.onChange;
|
delete options.onChange;
|
||||||
|
delete options.onFormat;
|
||||||
|
|
||||||
options.rulers = [makeRuler(this.props)];
|
options.rulers = [makeRuler(this.props)];
|
||||||
|
|
||||||
@@ -26,6 +27,12 @@ class CodeMirrorPanel extends React.Component {
|
|||||||
options.gutters = ["CodeMirror-linenumbers", "CodeMirror-foldgutter"];
|
options.gutters = ["CodeMirror-linenumbers", "CodeMirror-foldgutter"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.props.onFormat) {
|
||||||
|
options.extraKeys = {
|
||||||
|
"Shift-Alt-F": this.handleFormat.bind(this),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
this._codeMirror = CodeMirror.fromTextArea(
|
this._codeMirror = CodeMirror.fromTextArea(
|
||||||
this._textareaRef.current,
|
this._textareaRef.current,
|
||||||
options
|
options
|
||||||
@@ -112,6 +119,17 @@ class CodeMirrorPanel extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleFormat() {
|
||||||
|
const result = this.props.onFormat();
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Promise.resolve(result).then(({ value, cursor }) => {
|
||||||
|
this.props.onChange(value);
|
||||||
|
this._codeMirror.setCursor(cursor);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="editor input">
|
<div className="editor input">
|
||||||
|
|||||||
@@ -95,6 +95,30 @@ export function getAstAutoFold(parser) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function convertSelectionToRange({ head, anchor }, content) {
|
||||||
|
const lines = content.split("\n");
|
||||||
|
return [head, anchor]
|
||||||
|
.map(
|
||||||
|
({ ch, line }) =>
|
||||||
|
lines.slice(0, line).join("\n").length + ch + (line ? 1 : 0)
|
||||||
|
)
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertOffsetToPosition(offset, content) {
|
||||||
|
let line = 0;
|
||||||
|
let ch = 0;
|
||||||
|
for (let i = 0; i < offset && i <= content.length; i++) {
|
||||||
|
if (content[i] === "\n") {
|
||||||
|
line++;
|
||||||
|
ch = 0;
|
||||||
|
} else {
|
||||||
|
ch++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { line, ch };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copied from https://github.com/prettier/prettier/blob/6fe21780115cf5f74f83876d64b03a727fbab220/src/cli/utils.js#L6-L27
|
* Copied from https://github.com/prettier/prettier/blob/6fe21780115cf5f74f83876d64b03a727fbab220/src/cli/utils.js#L6-L27
|
||||||
* @template Obj
|
* @template Obj
|
||||||
|
|||||||
@@ -112,6 +112,25 @@ header h1 {
|
|||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disabled-output-panel {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled-output-panel .explanation {
|
||||||
|
font-size: 16px;
|
||||||
|
max-width: 400px;
|
||||||
|
color: darkred;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled-output-panel .btn {
|
||||||
|
font-size: 22px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
/* display as 2x2 grid */
|
/* display as 2x2 grid */
|
||||||
@media (min-width: 800px) {
|
@media (min-width: 800px) {
|
||||||
.options-container {
|
.options-container {
|
||||||
@@ -258,7 +277,7 @@ input[type="number"] {
|
|||||||
border-color: #c8e1ff;
|
border-color: #c8e1ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:hover {
|
.btn:hover:enabled {
|
||||||
background-color: #e6ebf1;
|
background-color: #e6ebf1;
|
||||||
background-image: linear-gradient(to bottom, #f0f3f6, #dce3ec);
|
background-image: linear-gradient(to bottom, #f0f3f6, #dce3ec);
|
||||||
border-color: #afb1b2;
|
border-color: #afb1b2;
|
||||||
@@ -271,6 +290,10 @@ input[type="number"] {
|
|||||||
box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15);
|
box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
color: #afb1b2;
|
||||||
|
}
|
||||||
|
|
||||||
.tooltip {
|
.tooltip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 6;
|
z-index: 6;
|
||||||
|
|||||||
@@ -30,6 +30,25 @@ for (const file in parsersLocation) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const docExplorerPlugin = {
|
||||||
|
parsers: {
|
||||||
|
"doc-explorer": {
|
||||||
|
parse: (text) =>
|
||||||
|
new Function(
|
||||||
|
`{ ${Object.keys(prettier.doc.builders)} }`,
|
||||||
|
`const result = (${text || "''"}\n); return result;`
|
||||||
|
)(prettier.doc.builders),
|
||||||
|
astFormat: "doc-explorer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
printers: {
|
||||||
|
"doc-explorer": {
|
||||||
|
print: (path) => path.getValue(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
languages: [{ name: "doc-explorer", parsers: ["doc-explorer"] }],
|
||||||
|
};
|
||||||
|
|
||||||
self.onmessage = function (event) {
|
self.onmessage = function (event) {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
uid: event.data.uid,
|
uid: event.data.uid,
|
||||||
@@ -45,6 +64,8 @@ function serializeAst(ast) {
|
|||||||
? { name: value.name, message: value.message, ...value }
|
? { name: value.name, message: value.message, ...value }
|
||||||
: typeof value === "bigint"
|
: typeof value === "bigint"
|
||||||
? `BigInt('${String(value)}')`
|
? `BigInt('${String(value)}')`
|
||||||
|
: typeof value === "symbol"
|
||||||
|
? String(value)
|
||||||
: value,
|
: value,
|
||||||
2
|
2
|
||||||
);
|
);
|
||||||
@@ -58,6 +79,7 @@ function handleMessage(message) {
|
|||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
prettier.getSupportInfo({
|
prettier.getSupportInfo({
|
||||||
showUnreleased: true,
|
showUnreleased: true,
|
||||||
|
plugins: [docExplorerPlugin],
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@@ -72,13 +94,15 @@ function handleMessage(message) {
|
|||||||
delete options.doc;
|
delete options.doc;
|
||||||
delete options.output2;
|
delete options.output2;
|
||||||
|
|
||||||
const plugins = [{ parsers }];
|
const plugins = [{ parsers }, docExplorerPlugin];
|
||||||
options.plugins = plugins;
|
options.plugins = plugins;
|
||||||
|
|
||||||
const formatResult = formatCode(message.code, options);
|
const formatResult = formatCode(message.code, options);
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
formatted: formatResult.formatted,
|
formatted: formatResult.formatted,
|
||||||
|
cursorOffset: formatResult.cursorOffset,
|
||||||
|
error: formatResult.error,
|
||||||
debug: {
|
debug: {
|
||||||
ast: null,
|
ast: null,
|
||||||
doc: null,
|
doc: null,
|
||||||
@@ -111,10 +135,10 @@ function handleMessage(message) {
|
|||||||
try {
|
try {
|
||||||
response.debug.doc = prettier.__debug.formatDoc(
|
response.debug.doc = prettier.__debug.formatDoc(
|
||||||
prettier.__debug.printToDoc(message.code, options),
|
prettier.__debug.printToDoc(message.code, options),
|
||||||
{ parser: "babel", plugins }
|
{ plugins }
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch {
|
||||||
response.debug.doc = String(e);
|
response.debug.doc = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,10 +166,23 @@ function formatCode(text, options) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.constructor && e.constructor.name === "SyntaxError") {
|
if (e.constructor && e.constructor.name === "SyntaxError") {
|
||||||
// Likely something wrong with the user's code
|
// Likely something wrong with the user's code
|
||||||
return { formatted: String(e) };
|
return { formatted: String(e), error: true };
|
||||||
}
|
}
|
||||||
// Likely a bug in Prettier
|
// Likely a bug in Prettier
|
||||||
// Provide the whole stack for debugging
|
// Provide the whole stack for debugging
|
||||||
return { formatted: e.stack || String(e) };
|
return { formatted: stringifyError(e), error: true };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stringifyError(e) {
|
||||||
|
const stringified = String(e);
|
||||||
|
if (typeof e.stack !== "string") {
|
||||||
|
return stringified;
|
||||||
|
}
|
||||||
|
if (e.stack.includes(stringified)) {
|
||||||
|
// Chrome
|
||||||
|
return e.stack;
|
||||||
|
}
|
||||||
|
// Firefox
|
||||||
|
return stringified + "\n" + e.stack;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user