Merge pull request #108 from eqlabs/feat/debug-prettify
Debug stream improvements.
This commit is contained in:
		@@ -27,7 +27,7 @@ const labelStyle = css({
 | 
			
		||||
  mb: "$0.5",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const AccountDialog = ({
 | 
			
		||||
export const AccountDialog = ({
 | 
			
		||||
  activeAccountAddress,
 | 
			
		||||
  setActiveAccountAddress,
 | 
			
		||||
}: {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,84 +1,136 @@
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import { useSnapshot } from "valtio";
 | 
			
		||||
import { useCallback, useEffect } from "react";
 | 
			
		||||
import { proxy, ref, useSnapshot } from "valtio";
 | 
			
		||||
import { Select } from ".";
 | 
			
		||||
import state from "../state";
 | 
			
		||||
import state, { ILog } from "../state";
 | 
			
		||||
import { extractJSON } from "../utils/json";
 | 
			
		||||
import LogBox from "./LogBox";
 | 
			
		||||
import Text from "./Text";
 | 
			
		||||
 | 
			
		||||
interface ISelect<T = string> {
 | 
			
		||||
  label: string;
 | 
			
		||||
  value: T;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const streamState = proxy({
 | 
			
		||||
  selectedAccount: null as ISelect | null,
 | 
			
		||||
  logs: [] as ILog[],
 | 
			
		||||
  socket: undefined as WebSocket | undefined,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const DebugStream = () => {
 | 
			
		||||
  const snap = useSnapshot(state);
 | 
			
		||||
  const { selectedAccount, logs, socket } = useSnapshot(streamState);
 | 
			
		||||
  const { accounts } = useSnapshot(state);
 | 
			
		||||
 | 
			
		||||
  const accountOptions = snap.accounts.map(acc => ({
 | 
			
		||||
  const accountOptions = accounts.map(acc => ({
 | 
			
		||||
    label: acc.name,
 | 
			
		||||
    value: acc.address,
 | 
			
		||||
  }));
 | 
			
		||||
  const [selectedAccount, setSelectedAccount] = useState<typeof accountOptions[0] | null>(null);
 | 
			
		||||
 | 
			
		||||
  const renderNav = () => (
 | 
			
		||||
    <>
 | 
			
		||||
      <Text css={{ mx: "$2", fontSize: "inherit" }}>Account: </Text>
 | 
			
		||||
      <Select
 | 
			
		||||
        instanceId="debugStreamAccount"
 | 
			
		||||
        instanceId="DSAccount"
 | 
			
		||||
        placeholder="Select account"
 | 
			
		||||
        options={accountOptions}
 | 
			
		||||
        hideSelectedOptions
 | 
			
		||||
        value={selectedAccount}
 | 
			
		||||
        onChange={acc => setSelectedAccount(acc as any)}
 | 
			
		||||
        css={{ width: "30%" }}
 | 
			
		||||
        onChange={acc => (streamState.selectedAccount = acc as any)}
 | 
			
		||||
        css={{ width: "100%" }}
 | 
			
		||||
      />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const prepareLog = useCallback((str: any): ILog => {
 | 
			
		||||
    if (typeof str !== "string") throw Error("Unrecognized debug log stream!");
 | 
			
		||||
 | 
			
		||||
    const match = str.match(/([\s\S]+(?:UTC|ISO|GMT[+|-]\d+))\ ?([\s\S]*)/m);
 | 
			
		||||
    const [_, tm, msg] = match || [];
 | 
			
		||||
 | 
			
		||||
    const extracted = extractJSON(msg);
 | 
			
		||||
    const timestamp = isNaN(Date.parse(tm || ""))
 | 
			
		||||
      ? tm
 | 
			
		||||
      : new Date(tm).toLocaleTimeString();
 | 
			
		||||
 | 
			
		||||
    const message = !extracted
 | 
			
		||||
      ? msg
 | 
			
		||||
      : msg.slice(0, extracted.start) + msg.slice(extracted.end + 1);
 | 
			
		||||
 | 
			
		||||
    const jsonData = extracted
 | 
			
		||||
      ? JSON.stringify(extracted.result, null, 2)
 | 
			
		||||
      : undefined;
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      type: "log",
 | 
			
		||||
      message,
 | 
			
		||||
      timestamp,
 | 
			
		||||
      jsonData,
 | 
			
		||||
      defaultCollapsed: true,
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const account = selectedAccount?.value;
 | 
			
		||||
    if (!account) {
 | 
			
		||||
      return;
 | 
			
		||||
    if (account && (!socket || !socket.url.endsWith(account))) {
 | 
			
		||||
      socket?.close();
 | 
			
		||||
      streamState.socket = ref(
 | 
			
		||||
        new WebSocket(
 | 
			
		||||
          `wss://hooks-testnet-debugstream.xrpl-labs.com/${account}`
 | 
			
		||||
        )
 | 
			
		||||
      );
 | 
			
		||||
    } else if (!account && socket) {
 | 
			
		||||
      socket.close();
 | 
			
		||||
      streamState.socket = undefined;
 | 
			
		||||
    }
 | 
			
		||||
    const socket = new WebSocket(`wss://hooks-testnet-debugstream.xrpl-labs.com/${account}`);
 | 
			
		||||
  }, [selectedAccount?.value, socket]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const account = selectedAccount?.value;
 | 
			
		||||
    const socket = streamState.socket;
 | 
			
		||||
    if (!socket) return;
 | 
			
		||||
 | 
			
		||||
    const onOpen = () => {
 | 
			
		||||
      state.debugLogs = [];
 | 
			
		||||
      state.debugLogs.push({
 | 
			
		||||
      streamState.logs = [];
 | 
			
		||||
      streamState.logs.push({
 | 
			
		||||
        type: "success",
 | 
			
		||||
        message: `Debug stream opened for account ${account}`,
 | 
			
		||||
      });
 | 
			
		||||
    };
 | 
			
		||||
    const onError = () => {
 | 
			
		||||
      state.debugLogs.push({
 | 
			
		||||
      streamState.logs.push({
 | 
			
		||||
        type: "error",
 | 
			
		||||
        message: "Something went wrong in establishing connection!",
 | 
			
		||||
        message: "Something went wrong! Check your connection and try again.",
 | 
			
		||||
      });
 | 
			
		||||
      setSelectedAccount(null);
 | 
			
		||||
    };
 | 
			
		||||
    const onClose = (e: CloseEvent) => {
 | 
			
		||||
      streamState.logs.push({
 | 
			
		||||
        type: "error",
 | 
			
		||||
        message: `Connection was closed. [code: ${e.code}]`,
 | 
			
		||||
      });
 | 
			
		||||
      streamState.selectedAccount = null;
 | 
			
		||||
    };
 | 
			
		||||
    const onMessage = (event: any) => {
 | 
			
		||||
      if (!event.data) return;
 | 
			
		||||
      state.debugLogs.push({
 | 
			
		||||
        type: "log",
 | 
			
		||||
        message: event.data,
 | 
			
		||||
      });
 | 
			
		||||
      streamState.logs.push(prepareLog(event.data));
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    socket.addEventListener("open", onOpen);
 | 
			
		||||
    socket.addEventListener("close", onError);
 | 
			
		||||
    socket.addEventListener("close", onClose);
 | 
			
		||||
    socket.addEventListener("error", onError);
 | 
			
		||||
    socket.addEventListener("message", onMessage);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      socket.removeEventListener("open", onOpen);
 | 
			
		||||
      socket.removeEventListener("close", onError);
 | 
			
		||||
      socket.removeEventListener("close", onClose);
 | 
			
		||||
      socket.removeEventListener("message", onMessage);
 | 
			
		||||
 | 
			
		||||
      socket.close();
 | 
			
		||||
      socket.removeEventListener("error", onError);
 | 
			
		||||
    };
 | 
			
		||||
  }, [selectedAccount]);
 | 
			
		||||
 | 
			
		||||
  }, [prepareLog, selectedAccount?.value, socket]);
 | 
			
		||||
  return (
 | 
			
		||||
    <LogBox
 | 
			
		||||
      enhanced
 | 
			
		||||
      renderNav={renderNav}
 | 
			
		||||
      title="Debug stream"
 | 
			
		||||
      logs={snap.debugLogs}
 | 
			
		||||
      clearLog={() => (state.debugLogs = [])}
 | 
			
		||||
      logs={logs}
 | 
			
		||||
      clearLog={() => (streamState.logs = [])}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,14 @@ import { styled } from "../stitches.config";
 | 
			
		||||
const StyledLink = styled("a", {
 | 
			
		||||
  color: "CurrentColor",
 | 
			
		||||
  textDecoration: "underline",
 | 
			
		||||
  cursor: 'pointer',
 | 
			
		||||
  variants: {
 | 
			
		||||
    highlighted: {
 | 
			
		||||
      true: {
 | 
			
		||||
        color: '$blue9'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default StyledLink;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,15 @@
 | 
			
		||||
import React, { useRef, useLayoutEffect, ReactNode } from "react";
 | 
			
		||||
import { useRef, useLayoutEffect, ReactNode, FC, useState, useCallback } from "react";
 | 
			
		||||
import { Notepad, Prohibit } from "phosphor-react";
 | 
			
		||||
import useStayScrolled from "react-stay-scrolled";
 | 
			
		||||
import NextLink from "next/link";
 | 
			
		||||
 | 
			
		||||
import Container from "./Container";
 | 
			
		||||
import Box from "./Box";
 | 
			
		||||
import Flex from "./Flex";
 | 
			
		||||
import LogText from "./LogText";
 | 
			
		||||
import { ILog } from "../state";
 | 
			
		||||
import Text from "./Text";
 | 
			
		||||
import Button from "./Button";
 | 
			
		||||
import Heading from "./Heading";
 | 
			
		||||
import Link from "./Link";
 | 
			
		||||
import state, { ILog } from "../state";
 | 
			
		||||
import { Pre, Link, Heading, Button, Text, Flex, Box } from ".";
 | 
			
		||||
import regexifyString from "regexify-string";
 | 
			
		||||
import { useSnapshot } from "valtio";
 | 
			
		||||
import { AccountDialog } from "./Accounts";
 | 
			
		||||
 | 
			
		||||
interface ILogBox {
 | 
			
		||||
  title: string;
 | 
			
		||||
@@ -21,14 +19,7 @@ interface ILogBox {
 | 
			
		||||
  enhanced?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const LogBox: React.FC<ILogBox> = ({
 | 
			
		||||
  title,
 | 
			
		||||
  clearLog,
 | 
			
		||||
  logs,
 | 
			
		||||
  children,
 | 
			
		||||
  renderNav,
 | 
			
		||||
  enhanced,
 | 
			
		||||
}) => {
 | 
			
		||||
const LogBox: FC<ILogBox> = ({ title, clearLog, logs, children, renderNav, enhanced }) => {
 | 
			
		||||
  const logRef = useRef<HTMLPreElement>(null);
 | 
			
		||||
  const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef);
 | 
			
		||||
 | 
			
		||||
@@ -55,6 +46,7 @@ const LogBox: React.FC<ILogBox> = ({
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Flex
 | 
			
		||||
          fluid
 | 
			
		||||
          css={{
 | 
			
		||||
            height: "48px",
 | 
			
		||||
            alignItems: "center",
 | 
			
		||||
@@ -78,7 +70,15 @@ const LogBox: React.FC<ILogBox> = ({
 | 
			
		||||
          >
 | 
			
		||||
            <Notepad size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
 | 
			
		||||
          </Heading>
 | 
			
		||||
          {renderNav?.()}
 | 
			
		||||
          <Flex
 | 
			
		||||
            row
 | 
			
		||||
            align="center"
 | 
			
		||||
            css={{
 | 
			
		||||
              width: "50%", // TODO make it max without breaking layout!
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            {renderNav?.()}
 | 
			
		||||
          </Flex>
 | 
			
		||||
          <Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}>
 | 
			
		||||
            {clearLog && (
 | 
			
		||||
              <Button ghost size="xs" onClick={clearLog}>
 | 
			
		||||
@@ -117,17 +117,11 @@ const LogBox: React.FC<ILogBox> = ({
 | 
			
		||||
                    backgroundColor: enhanced ? "$backgroundAlt" : undefined,
 | 
			
		||||
                  },
 | 
			
		||||
                },
 | 
			
		||||
                p: enhanced ? "$2 $1" : undefined,
 | 
			
		||||
                p: enhanced ? "$1" : undefined,
 | 
			
		||||
                my: enhanced ? "$1" : undefined,
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <LogText variant={log.type}>
 | 
			
		||||
                {log.message}{" "}
 | 
			
		||||
                {log.link && (
 | 
			
		||||
                  <NextLink href={log.link} shallow passHref>
 | 
			
		||||
                    <Link as="a">{log.linkText}</Link>
 | 
			
		||||
                  </NextLink>
 | 
			
		||||
                )}
 | 
			
		||||
              </LogText>
 | 
			
		||||
              <Log {...log} />
 | 
			
		||||
            </Box>
 | 
			
		||||
          ))}
 | 
			
		||||
          {children}
 | 
			
		||||
@@ -137,4 +131,74 @@ const LogBox: React.FC<ILogBox> = ({
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Log: FC<ILog> = ({
 | 
			
		||||
  type,
 | 
			
		||||
  timestamp: timestamp,
 | 
			
		||||
  message: _message,
 | 
			
		||||
  link,
 | 
			
		||||
  linkText,
 | 
			
		||||
  defaultCollapsed,
 | 
			
		||||
  jsonData: _jsonData,
 | 
			
		||||
}) => {
 | 
			
		||||
  const [expanded, setExpanded] = useState(!defaultCollapsed);
 | 
			
		||||
  const { accounts } = useSnapshot(state);
 | 
			
		||||
  const [dialogAccount, setDialogAccount] = useState<string | null>(null);
 | 
			
		||||
 | 
			
		||||
  const enrichAccounts = useCallback(
 | 
			
		||||
    (str?: string): ReactNode => {
 | 
			
		||||
      if (!str || !accounts.length) return null;
 | 
			
		||||
 | 
			
		||||
      const pattern = `(${accounts.map(acc => acc.address).join("|")})`;
 | 
			
		||||
      const res = regexifyString({
 | 
			
		||||
        pattern: new RegExp(pattern, "gim"),
 | 
			
		||||
        decorator: (match, idx) => {
 | 
			
		||||
          const name = accounts.find(acc => acc.address === match)?.name;
 | 
			
		||||
          return (
 | 
			
		||||
            <Link
 | 
			
		||||
              key={match + idx}
 | 
			
		||||
              as="a"
 | 
			
		||||
              onClick={() => setDialogAccount(match)}
 | 
			
		||||
              title={match}
 | 
			
		||||
              highlighted
 | 
			
		||||
            >
 | 
			
		||||
              {name || match}
 | 
			
		||||
            </Link>
 | 
			
		||||
          );
 | 
			
		||||
        },
 | 
			
		||||
        input: str,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return <>{res}</>;
 | 
			
		||||
    },
 | 
			
		||||
    [accounts]
 | 
			
		||||
  );
 | 
			
		||||
  _message = _message.trim().replace(/\n /gi, "\n");
 | 
			
		||||
  const message = enrichAccounts(_message);
 | 
			
		||||
  const jsonData = enrichAccounts(_jsonData);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <AccountDialog
 | 
			
		||||
        setActiveAccountAddress={setDialogAccount}
 | 
			
		||||
        activeAccountAddress={dialogAccount}
 | 
			
		||||
      />
 | 
			
		||||
      <LogText variant={type}>
 | 
			
		||||
        {timestamp && <Text muted monospace>{timestamp} </Text>}
 | 
			
		||||
        <Pre>{message} </Pre>
 | 
			
		||||
        {link && (
 | 
			
		||||
          <NextLink href={link} shallow passHref>
 | 
			
		||||
            <Link as="a">{linkText}</Link>
 | 
			
		||||
          </NextLink>
 | 
			
		||||
        )}
 | 
			
		||||
        {jsonData && (
 | 
			
		||||
          <Link onClick={() => setExpanded(!expanded)} as="a">
 | 
			
		||||
            {expanded ? "Collapse" : "Expand"}
 | 
			
		||||
          </Link>
 | 
			
		||||
        )}
 | 
			
		||||
        {expanded && jsonData && <Pre block>{jsonData}</Pre>}
 | 
			
		||||
      </LogText>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default LogBox;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										27
									
								
								components/Pre.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								components/Pre.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
import { styled } from "../stitches.config";
 | 
			
		||||
 | 
			
		||||
const Pre = styled("span", {
 | 
			
		||||
  m: 0,
 | 
			
		||||
  wordBreak: "break-all",
 | 
			
		||||
  fontFamily: '$monospace',
 | 
			
		||||
  whiteSpace: 'pre-wrap',
 | 
			
		||||
  variants: {
 | 
			
		||||
    fluid: {
 | 
			
		||||
      true: {
 | 
			
		||||
        width: "100%",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    line: {
 | 
			
		||||
      true: {
 | 
			
		||||
        whiteSpace: 'pre-line'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    block: {
 | 
			
		||||
      true: {
 | 
			
		||||
        display: 'block'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default Pre;
 | 
			
		||||
@@ -14,6 +14,11 @@ const Text = styled("span", {
 | 
			
		||||
      true: {
 | 
			
		||||
        color: '$mauve9'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    monospace: {
 | 
			
		||||
      true: {
 | 
			
		||||
        fontFamily: '$monospace'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ export * from "./Tabs";
 | 
			
		||||
export * from "./AlertDialog";
 | 
			
		||||
export { default as Box } from "./Box";
 | 
			
		||||
export { default as Button } from "./Button";
 | 
			
		||||
export { default as Pre } from "./Pre";
 | 
			
		||||
export { default as ButtonGroup } from "./ButtonGroup";
 | 
			
		||||
export { default as DeployFooter } from "./DeployFooter";
 | 
			
		||||
export * from "./Dialog";
 | 
			
		||||
 
 | 
			
		||||
@@ -47,6 +47,7 @@
 | 
			
		||||
    "react-stay-scrolled": "^7.4.0",
 | 
			
		||||
    "react-time-ago": "^7.1.9",
 | 
			
		||||
    "reconnecting-websocket": "^4.4.0",
 | 
			
		||||
    "regexify-string": "^1.0.17",
 | 
			
		||||
    "valtio": "^1.2.5",
 | 
			
		||||
    "vscode-languageserver": "^7.0.0",
 | 
			
		||||
    "vscode-uri": "^3.0.2",
 | 
			
		||||
@@ -63,4 +64,4 @@
 | 
			
		||||
    "eslint-config-next": "11.1.2",
 | 
			
		||||
    "typescript": "4.4.4"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -34,8 +34,11 @@ export interface IAccount {
 | 
			
		||||
export interface ILog {
 | 
			
		||||
  type: "error" | "warning" | "log" | "success";
 | 
			
		||||
  message: string;
 | 
			
		||||
  jsonData?: any,
 | 
			
		||||
  timestamp?: string;
 | 
			
		||||
  link?: string;
 | 
			
		||||
  linkText?: string;
 | 
			
		||||
  defaultCollapsed?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IState {
 | 
			
		||||
@@ -52,7 +55,6 @@ export interface IState {
 | 
			
		||||
  logs: ILog[];
 | 
			
		||||
  deployLogs: ILog[];
 | 
			
		||||
  transactionLogs: ILog[];
 | 
			
		||||
  debugLogs: ILog[];
 | 
			
		||||
  editorCtx?: typeof monaco.editor;
 | 
			
		||||
  editorSettings: {
 | 
			
		||||
    tabSize: number;
 | 
			
		||||
@@ -78,7 +80,6 @@ let initialState: IState = {
 | 
			
		||||
  logs: [],
 | 
			
		||||
  deployLogs: [],
 | 
			
		||||
  transactionLogs: [],
 | 
			
		||||
  debugLogs: [],
 | 
			
		||||
  editorCtx: undefined,
 | 
			
		||||
  gistId: undefined,
 | 
			
		||||
  gistOwner: undefined,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								utils/json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								utils/json.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
export const extractJSON = (str?: string) => {
 | 
			
		||||
    if (!str) return
 | 
			
		||||
    let firstOpen = 0, firstClose = 0, candidate = '';
 | 
			
		||||
    firstOpen = str.indexOf('{', firstOpen + 1);
 | 
			
		||||
    do {
 | 
			
		||||
        firstClose = str.lastIndexOf('}');
 | 
			
		||||
        if (firstClose <= firstOpen) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        do {
 | 
			
		||||
            candidate = str.substring(firstOpen, firstClose + 1);
 | 
			
		||||
            try {
 | 
			
		||||
                let result = JSON.parse(candidate);
 | 
			
		||||
                return { result, start: firstOpen < 0 ? 0 : firstOpen, end: firstClose }
 | 
			
		||||
            }
 | 
			
		||||
            catch (e) { }
 | 
			
		||||
            firstClose = str.substring(0, firstClose).lastIndexOf('}');
 | 
			
		||||
        } while (firstClose > firstOpen);
 | 
			
		||||
        firstOpen = str.indexOf('{', firstOpen + 1);
 | 
			
		||||
    } while (firstOpen != -1);
 | 
			
		||||
}
 | 
			
		||||
@@ -3989,6 +3989,11 @@ regenerator-runtime@^0.13.4:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
 | 
			
		||||
  integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
 | 
			
		||||
 | 
			
		||||
regexify-string@^1.0.17:
 | 
			
		||||
  version "1.0.17"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/regexify-string/-/regexify-string-1.0.17.tgz#b9e571b51c8ec566eb82b7121744dae0d8e829de"
 | 
			
		||||
  integrity sha512-mmD0AUNaY/piGW2AyACWdQOjIAwNuWz+KIvxfBZPDdCBAexiROeQxdxTaYAWcIxwtUAOwojdTta6CMMil84jXw==
 | 
			
		||||
 | 
			
		||||
regexp.prototype.flags@^1.3.1:
 | 
			
		||||
  version "1.3.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user