Merge pull request #454 from FiretableProject/develop

Develop
This commit is contained in:
Shams
2021-06-18 14:40:06 +10:00
committed by GitHub
11 changed files with 547 additions and 121 deletions

View File

@@ -7,57 +7,74 @@ import admin from "firebase-admin";
export default async function generateConfig(
schemaPath: string,
user: admin.auth.UserRecord
user: admin.auth.UserRecord,
streamLogger
) {
return await generateConfigFromTableSchema(schemaPath, user).then(
async (success) => {
if (!success) {
console.log("generateConfigFromTableSchema failed to complete");
return await generateConfigFromTableSchema(
schemaPath,
user,
streamLogger
).then(async (success) => {
if (!success) {
await streamLogger.info(
`generateConfigFromTableSchema failed to complete`
);
return false;
}
await streamLogger.info(`generateConfigFromTableSchema done`);
const configFile = fs.readFileSync(
path.resolve(__dirname, "../functions/src/functionConfig.ts"),
"utf-8"
);
await streamLogger.info(`configFile: ${JSON.stringify(configFile)}`);
const requiredDependencies = configFile.match(
/(?<=(require\(("|'))).*?(?=("|')\))/g
);
if (requiredDependencies) {
const packgesAdded = await addPackages(
requiredDependencies.map((p: any) => ({ name: p })),
user,
streamLogger
);
if (!packgesAdded) {
return false;
}
}
await streamLogger.info(
`requiredDependencies: ${JSON.stringify(requiredDependencies)}`
);
console.log("generateConfigFromTableSchema done");
const configFile = fs.readFileSync(
path.resolve(__dirname, "../functions/src/functionConfig.ts"),
"utf-8"
);
const requiredDependencies = configFile.match(
/(?<=(require\(("|'))).*?(?=("|')\))/g
);
if (requiredDependencies) {
const packgesAdded = await addPackages(
requiredDependencies.map((p: any) => ({ name: p })),
user
);
if (!packgesAdded) {
return false;
}
}
const isFunctionConfigValid = await asyncExecute(
"cd build/functions/src; tsc functionConfig.ts",
commandErrorHandler({
const isFunctionConfigValid = await asyncExecute(
"cd build/functions/src; tsc functionConfig.ts",
commandErrorHandler(
{
user,
functionConfigTs: configFile,
description: `Invalid compiled functionConfig.ts`,
})
);
if (!isFunctionConfigValid) {
},
streamLogger
)
);
await streamLogger.info(
`isFunctionConfigValid: ${JSON.stringify(isFunctionConfigValid)}`
);
if (!isFunctionConfigValid) {
return false;
}
const { sparksConfig } = require("../functions/src/functionConfig.js");
const requiredSparks = sparksConfig.map((s: any) => s.type);
await streamLogger.info(
`requiredSparks: ${JSON.stringify(requiredSparks)}`
);
for (const lib of requiredSparks) {
const success = await addSparkLib(lib, user, streamLogger);
if (!success) {
return false;
}
const { sparksConfig } = require("../functions/src/functionConfig.js");
const requiredSparks = sparksConfig.map((s: any) => s.type);
console.log({ requiredSparks });
for (const lib of requiredSparks) {
const success = await addSparkLib(lib, user);
if (!success) {
return false;
}
}
return true;
}
);
return true;
});
}

View File

@@ -6,14 +6,22 @@ import { parseSparksConfig } from "../utils";
export const generateConfigFromTableSchema = async (
schemaDocPath: string,
user: admin.auth.UserRecord
user: admin.auth.UserRecord,
streamLogger
) => {
await streamLogger.info("getting schema...");
const schemaDoc = await db.doc(schemaDocPath).get();
const schemaData = schemaDoc.data();
if (!schemaData) throw new Error("no schema found");
// Temporarily disabled because this is super long
// await streamLogger.info(`schemaData: ${JSON.stringify(schemaData)}`);
const derivativeColumns = Object.values(schemaData.columns).filter(
(col: any) => col.type === "DERIVATIVE"
);
await streamLogger.info(
`derivativeColumns: ${JSON.stringify(derivativeColumns)}`
);
const derivativesConfig = `[${derivativeColumns.reduce(
(acc, currColumn: any) => {
if (
@@ -37,11 +45,16 @@ export const generateConfigFromTableSchema = async (
},
""
)}]`;
await streamLogger.info(
`derivativesConfig: ${JSON.stringify(derivativesConfig)}`
);
const initializableColumns = Object.values(
schemaData.columns
).filter((col: any) => Boolean(col.config?.defaultValue));
console.log(JSON.stringify({ initializableColumns }));
await streamLogger.info(
`initializableColumns: ${JSON.stringify(initializableColumns)}`
);
const initializeConfig = `[${initializableColumns.reduce(
(acc, currColumn: any) => {
if (currColumn.config.defaultValue.type === "static") {
@@ -66,6 +79,9 @@ export const generateConfigFromTableSchema = async (
},
""
)}]`;
await streamLogger.info(
`initializeConfig: ${JSON.stringify(initializeConfig)}`
);
const documentSelectColumns = Object.values(schemaData.columns).filter(
(col: any) => col.type === "DOCUMENT_SELECT" && col.config?.trackedFields
);
@@ -79,8 +95,12 @@ export const generateConfigFromTableSchema = async (
},
""
)}]`;
await streamLogger.info(
`documentSelectColumns: ${JSON.stringify(documentSelectColumns)}`
);
const sparksConfig = parseSparksConfig(schemaData.sparks, user);
const sparksConfig = parseSparksConfig(schemaData.sparks, user, streamLogger);
await streamLogger.info(`sparksConfig: ${JSON.stringify(sparksConfig)}`);
const collectionType = schemaDocPath.includes("subTables")
? "subCollection"
@@ -132,6 +152,7 @@ export const generateConfigFromTableSchema = async (
default:
break;
}
await streamLogger.info(`collectionType: ${JSON.stringify(collectionType)}`);
// generate field types from table meta data
const fieldTypes = JSON.stringify(
@@ -147,6 +168,7 @@ export const generateConfigFromTableSchema = async (
};
}, {})
);
await streamLogger.info(`fieldTypes: ${JSON.stringify(fieldTypes)}`);
const exports: any = {
fieldTypes,
@@ -157,10 +179,12 @@ export const generateConfigFromTableSchema = async (
documentSelectConfig,
sparksConfig,
};
await streamLogger.info(`exports: ${JSON.stringify(exports)}`);
const fileData = Object.keys(exports).reduce((acc, currKey) => {
return `${acc}\nexport const ${currKey} = ${exports[currKey]}`;
}, ``);
await streamLogger.info(`fileData: ${JSON.stringify(fileData)}`);
const path = require("path");
fs.writeFileSync(

View File

@@ -21,7 +21,8 @@ export const asyncExecute = async (command: string, callback: any) =>
export const addPackages = async (
packages: { name: string; version?: string }[],
user: admin.auth.UserRecord
user: admin.auth.UserRecord,
streamLogger
) => {
const packagesString = packages.reduce((acc, currPackage) => {
return `${acc} ${currPackage.name}@${currPackage.version ?? "latest"}`;
@@ -29,10 +30,13 @@ export const addPackages = async (
if (packagesString.trim().length !== 0) {
const success = await asyncExecute(
`cd build/functions;yarn add ${packagesString}`,
commandErrorHandler({
user,
description: "Error adding packages",
})
commandErrorHandler(
{
user,
description: "Error adding packages",
},
streamLogger
)
);
return success;
}
@@ -41,7 +45,8 @@ export const addPackages = async (
export const addSparkLib = async (
name: string,
user: admin.auth.UserRecord
user: admin.auth.UserRecord,
streamLogger
) => {
try {
const { dependencies } = require(`../sparksLib/${name}`);
@@ -49,24 +54,30 @@ export const addSparkLib = async (
name: key,
version: dependencies[key],
}));
const success = await addPackages(packages, user);
const success = await addPackages(packages, user, streamLogger);
if (!success) {
return false;
}
} catch (error) {
logErrorToDB({
user,
errorDescription: "Error parsing dependencies",
});
logErrorToDB(
{
user,
errorDescription: "Error parsing dependencies",
},
streamLogger
);
return false;
}
const success = await asyncExecute(
`cp build/sparksLib/${name}.ts build/functions/src/sparks/${name}.ts`,
commandErrorHandler({
user,
description: "Error copying sparksLib",
})
commandErrorHandler(
{
user,
description: "Error copying sparksLib",
},
streamLogger
)
);
return success;
};

View File

@@ -2,6 +2,7 @@ const express = require("express");
const bodyParser = require("body-parser");
const cors = require("cors");
import { asyncExecute } from "./compiler/terminal";
import { createStreamLogger } from "./utils";
import generateConfig from "./compiler";
import { auth } from "./firebaseConfig";
import meta from "./package.json";
@@ -73,29 +74,37 @@ app.post("/", jsonParser, async (req: any, res: any) => {
});
}
const success = await generateConfig(configPath, user);
const streamLogger = await createStreamLogger(configPath);
await streamLogger.info("streamLogger created");
const success = await generateConfig(configPath, user, streamLogger);
if (!success) {
console.log(`generateConfig failed to complete`);
await streamLogger.error("generateConfig failed to complete");
await streamLogger.fail();
res.send({
success: false,
reason: `generateConfig failed to complete`,
});
return;
}
console.log("generateConfig done");
await streamLogger.info("generateConfig success");
let hasEnvError = false;
if (!process.env._PROJECT_ID) {
await logErrorToDB({
errorDescription: `Invalid env: _PROJECT_ID (${process.env._PROJECT_ID})`,
user,
});
await logErrorToDB(
{
errorDescription: `Invalid env: _PROJECT_ID (${process.env._PROJECT_ID})`,
user,
},
streamLogger
);
hasEnvError = true;
}
if (hasEnvError) {
await streamLogger.error("Invalid env:_PROJECT_ID");
await streamLogger.fail();
res.send({
success: false,
reason: "Invalid env:_PROJECT_ID",
@@ -106,7 +115,7 @@ app.post("/", jsonParser, async (req: any, res: any) => {
await asyncExecute(
`cd build/functions; \
yarn install`,
commandErrorHandler({ user })
commandErrorHandler({ user }, streamLogger)
);
await asyncExecute(
@@ -114,10 +123,10 @@ app.post("/", jsonParser, async (req: any, res: any) => {
yarn deployFT \
--project ${process.env._PROJECT_ID} \
--only functions`,
commandErrorHandler({ user })
commandErrorHandler({ user }, streamLogger)
);
console.log("build complete");
await streamLogger.success();
res.send({
success: true,
});

View File

@@ -16,13 +16,36 @@ async function insertErrorRecordToDB(errorRecord: object) {
await db.collection("_FT_ERRORS").add(errorRecord);
}
function commandErrorHandler(meta: {
user: admin.auth.UserRecord;
description?: string;
functionConfigTs?: string;
sparksConfig?: string;
}) {
async function insertErrorToStreamer(errorRecord: object, streamLogger) {
let errorString = "";
for (const key of [
"command",
"description",
"functionConfigTs",
"sparksConfig",
"stderr",
"errorStackTrace",
]) {
const value = errorRecord[key];
if (value) {
errorString += `\n\n${key}: ${value}`;
}
}
await streamLogger.error(errorString);
}
function commandErrorHandler(
meta: {
user: admin.auth.UserRecord;
description?: string;
functionConfigTs?: string;
sparksConfig?: string;
},
streamLogger
) {
return async function (error, stdout, stderr) {
await streamLogger.info(stdout);
if (!error) {
return;
}
@@ -39,17 +62,21 @@ function commandErrorHandler(meta: {
functionConfigTs: meta?.functionConfigTs ?? "",
sparksConfig: meta?.sparksConfig ?? "",
};
await insertErrorToStreamer(errorRecord, streamLogger);
insertErrorRecordToDB(errorRecord);
};
}
async function logErrorToDB(data: {
errorDescription: string;
errorExtraInfo?: string;
errorTraceStack?: string;
user: admin.auth.UserRecord;
sparksConfig?: string;
}) {
async function logErrorToDB(
data: {
errorDescription: string;
errorExtraInfo?: string;
errorTraceStack?: string;
user: admin.auth.UserRecord;
sparksConfig?: string;
},
streamLogger?
) {
console.error(data.errorDescription);
const errorRecord = {
@@ -61,13 +88,16 @@ async function logErrorToDB(data: {
errorExtraInfo: data?.errorExtraInfo ?? "",
errorStackTrace: data?.errorTraceStack ?? "",
};
if (streamLogger) {
await insertErrorToStreamer(errorRecord, streamLogger);
}
insertErrorRecordToDB(errorRecord);
}
function parseSparksConfig(
sparks: string | undefined,
user: admin.auth.UserRecord
user: admin.auth.UserRecord,
streamLogger
) {
if (sparks) {
try {
@@ -76,16 +106,81 @@ function parseSparksConfig(
.replace(/^(\s*)sparks.config\(/, "")
.replace(/\);?\s*$/, "");
} catch (error) {
logErrorToDB({
errorDescription: "Sparks is not wrapped with sparks.config",
errorTraceStack: error.stack,
user,
sparksConfig: sparks,
});
logErrorToDB(
{
errorDescription: "Sparks is not wrapped with sparks.config",
errorTraceStack: error.stack,
user,
sparksConfig: sparks,
},
streamLogger
);
}
}
return "[]";
}
export { commandErrorHandler, logErrorToDB, parseSparksConfig };
async function createStreamLogger(tableConfigPath: string) {
const startTimeStamp = Date.now();
const fullLog: {
log: string;
level: "info" | "error";
timestamp: number;
}[] = [];
const logRef = db
.doc(tableConfigPath)
.collection("ftBuildLogs")
.doc(startTimeStamp.toString());
await logRef.set({ startTimeStamp, status: "BUILDING" });
console.log(
`streamLogger created. tableConfigPath: ${tableConfigPath}, startTimeStamp: ${startTimeStamp}`
);
return {
info: async (log: string) => {
console.log(log);
fullLog.push({
log,
level: "info",
timestamp: Date.now(),
});
await logRef.update({
fullLog,
});
},
error: async (log: string) => {
console.error(log);
fullLog.push({
log,
level: "error",
timestamp: Date.now(),
});
await logRef.update({
fullLog,
});
},
success: async () => {
console.log("streamLogger marked as SUCCESS");
await logRef.update({
status: "SUCCESS",
successTimeStamp: Date.now(),
});
},
fail: async () => {
console.log("streamLogger marked as FAIL");
await logRef.update({
status: "FAIL",
failTimeStamp: Date.now(),
});
},
};
}
export {
commandErrorHandler,
logErrorToDB,
parseSparksConfig,
createStreamLogger,
};

View File

@@ -19,6 +19,7 @@
"@monaco-editor/react": "^4.1.0",
"@tinymce/tinymce-react": "^3.4.0",
"algoliasearch": "^4.8.6",
"ansi-to-react": "^6.1.5",
"chroma-js": "^2.1.0",
"csv-parse": "^4.15.3",
"date-fns": "^2.19.0",
@@ -30,6 +31,7 @@
"json2csv": "^5.0.6",
"jszip": "^3.6.0",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"query-string": "^6.8.3",
"react": "^16.9.0",
"react-beautiful-dnd": "^13.0.0",

View File

@@ -64,24 +64,6 @@ const useStyles = makeStyles((theme) =>
...theme.typography.body1,
// https://codepen.io/evank/pen/wWbRNO
background: `
linear-gradient(
var(--bg-paper) 50%,
${fade(theme.palette.background.paper, 0)}
),
linear-gradient(
${fade(theme.palette.background.paper, 0)},
var(--bg-paper) 50%
) 0 100%,
linear-gradient(
to top, ${theme.palette.divider} 1px,
${fade(theme.palette.divider, 0)}
),
linear-gradient(to top,
${theme.palette.divider} 1px,
${fade(theme.palette.divider, 0)}
) 0 calc(100% - 0.5px)`,
backgroundRepeat: "no-repeat",
backgroundColor: "var(--bg-paper)",
backgroundSize: "100% 2px, 100% 3px, 100% 1px, 100% 1px",

View File

@@ -142,15 +142,17 @@ export default function SparksEditor() {
</>
}
actions={{
primary:showForceSave ? {
children: "Force Save",
onClick: handleSave,
}:{
children: "Save Changes",
onClick: handleSave,
disabled:!isSparksValid || localSparks === tableState?.config.sparks ,
},
primary: showForceSave
? {
children: "Force Save",
onClick: handleSave,
}
: {
children: "Save Changes",
onClick: handleSave,
disabled:
!isSparksValid || localSparks === tableState?.config.sparks,
},
secondary: {
children: "Cancel",
onClick: handleClose,

View File

@@ -0,0 +1,256 @@
import React, { useState } from "react";
import useRouter from "hooks/useRouter";
import useTable from "hooks/useFiretable/useTable";
import { useFiretableContext } from "contexts/FiretableContext";
import _camelCase from "lodash/camelCase";
import _get from "lodash/get";
import _find from "lodash/find";
import _sortBy from "lodash/sortBy";
import moment from "moment";
import {
Chip,
CircularProgress,
Typography,
Box,
Tabs,
Tab,
Button,
} from "@material-ui/core";
import Modal from "components/Modal";
import { makeStyles } from "@material-ui/core/styles";
import LogsIcon from "@material-ui/icons/QueryBuilder";
import SuccessIcon from "@material-ui/icons/CheckCircle";
import FailIcon from "@material-ui/icons/Cancel";
import TableHeaderButton from "./TableHeaderButton";
import Ansi from "ansi-to-react";
import EmptyState from "components/EmptyState";
import PropTypes from "prop-types";
function a11yProps(index) {
return {
id: `vertical-tab-${index}`,
"aria-controls": `vertical-tabpanel-${index}`,
};
}
const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1,
backgroundColor: theme.palette.background.paper,
display: "flex",
height: `calc(100vh - 200px)`,
},
tabs: {
borderRight: `1px solid ${theme.palette.divider}`,
},
tab: {
display: "flex",
flexWrap: "nowrap",
alignItems: "center",
justifyItems: "center",
},
logPanel: {
width: "100%",
},
logEntryWrapper: {
overflowY: "scroll",
maxHeight: "100%",
},
logNumber: {
float: "left",
width: "3em",
textAlign: "right",
paddingRight: "1em",
},
logEntry: {
lineBreak: "anywhere",
paddingLeft: "3em",
whiteSpace: "break-spaces",
userSelect: "text",
},
logTypeInfo: {
color: "green",
},
logTypeError: {
color: "red",
},
}));
LogPanel.propTypes = {
logs: PropTypes.array,
status: PropTypes.string,
index: PropTypes.any.isRequired,
value: PropTypes.any.isRequired,
};
function LogRow({ logRecord, index }) {
const classes = useStyles();
return (
<Box>
<Typography variant="body2" className={classes.logNumber}>
{index}
</Typography>
<Typography variant="body2" className={classes.logEntry}>
<Ansi
className={
logRecord.level === "info"
? classes.logTypeInfo
: classes.logTypeError
}
>
{moment(logRecord.timestamp).format("LTS")}
</Ansi>
{" "}
<Ansi>
{logRecord.log.replaceAll("\\n", "\n").replaceAll("\\t", "\t")}
</Ansi>
</Typography>
</Box>
);
}
function LogPanel(props) {
const { logs, status, value, index, ...other } = props;
const classes = useStyles();
return (
<div
role="tabpanel"
hidden={value !== index}
id={`vertical-tabpanel-${index}`}
aria-labelledby={`vertical-tab-${index}`}
className={classes.logPanel}
{...other}
>
{value === index && (
<Box p={3} className={classes.logEntryWrapper}>
{logs?.map((log, index) => {
return <LogRow logRecord={log} index={index} />;
})}
</Box>
)}
</div>
);
}
export default function TableLogs() {
const router = useRouter();
const { tableState } = useFiretableContext();
const classes = useStyles();
const [open, setOpen] = useState(false);
const [tabIndex, setTabIndex] = React.useState(0);
const tableCollection = decodeURIComponent(router.match.params.id);
const ftBuildStreamID =
"_FIRETABLE_/settings/schema/" +
tableCollection
.split("/")
.filter(function (_, i) {
// replace IDs with subTables that appears at even indexes
return i % 2 === 0;
})
.join("/subTables/");
const [collectionState] = useTable({
path: `${ftBuildStreamID}/ftBuildLogs`,
orderBy: [{ key: "startTimeStamp", direction: "desc" }],
});
const latestStatus = collectionState?.rows?.[0]?.status;
const handleTabChange = (event, newValue) => {
setTabIndex(newValue);
};
const handleClose = () => {
setOpen(false);
};
return (
<>
<TableHeaderButton
title="Build Logs"
onClick={() => setOpen(true)}
icon={
<>
{latestStatus === "BUILDING" && <CircularProgress size={20} />}
{latestStatus === "SUCCESS" && <SuccessIcon />}
{latestStatus === "FAIL" && <FailIcon />}
{!latestStatus && <LogsIcon />}
</>
}
/>
{open && !!tableState && (
<Modal
onClose={handleClose}
maxWidth="xl"
fullWidth
title={
<>
Build Logs <Chip label="ALPHA" size="small" />
</>
}
children={
<>
{!latestStatus && (
<EmptyState
message="No Logs Found"
description={
"When you start building, your logs should be shown here shortly"
}
/>
)}
{latestStatus && (
<div className={classes.root}>
<Tabs
orientation="vertical"
variant="scrollable"
value={tabIndex}
onChange={handleTabChange}
className={classes.tabs}
>
{collectionState.rows.map((logEntry, index) => (
<Tab
label={
<Box className={classes.tab}>
<Box>
{moment(logEntry.startTimeStamp).format(
"MMMM D YYYY h:mm:ssa"
)}
</Box>
<Box>
{logEntry.status === "BUILDING" && (
<CircularProgress size={24} />
)}
{logEntry.status === "SUCCESS" && <SuccessIcon />}
{logEntry.status === "FAIL" && <FailIcon />}
</Box>
</Box>
}
{...a11yProps(index)}
/>
))}
</Tabs>
{collectionState.rows.map((logEntry, index) => (
<LogPanel
value={tabIndex}
index={index}
logs={logEntry?.fullLog}
status={logEntry?.status}
/>
))}
</div>
)}
</>
}
/>
)}
</>
);
}

View File

@@ -18,6 +18,7 @@ import Filters from "../Filters";
import ImportCSV from "./ImportCsv";
import Export from "./Export";
import TableSettings from "./TableSettings";
import TableLogs from "./TableLogs";
import HiddenFields from "../HiddenFields";
import Sparks from "./Sparks";
import ReExecute from "./ReExecute";
@@ -219,6 +220,10 @@ export default function TableHeader({
</Grid>
)}
<Grid item>
<TableLogs />
</Grid>
<Grid item>
<TableSettings />
</Grid>

View File

@@ -3109,6 +3109,11 @@ alphanum-sort@^1.0.0:
resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=
anser@^1.4.1:
version "1.4.10"
resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.10.tgz#befa3eddf282684bd03b63dcda3927aef8c2e35b"
integrity sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==
ansi-align@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f"
@@ -3192,6 +3197,14 @@ ansi-styles@^4.1.0:
"@types/color-name" "^1.1.1"
color-convert "^2.0.1"
ansi-to-react@^6.1.5:
version "6.1.5"
resolved "https://registry.yarnpkg.com/ansi-to-react/-/ansi-to-react-6.1.5.tgz#00ca9e4f566b468f0aa6a213f34b752b3e7121a9"
integrity sha512-u7fkGMK+p6keOtgExruUDVmEnTBAWizlHJXzx9eJxw4rCVE6omzm0Gw4BQmB9oUPr0WriYf7Wyprms8uL8eL5Q==
dependencies:
anser "^1.4.1"
escape-carriage "^1.3.0"
ansicolors@~0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979"
@@ -5921,6 +5934,11 @@ escalade@^3.1.0:
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
escape-carriage@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/escape-carriage/-/escape-carriage-1.3.0.tgz#71006b2d4da8cb6828686addafcb094239c742f3"
integrity sha512-ATWi5MD8QlAGQOeMgI8zTp671BG8aKvAC0M7yenlxU4CRLGO/sKthxVUyjiOFKjHdIo+6dZZUNFgHFeVEaKfGQ==
escape-goat@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675"
@@ -10136,6 +10154,11 @@ mkdirp@^1.0.3:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
moment@^2.29.1:
version "2.29.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
monaco-editor@^0.21.2:
version "0.21.2"
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.21.2.tgz#37054e63e480d51a2dd17d609dcfb192304d5605"