Add Astro <Debug/> component (#675)

* Initial MVP Debug component

* Document the prettifying of the input

* Just make `<Debug/>` a wrapper around `<Prism/>` lol

* feat: add details/summary debug component

* chore: remove Props (unused)

* fix: prefer `div` to semantic elements

* chore: format

* fix: prop-drill `class` into components

* fix: ensure `astro/components` are evaluated lazily

* feat(debug): export debug component from `astro/debug`

* fix: minimal example local snowpack config

* docs: add debugging docs

* chore: add changeset

* docs: update debug docs

Co-authored-by: Nate Moore <nate@skypack.dev>
This commit is contained in:
Caleb Jasik 2021-08-23 15:43:22 -05:00 committed by GitHub
parent 2fd004dcd9
commit efb41f22c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 350 additions and 0 deletions

View file

@ -0,0 +1,14 @@
---
'astro': patch
---
Add `<Debug>` component for JavaScript-free client-side debugging.
```astro
---
import Debug from 'astro/debug';
const obj = { /* ... */ }
---
<Debug {obj} />
```

View file

@ -18,6 +18,7 @@ export const SIDEBAR = {
{ text: 'Guides', header: true }, { text: 'Guides', header: true },
{ text: 'Styling & CSS', link: 'guides/styling' }, { text: 'Styling & CSS', link: 'guides/styling' },
{ text: 'Markdown', link: 'guides/markdown-content' }, { text: 'Markdown', link: 'guides/markdown-content' },
{ text: 'Debugging', link: 'guides/debugging' },
{ text: 'Data Fetching', link: 'guides/data-fetching' }, { text: 'Data Fetching', link: 'guides/data-fetching' },
{ text: 'Pagination', link: 'guides/pagination' }, { text: 'Pagination', link: 'guides/pagination' },
{ text: 'RSS', link: 'guides/rss' }, { text: 'RSS', link: 'guides/rss' },

View file

@ -0,0 +1,6 @@
---
layout: ~/layouts/MainLayout.astro
title: Debugging
---
Astro runs on the server and logs directly to your terminal, so it can be difficult to debug values from Astro. Astro's built-in `<Debug>` component can help you inspect values inside your files on the clientside. Read more about the [built-in Debug Component](/reference/builtin-components#debug-).

View file

@ -30,3 +30,22 @@ import { Prism } from 'astro/components';
``` ```
This component provides syntax highlighting for code blocks. Since this never changes in the client it makes sense to use an Astro component (it's equally reasonable to use a framework component for this kind of thing; Astro is server-only by default for all frameworks!). This component provides syntax highlighting for code blocks. Since this never changes in the client it makes sense to use an Astro component (it's equally reasonable to use a framework component for this kind of thing; Astro is server-only by default for all frameworks!).
## `<Debug />`
```astro
---
import { Debug } from 'astro/debug';
const serverObject = {
a: 0,
b: "string",
c: {
nested: "object"
}
}
---
<Debug {serverObject} />
```
This component provides a way to inspect values on the clientside, without any JavaScript.

View file

@ -9,5 +9,8 @@
}, },
"devDependencies": { "devDependencies": {
"astro": "^0.19.2" "astro": "^0.19.2"
},
"snowpack": {
"workspaceRoot": "../.."
} }
} }

View file

@ -0,0 +1,306 @@
---
const key = Object.keys(Astro.props)[0];
const value = Astro.props[key];
const getType = (node: unknown) => {
if (Array.isArray(node)) return 'array';
if (node === null) return 'null';
if (typeof node === 'object') {
if ((node as any).then) return 'promise';
}
return typeof node;
};
const getSummary = (node: any, key: string, className: string) => {
const type = getType(node);
let value;
let open;
let close;
if (type === 'function') {
return <>
{(key || !key && key === 0) && <><span class={`${className} key`}>{key}</span><span class={`${className} sep`}>:</span></>}
<span class={`${className} value value-function`}>{node.name}<span class={`${className} punc`}>()</span></span>
</>
}
if (type === 'promise') {
return <>
{(key || !key && key === 0) && <><span class={`${className} key`}>{key}</span><span class={`${className} sep`}>:</span></>}
<span class={`${className} value value-promise`}>Promise</span>
</>
}
if (type === 'array') {
value = node.length;
open = <><span class={`${className} none`}>Array</span>{'['}</>;
close = ']';
} else if (type === 'object') {
const keys = Object.keys(node);
if (keys.length === 0) {
value = 'Empty';
} else if (keys.length > 3) {
value = '…';
} else {
value = keys.slice(0, 3).join(',');
}
open = '{';
close = '}';
};
return <>
{key && <><span class={`${className} key`}>{key}</span>: </>}
{open && <span class={`${className} punc`}>{open}</span>}
<span class={`${className} hide`}>
<span class={`${className} len`}>{value}</span>
{close && <span class={`${className} punc`}>{close}</span>}
</span>
</>;
};
const Details = ({ node, key, children, class: className }) => {
const type = getType(node);
const props = {};
if (type === 'array' || type === 'object') {
props['data-char'] = type === 'array' ? ']' : '}'
props.open = !key && type === 'object' ? '' : undefined;
}
return (
<details {...props} class={className}>
<Summary node={node} key={key} class={className} />
{children}
</details>
);
}
const Summary = ({ node, key, class: className }) => {
return (
<summary class={className}>{getSummary(node, key, className)}</summary>
);
}
const Empty = Symbol('Empty');
const KeyValue = ({ key, value, dim, class: className }) => {
let type = key === '__proto__' ? 'prototype' : getType(value);
if (type === 'null') {
value = 'null';
} else if (type === 'undefined') {
value = 'undefined';
} else if (value === Empty) {
type = 'empty';
value = 'Empty';
} else {
value = JSON.stringify(value);
}
return (
<div class={`${className} line`}>
{(key || !key && key === 0) && <><span class={`${className} key ${dim ? 'key-dim' : ''}`.trim()}>{key}</span><span class={`${className} sep`}>:</span></>}
<span class={`${className} value value-${type}`}>
{value}
</span>
</div>
)
}
const Node = ({ node, key, class: className, ...props }) => {
const type = getType(node);
className = className.replace(/debug-value/g, '');
if (type === 'array' || type === 'object') {
let children = [];
if (type === 'array' && node.length > 0 && Object.entries(node).length === 0) {
children = Array.from({ length: node.length }, (_, key) => <Node node={Empty} key={key} class={className} />);
} else {
children = Object.entries(node).map(([key, value]) => <Node node={value} key={key} class={className} />);
}
return (
<Details node={node} key={key} children={children} class={className} />
);
} else if (type === 'function') {
return (
<Details node={node} key={key} class={className} children={
<>
<KeyValue key="name" value={node.name} dim={true} class={className} />
<KeyValue key="__proto__" value="Function" dim={true} class={className} />
</>
}/>
);
} else if (type === 'promise') {
return (
<Details node={node} key={key} class={className} children={
<KeyValue key="__proto__" value="Promise" dim={true} />
} />
);
}
return <KeyValue key={key} value={node} class={className} />;
}
---
<div class="debug">
<div class="debug-header">
<h2 class="debug-title"><span class="debug-label">Debug</span> <span class="debug-name">"{key}"</span></h2>
</div>
<main>
<Node node={value} class="debug-value" />
</main>
</div>
<style lang="scss">
.debug-header {
background: #FF1639;
margin: -1rem -1.5rem 1rem;
padding: 0.25rem 0.75rem;
}
.debug-title {
font-size: 1em;
color: #fff;
margin: 0.5em 0;
}
.debug-label {
font-weight: bold;
text-transform: uppercase;
margin-right: 0.75em;
}
.debug {
all: initial;
display: flex;
flex-direction: column;
padding: 1rem 1.5rem;
overflow-y: hidden;
overflow-x: auto;
border: 1px solid #FFCFD6;
background: #FFF;
font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace;
font-size: 0.8rem;
line-height: 1.44;
color: #6b7280;
white-space: pre;
}
details[open] > summary span.hide {
visibility: hidden;
}
details[open] > summary span.none {
display: none;
}
details > details {
padding-left: 1em;
}
.line {
padding-left: 1.125em;
}
details[open]::after {
content: attr(data-char);
}
.sep {
margin-right: 0.25em;
}
details > summary {
cursor: pointer;
}
details:hover > summary::before,
details:focus > summary::before {
transform: translate(0.25em, -0.25em);
}
details > summary::before {
content: '';
background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M13 8L2.60769 14L2.6077 2L13 8Z' fill='%236B7280' /%3E%3C/svg%3E%0A");
background-size: 1em;
display: inline-flex;
font-size: 0.5em;
width: 1em;
height: 1em;
margin-right: 1.25em;
margin-left: -2em;
transform: translate(0, -0.25em);
line-height: 1em;
transition: transform 120ms ease-in;
}
details[open] > summary::before {
transform: translate(0.25em, -0.25em) rotate(90deg);
}
.debug :global(::marker) {
content: '';
width: 0;
visibility: hidden;
}
.key {
color: #882de7;
}
.key-dim {
color: #B881F1;
}
.len {
color: #B881F1;
}
details:hover > summary,
details:focus > summary,
details:hover > summary .punc,
details:focus > summary .punc,
details:hover[open]::after,
details:focus[open]::after {
color: #000012;
}
details:hover > summary .len,
details:focus > summary .len {
color: #882DE7;
}
.punc {
color: #6b7280;
}
.value-string {
color: #17c083;
}
.value-function::before {
content: 'ƒ ';
color: #3894ff;
}
.value-function {
color: #5076f9;
}
.value-number {
color: #ff5d01;
}
.value-null,
.value-undefined {
color: #9ca3af;
}
main > .line {
margin-left: -0.75em;
padding-left: 0;
}
</style>

View file

@ -16,6 +16,7 @@
"./snowpack-plugin": "./snowpack-plugin.cjs", "./snowpack-plugin": "./snowpack-plugin.cjs",
"./snowpack-plugin-jsx": "./snowpack-plugin-jsx.cjs", "./snowpack-plugin-jsx": "./snowpack-plugin-jsx.cjs",
"./components": "./components/index.js", "./components": "./components/index.js",
"./debug": "./components/Debug.astro",
"./components/*": "./components/*", "./components/*": "./components/*",
"./runtime/svelte": "./dist/frontend/runtime/svelte.js", "./runtime/svelte": "./dist/frontend/runtime/svelte.js",
"./internal/*": "./dist/internal/*", "./internal/*": "./dist/internal/*",