mirror of
https://github.com/denoland/deno.git
synced 2024-11-21 15:04:11 -05:00
feat: serialize JSX for console
This commit is contained in:
parent
5edd102f3f
commit
d1677a1a8f
5 changed files with 531 additions and 1 deletions
|
@ -1,6 +1,11 @@
|
|||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
/// <reference path="../../core/internal.d.ts" />
|
||||
import {
|
||||
preactAdapter,
|
||||
reactAdapter,
|
||||
serialize,
|
||||
} from "ext:deno_console/02_jsx.js";
|
||||
|
||||
const core = globalThis.Deno.core;
|
||||
const internals = globalThis.__bootstrap.internals;
|
||||
|
@ -169,6 +174,10 @@ const styles = {
|
|||
regexp: "red",
|
||||
module: "underline",
|
||||
internalError: "red",
|
||||
jsxElement: "green",
|
||||
jsxComponent: "magenta",
|
||||
jsxAttribute: "yellow",
|
||||
jsxOther: "reset",
|
||||
};
|
||||
|
||||
const defaultFG = 39;
|
||||
|
@ -919,12 +928,26 @@ function formatRaw(ctx, value, recurseTimes, typedArray, proxyDetails) {
|
|||
if (noIterator) {
|
||||
keys = getKeys(value, ctx.showHidden);
|
||||
braces = ["{", "}"];
|
||||
|
||||
// Preact JSX
|
||||
if (
|
||||
constructor === undefined && value !== null && typeof value === "object"
|
||||
) {
|
||||
return serialize(ctx, preactAdapter, value, ctx.indentationLvl, 10);
|
||||
}
|
||||
|
||||
if (constructor === "Object") {
|
||||
if (isArgumentsObject(value)) {
|
||||
braces[0] = "[Arguments] {";
|
||||
} else if (tag !== "") {
|
||||
braces[0] = `${getPrefix(constructor, tag, "Object")}{`;
|
||||
}
|
||||
|
||||
// React JSX
|
||||
if ("$$typeof" in value && typeof value.$$typeof === "symbol") {
|
||||
return serialize(ctx, reactAdapter, value, ctx.indentationLvl, 10);
|
||||
}
|
||||
|
||||
if (keys.length === 0 && protoProps === undefined) {
|
||||
return `${braces[0]}}`;
|
||||
}
|
||||
|
|
431
ext/console/02_jsx.js
Normal file
431
ext/console/02_jsx.js
Normal file
|
@ -0,0 +1,431 @@
|
|||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
/// <reference path="../../core/internal.d.ts" />
|
||||
|
||||
const primordials = globalThis.__bootstrap.primordials;
|
||||
const {
|
||||
Array,
|
||||
ArrayIsArray,
|
||||
ArrayPrototypePush,
|
||||
ArrayPrototypePushApply,
|
||||
ObjectKeys,
|
||||
String,
|
||||
StringPrototypeRepeat,
|
||||
StringPrototypeSlice,
|
||||
SymbolFor,
|
||||
} = primordials;
|
||||
|
||||
/**
|
||||
* React adapter to serialize JSX to string
|
||||
* @type {import("ext:deno_console/jsx").JSXSerializeAdapter}
|
||||
*/
|
||||
export const reactAdapter = {
|
||||
getName(vnode) {
|
||||
if (typeof vnode.type === "string") return vnode.type;
|
||||
if (typeof vnode.type === "function") {
|
||||
return vnode.type.displayName || vnode.type.name || "Anonymous";
|
||||
}
|
||||
if (vnode.type == SymbolFor("react.fragment")) return "Fragment";
|
||||
if (vnode.type == SymbolFor("react.suspense")) return "Suspense";
|
||||
if (vnode.type == SymbolFor("react.strict_mode")) return "StrictMode";
|
||||
if (vnode.type == SymbolFor("react.profiler")) return "Profiler";
|
||||
|
||||
if (
|
||||
vnode.type !== null &&
|
||||
typeof vnode.type === "object" &&
|
||||
typeof vnode.type.$$typeof === "symbol"
|
||||
) {
|
||||
const $$typeof = vnode.type.$$typeof;
|
||||
if ($$typeof === SymbolFor("react.provider")) {
|
||||
const suffix = vnode.type.displayName
|
||||
? "." + vnode.type.displayName
|
||||
: "";
|
||||
return `Provider${suffix}`;
|
||||
} else if ($$typeof === SymbolFor("react.context")) {
|
||||
const suffix = vnode.type.displayName
|
||||
? "." + vnode.type.displayName
|
||||
: "";
|
||||
return `Consumer${suffix}`;
|
||||
} else if (
|
||||
$$typeof === SymbolFor("react.memo") &&
|
||||
typeof vnode.type.type === "function"
|
||||
) {
|
||||
const fnName = vnode.type.type.displayName || vnode.type.type.name ||
|
||||
"";
|
||||
return `Memo${fnName ? `(${fnName})` : ""}`;
|
||||
} else if ($$typeof === SymbolFor("react.forward_ref")) {
|
||||
const name = vnode.type.render.displayName ||
|
||||
vnode.type.render.name ||
|
||||
"Anonymous";
|
||||
return `ForwardRef(${name})`;
|
||||
}
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
},
|
||||
getTextIfTextNode() {
|
||||
// React doesn't have proper text elements afaik
|
||||
return null;
|
||||
},
|
||||
isFragment(vnode) {
|
||||
return vnode.type === SymbolFor("react.fragment");
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Preact adapter to serialize JSX to string
|
||||
* @type {import("ext:deno_console/jsx").JSXSerializeAdapter}
|
||||
*/
|
||||
export const preactAdapter = {
|
||||
getName(vnode) {
|
||||
if (typeof vnode.type === "string") return vnode.type;
|
||||
if (typeof vnode.type === "function") {
|
||||
const name = vnode.type.displayName || vnode.type.name || "Anonymous";
|
||||
// TODO: Fragments can only be detected by importing them, this is
|
||||
// a hacky workaround
|
||||
if (name === "k") return "Fragment";
|
||||
|
||||
if ("contextType" in vnode.type) {
|
||||
const ct = vnode.type.contextType;
|
||||
const name = vnode.type === ct.Consumer ? "Consumer" : "Provider";
|
||||
const suffix = ct.displayName ? `.${ct.displayName}` : "";
|
||||
return name + suffix;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
return "Unknown";
|
||||
},
|
||||
getTextIfTextNode(vnode) {
|
||||
return vnode.type === null ? String(vnode.props.data) : null;
|
||||
},
|
||||
isFragment(vnode) {
|
||||
// TODO: Fragments can only be detected by importing them, this is
|
||||
// a hacky workaround
|
||||
return typeof vnode.type === "function" && vnode.type.name === "k";
|
||||
},
|
||||
isValidElement(value) {
|
||||
return value !== null && typeof value === "object";
|
||||
},
|
||||
};
|
||||
|
||||
function isVNode(x) {
|
||||
return (
|
||||
x !== null &&
|
||||
typeof x === "object" &&
|
||||
"type" in x &&
|
||||
"props" in x &&
|
||||
"key" in x
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} n
|
||||
* @returns {string}
|
||||
*/
|
||||
function indent(n) {
|
||||
if (n === 0) return "";
|
||||
return StringPrototypeRepeat(" ", n);
|
||||
}
|
||||
|
||||
const OTHER = "jsxOther";
|
||||
const ELEMENT = "jsxElement";
|
||||
const COMPONENT = "jsxComponent";
|
||||
const SPECIAL = "special";
|
||||
const ATTR = "jsxAttribute";
|
||||
|
||||
/**
|
||||
* Serialize JSX to a pretty formatted string
|
||||
* @param {{ stylize: (str: string, color: string) => string }} ctx
|
||||
* @param {import("ext:deno_console/jsx").JSXSerializeAdapter} adapter
|
||||
* @param {import("ext:deno_console/jsx").SharedVNode} vnode
|
||||
* @param {number} level
|
||||
* @param {number} limit
|
||||
* @returns {string}
|
||||
*/
|
||||
export function serialize(ctx, adapter, vnode, level, limit) {
|
||||
const space = indent(level);
|
||||
|
||||
const text = adapter.getTextIfTextNode(vnode);
|
||||
if (text !== null) {
|
||||
return space + text;
|
||||
}
|
||||
|
||||
const isKeyed = vnode.key !== null && vnode.key !== undefined;
|
||||
|
||||
const isFragment = adapter.isFragment(vnode);
|
||||
const isDomNode = !isFragment && typeof vnode.type === "string";
|
||||
let namePretty;
|
||||
|
||||
let TAGS = ELEMENT;
|
||||
// Fragments are a special case since they have special nameless
|
||||
// syntax `<>`. They can only have a `key` prop in React, but Preact
|
||||
// doesn't have that restriction
|
||||
if (isFragment) {
|
||||
TAGS = isKeyed ? COMPONENT : ELEMENT;
|
||||
namePretty = isKeyed ? ctx.stylize("Fragment", COMPONENT) : "";
|
||||
} else {
|
||||
const name = adapter.getName(vnode);
|
||||
// Preact text node
|
||||
if (name === null) {
|
||||
return space + vnode.props.data;
|
||||
}
|
||||
|
||||
if (isDomNode) {
|
||||
TAGS = ELEMENT;
|
||||
namePretty = ctx.stylize(name, ELEMENT);
|
||||
} else {
|
||||
TAGS = COMPONENT;
|
||||
namePretty = ctx.stylize(name, COMPONENT);
|
||||
}
|
||||
}
|
||||
|
||||
let out = space + ctx.stylize("<", TAGS) + namePretty;
|
||||
|
||||
if (isKeyed) {
|
||||
const value = ctx.stylize(`"${String(vnode.key)}"`, "string");
|
||||
out += ` ${ctx.stylize("key", ATTR)}${ctx.stylize("=", OTHER)}${value}`;
|
||||
}
|
||||
|
||||
out += serializeProps(
|
||||
ctx,
|
||||
adapter,
|
||||
vnode.props,
|
||||
isDomNode,
|
||||
level + 1 > limit,
|
||||
);
|
||||
|
||||
/** @type {import("ext:deno_console/jsx").NormalizedChild} */
|
||||
const children = [];
|
||||
/** @type {import("ext:deno_console/jsx").VElement} */
|
||||
const rawChildren = vnode.props.children;
|
||||
normalizeChildren(children, rawChildren, 0);
|
||||
|
||||
if (children.length > 0) {
|
||||
const singleChild = !ArrayIsArray(vnode.props.children);
|
||||
|
||||
out += ctx.stylize(">", TAGS);
|
||||
|
||||
// Single text child may be formatted in same line
|
||||
if (level + 1 > limit) {
|
||||
if (children.length > 0) {
|
||||
out += "...";
|
||||
}
|
||||
} else if (children.length === 1 && typeof children[0] === "string") {
|
||||
const str = children[0];
|
||||
const fitsSameLine = str.length < 40;
|
||||
out += fitsSameLine ? str : indent(level + 1) + str + "\n";
|
||||
} else if (children.length === 1 && typeof children[0] === "function") {
|
||||
const fn = children[0];
|
||||
out += ctx.stylize("{", OTHER) +
|
||||
ctx.stylize(`[Function: ${fn.name || "Anonymous"}]`, SPECIAL) +
|
||||
ctx.stylize("}", OTHER);
|
||||
} else {
|
||||
out += "\n";
|
||||
|
||||
const unwrapped = singleChild ? children[0] : children;
|
||||
|
||||
if (ArrayIsArray(unwrapped)) {
|
||||
for (let i = 0; i < unwrapped.length; i++) {
|
||||
out += serializeChildren(ctx, adapter, unwrapped[i], level, limit);
|
||||
}
|
||||
} else {
|
||||
out += serializeChildren(ctx, adapter, unwrapped, level, limit);
|
||||
}
|
||||
|
||||
out += indent(level);
|
||||
}
|
||||
|
||||
out += ctx.stylize("</", TAGS) + namePretty +
|
||||
ctx.stylize(">", TAGS);
|
||||
} else if (isFragment && !isKeyed) {
|
||||
out += ctx.stylize("></>", TAGS);
|
||||
} else {
|
||||
out += ctx.stylize(" />", TAGS);
|
||||
}
|
||||
|
||||
if (level > 0 && level + 1 < limit) {
|
||||
out += "\n";
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ stylize: (str: string, color: string) => string }} ctx
|
||||
* @param {import("ext:deno_console/jsx").JSXSerializeAdapter} adapter
|
||||
* @param {import("ext:deno_console/jsx".NormalizedChild)} child
|
||||
* @param {number} level
|
||||
* @param {number} limit
|
||||
* @returns
|
||||
*/
|
||||
function serializeChildren(
|
||||
ctx,
|
||||
adapter,
|
||||
child,
|
||||
level,
|
||||
limit,
|
||||
) {
|
||||
let out = "";
|
||||
if (typeof child === "string") {
|
||||
return indent(level + 1) + child + "\n";
|
||||
} else if (typeof child === "function") {
|
||||
return (
|
||||
indent(level + 1) +
|
||||
ctx.stylize("{", OTHER) +
|
||||
ctx.stylize(`[Function: ${child.name || "Anonymous"}]`, SPECIAL) +
|
||||
ctx.stylize("}", OTHER) +
|
||||
"\n"
|
||||
);
|
||||
} else if (Array.isArray(child)) {
|
||||
out += indent(level + 1) + "[\n";
|
||||
|
||||
for (let i = 0; i < child.length; i++) {
|
||||
const actual = child[i];
|
||||
out += serializeChildren(ctx, adapter, actual, level + 1, limit);
|
||||
out = StringPrototypeSlice(out, 0, -1) + ctx.stylize(",\n", OTHER);
|
||||
}
|
||||
|
||||
out += indent(level + 1) + "]\n";
|
||||
} else {
|
||||
out += serialize(ctx, adapter, child, level + 1, limit);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten children into an array. Nested arrays are flattened and some
|
||||
* values like null + undefined or booleans are filtered out.
|
||||
* @param {import("ext:deno_console/jsx").NormalizedChild[]} out
|
||||
* @param {import("ext:deno_console/jsx").VElement} child
|
||||
* @param {number} level
|
||||
*/
|
||||
function normalizeChildren(
|
||||
out,
|
||||
child,
|
||||
level,
|
||||
) {
|
||||
// These are ignored as children
|
||||
if (child === null || child === undefined || typeof child === "boolean") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Preserve nested arrays. Frameworks typically replace that with an
|
||||
// implicit Fragment, but showing that would be more confusing than
|
||||
// showing the array
|
||||
if (ArrayIsArray(child)) {
|
||||
/** @type {import("ext:deno_console/jsx").NormalizedChild[]} */
|
||||
const out2 = [];
|
||||
for (let i = 0; i < child.length; i++) {
|
||||
normalizeChildren(out2, child[i], level + 1);
|
||||
}
|
||||
|
||||
if (out2.length > 0) {
|
||||
if (out2.length === 1 && typeof out2[0] === "string") {
|
||||
if (out.length > 0) {
|
||||
const last = out[out.length - 1];
|
||||
if (typeof last === "string") {
|
||||
out[out.length - 1] += out2[0];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ArrayPrototypePushApply(out, out2);
|
||||
} else if (level === 0) {
|
||||
ArrayPrototypePushApply(out, out2);
|
||||
} else {
|
||||
ArrayPrototypePush(out, out2);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
} else if (typeof child === "function") {
|
||||
ArrayPrototypePush(out, child);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if child can be merged into previous child
|
||||
if (out.length > 0 && !isVNode(child)) {
|
||||
const last = out[out.length - 1];
|
||||
if (typeof last === "string") {
|
||||
out[out.length - 1] += String(child);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ArrayPrototypePush(out, isVNode(child) ? child : String(child));
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize JSX props
|
||||
* @param {{ stylize: (str: string, color: string) => string }} ctx
|
||||
* @param {import("ext:deno_console/jsx").JSXSerializeAdapter} adapter
|
||||
* @param {Record<string, unknown>} props
|
||||
* @param {boolean} isDomNode
|
||||
* @param {boolean} skipVNodeSerialisation
|
||||
* @returns {string}
|
||||
*/
|
||||
function serializeProps(
|
||||
ctx,
|
||||
adapter,
|
||||
props,
|
||||
isDomNode,
|
||||
skipVNodeSerialisation,
|
||||
) {
|
||||
// TODO: Primodals
|
||||
const sorted = ObjectKeys(props).sort((a, b) => a.localeCompare(b));
|
||||
let out = "";
|
||||
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
const name = sorted[i];
|
||||
const value = props[name];
|
||||
|
||||
// Empty values for DOM nodes
|
||||
if (
|
||||
name === "children" ||
|
||||
name === "key" ||
|
||||
name === "ref" ||
|
||||
value === undefined ||
|
||||
(isDomNode && value === null)
|
||||
) {
|
||||
continue;
|
||||
} else if (typeof value === "string") {
|
||||
out += ` ${ctx.stylize(name, ATTR)}${
|
||||
ctx.stylize("=", OTHER)
|
||||
}${ctx.stylize(`"${value}"`), "green"}`;
|
||||
} else {
|
||||
// Complex types
|
||||
out += ` ${ctx.stylize(name, ATTR)}`;
|
||||
|
||||
// Truthy boolean values are usually displayed without value
|
||||
if (value === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
out += ctx.stylize("={", OTHER);
|
||||
|
||||
if (ArrayIsArray(value)) {
|
||||
out += ctx.stylize(`Array(${value.length})`, SPECIAL);
|
||||
} else if (value instanceof Map) {
|
||||
out += ctx.stylize(`Map(${value.size})`, SPECIAL);
|
||||
} else if (value instanceof Set) {
|
||||
out += ctx.stylize(`Set(${value.size})`, SPECIAL);
|
||||
} else if (typeof value === "function") {
|
||||
out += ctx.stylize(
|
||||
`[Function: ${value.name || "Anonymous"}]`,
|
||||
SPECIAL,
|
||||
);
|
||||
} else if (isVNode(value)) {
|
||||
if (skipVNodeSerialisation) {
|
||||
out += ctx.stylize("...", OTHER);
|
||||
} else {
|
||||
out += serialize(ctx, adapter, value, 0, 0);
|
||||
}
|
||||
} else {
|
||||
out += ctx.stylize(String(value), SPECIAL);
|
||||
}
|
||||
|
||||
out += ctx.stylize("}", OTHER);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
75
ext/console/internal.d.ts
vendored
75
ext/console/internal.d.ts
vendored
|
@ -10,3 +10,78 @@ declare module "ext:deno_console/01_console.js" {
|
|||
evaluate: boolean;
|
||||
}): Record<string, unknown>;
|
||||
}
|
||||
|
||||
declare module "ext:deno_console/02_jsx.js" {
|
||||
import { JSXSerializeAdapter, SharedVNode } from "ext:deno_console/jsx";
|
||||
|
||||
export const reactAdapter: JSXSerializeAdapter;
|
||||
export const preactAdapter: JSXSerializeAdapter;
|
||||
|
||||
export function serialize(
|
||||
adapter: JSXSerializeAdapter,
|
||||
vnode: SharedVNode,
|
||||
level: number,
|
||||
limit: number
|
||||
): string;
|
||||
}
|
||||
|
||||
declare module "ext:deno_console/jsx" {
|
||||
export interface ComponentFunction<T = unknown> {
|
||||
(props: T): VElement;
|
||||
displayName?: string;
|
||||
contenxtType?: {
|
||||
displayName?: string;
|
||||
Provider?: (props: unknown) => VElement;
|
||||
Consumer?: (props: unknown) => VElement;
|
||||
};
|
||||
}
|
||||
|
||||
export type VChild =
|
||||
| string
|
||||
| number
|
||||
| null
|
||||
| undefined
|
||||
// deno-lint-ignore ban-types
|
||||
| Function
|
||||
| SharedVNode;
|
||||
export type VElement = VChild | VElement[] | Iterable<VElement>;
|
||||
export type NormalizedChild =
|
||||
| string
|
||||
// deno-lint-ignore ban-types
|
||||
| Function
|
||||
| SharedVNode
|
||||
| NormalizedChild[];
|
||||
|
||||
export interface PreactComponentInstance {
|
||||
sub?: unknown;
|
||||
displayName?: string;
|
||||
}
|
||||
|
||||
export interface SharedVNode {
|
||||
$$typeof?: symbol;
|
||||
// In Preact "null" means text and "props.data" is the text.
|
||||
// In React symbols are for special nodes like Fragments
|
||||
type:
|
||||
| string
|
||||
| ComponentFunction
|
||||
| null
|
||||
| number
|
||||
| symbol
|
||||
// React
|
||||
| {
|
||||
$$typeof: symbol;
|
||||
displayName?: string;
|
||||
type: ComponentFunction;
|
||||
render?: ComponentFunction;
|
||||
};
|
||||
props: Record<string, unknown>;
|
||||
key: null | undefined | number | string;
|
||||
}
|
||||
|
||||
export interface JSXSerializeAdapter {
|
||||
getTextIfTextNode(x: SharedVNode): string | null;
|
||||
isFragment(x: SharedVNode): boolean;
|
||||
getName(x: SharedVNode): string;
|
||||
isValidElement(x: unknown): x is SharedVNode;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
use std::path::PathBuf;
|
||||
|
||||
deno_core::extension!(deno_console, esm = ["01_console.js"],);
|
||||
deno_core::extension!(deno_console, esm = ["01_console.js", "02_jsx.js"],);
|
||||
|
||||
pub fn get_declaration() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("lib.deno_console.d.ts")
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"ext:deno_broadcast_channel/01_broadcast_channel.js": "../ext/broadcast_channel/01_broadcast_channel.js",
|
||||
"ext:deno_cache/01_cache.js": "../ext/cache/01_cache.js",
|
||||
"ext:deno_console/01_console.js": "../ext/console/01_console.js",
|
||||
"ext:deno_console/02_jsx.js": "../ext/console/02_jsx.js",
|
||||
"ext:deno_crypto/00_crypto.js": "../ext/crypto/00_crypto.js",
|
||||
"ext:deno_fetch/20_headers.js": "../ext/fetch/20_headers.js",
|
||||
"ext:deno_fetch/21_formdata.js": "../ext/fetch/21_formdata.js",
|
||||
|
|
Loading…
Reference in a new issue