Use query parameters for year, day and mode. Handle file stream message.

This commit is contained in:
Mikaël Capelle 2024-12-10 16:26:04 +00:00
parent cb69fea7ac
commit eb64edb22f
10 changed files with 351 additions and 153 deletions

View File

@ -6,7 +6,8 @@ import subprocess
from pathlib import Path from pathlib import Path
from typing import Any, Literal, TypeAlias from typing import Any, Literal, TypeAlias
from fastapi import FastAPI, Form from fastapi import FastAPI, Form, HTTPException
from fastapi.responses import FileResponse
from sse_starlette import EventSourceResponse from sse_starlette import EventSourceResponse
DataMode: TypeAlias = Literal["holt59", "tests"] DataMode: TypeAlias = Literal["holt59", "tests"]
@ -14,6 +15,7 @@ DataMode: TypeAlias = Literal["holt59", "tests"]
LOGGER = logging.getLogger("uvicorn.info") LOGGER = logging.getLogger("uvicorn.info")
GIT_AOC_PATH = Path("/code") GIT_AOC_PATH = Path("/code")
AOC_FILES_PATH = Path("/files")
def update_repository(): def update_repository():
@ -54,6 +56,16 @@ async def data(year: int, day: int, mode: DataMode = "tests"):
return {"content": read_test_file(year, day, mode)} return {"content": read_test_file(year, day, mode)}
@app.get("/file")
async def file(name: str):
path = AOC_FILES_PATH.joinpath(name).resolve()
if path.parent != AOC_FILES_PATH:
raise HTTPException(403)
return FileResponse(path)
@app.post("/submit-sse") @app.post("/submit-sse")
async def submit_sse(year: int = Form(), day: int = Form(), input: str = Form()): async def submit_sse(year: int = Form(), day: int = Form(), input: str = Form()):
data = input.rstrip().replace("\r\n", "\n") data = input.rstrip().replace("\r\n", "\n")
@ -62,6 +74,8 @@ async def submit_sse(year: int = Form(), day: int = Form(), input: str = Form())
"holt59-aoc", "holt59-aoc",
"--stdin", "--stdin",
"--api", "--api",
"--output",
AOC_FILES_PATH,
"--year", "--year",
str(year), str(year),
str(day), str(day),

View File

@ -13,13 +13,16 @@
"format": "prettier --write src/" "format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"@viz-js/viz": "^3.11.0",
"ansi-up": "^1.0.0",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"bootstrap-icons": "^1.11.3", "bootstrap-icons": "^1.11.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"luxon": "^3.5.0", "luxon": "^3.5.0",
"pinia": "^2.2.6", "pinia": "^2.2.6",
"sse.js": "^2.5.0", "sse.js": "^2.5.0",
"vue": "^3.5.13" "vue": "^3.5.13",
"vue-router": "4"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node22": "^22.0.0", "@tsconfig/node22": "^22.0.0",

View File

@ -1,134 +1,3 @@
<script setup lang="ts">
import { DateTime } from 'luxon';
import ResultVue from '@/components/ResultVue.vue'
import { loadData } from '@/api';
import { range } from 'lodash';
import { onMounted, ref, watch } from 'vue';
const inAocPeriod = (date: DateTime) => {
return !(date.day > 25 || date.month != 12);
}
const currentInput = ref("");
const showLogs = ref(false);
const today = DateTime.now();
const maxYear = today.month == 12 ? today.year : today.year - 1;
const initialDay = ref(inAocPeriod(today) ? today.day : 1);
const initialYear = ref(maxYear);
const currentDay = ref(initialDay.value);
const currentYear = ref(maxYear);
const years = range(2015, maxYear + 1);
const days = range(1, 26);
// handle current day clicked
const todayClicked = () => {
const today = DateTime.now();
initialDay.value = inAocPeriod(today) ? today.day : 1;
initialYear.value = today.month == 12 ? today.year : today.year - 1;
currentDay.value = initialDay.value;
currentYear.value = initialYear.value;
};
// handle load data + load data of current day on startup
const dataLoading = ref(false);
const lastInputLoaded = ref("");
const lastModeLoaded = ref<"holt59" | "tests">("tests");
const loadDataClicked = async (mode: "holt59" | "tests" = "tests") => {
dataLoading.value = true;
currentInput.value = await loadData(currentYear.value, currentDay.value, mode);
dataLoading.value = false;
lastModeLoaded.value = mode;
lastInputLoaded.value = currentInput.value;
};
onMounted(loadDataClicked);
// handle change of day/year
watch([currentDay, currentYear], () => {
if (currentInput.value == lastInputLoaded.value) {
loadDataClicked(lastModeLoaded.value);
}
});
// handle submit
const resultComponent = ref<typeof ResultVue>();
const submitRunning = ref(false);
const submitClicked = async () => {
submitRunning.value = true;
await resultComponent.value!.submit(currentYear.value, currentDay.value, currentInput.value, () => { });
submitRunning.value = false;
};
</script>
<template> <template>
<nav class="container mt-3"> <RouterView />
<h1>Advent-Of-Code &mdash; Holt59</h1>
<hr />
</nav>
<ResultVue ref="resultComponent" :show-logs="showLogs" />
<section class="container d-flex flex-column flex-grow-1 position-relative">
<form method="POST" class="d-flex flex-column flex-grow-1">
<div class="row mb-2 align-items-center">
<div class="col-sm-2">
<button class="form-control btn btn-outline-info" @click.prevent="todayClicked"
:disabled="initialYear == currentYear && initialDay == currentDay">Today</button>
</div>
<div class="col-sm-2">
<select class="form-select" autocomplete="off" v-model="currentYear">
<option v-for="year in years" :key="year" :value="year">{{ year }}</option>
</select>
</div>
<div class="col-sm-2">
<select class="form-select" autocomplete="off" v-model="currentDay">
<option v-for="day in days" :key="day" :value="day">{{ day }}</option>
</select>
</div>
<div class="col-sm-2">
<button class="form-control btn btn-success" id="load-data"
@click.prevent="() => { loadDataClicked('tests') }"
@contextmenu.prevent="() => { loadDataClicked('holt59') }">
<span class="spinner-border spinner-border-sm" :class="{ 'd-none': !dataLoading }" role="status"
aria-hidden="true"></span>
<span class="submit-text" :class="{ 'd-none': dataLoading }">Test Data</span></button>
</div>
<div class="col-sm-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="showLogs" v-model="showLogs">
<label class="form-check-label" for="showLogs">
Show Logs
</label>
</div>
</div>
<div class="col-sm-2">
<button type="submit" class="form-control btn btn-primary" @click.prevent="submitClicked">
<span class="spinner-border spinner-border-sm" :class="{ 'd-none': !submitRunning }" role="status"
aria-hidden="true"></span>
<span class="submit-text" :class="{ 'd-none': submitRunning }">Go!</span></button>
</div>
</div>
<textarea class="area-input flex-grow-1 w-100" v-model="currentInput"></textarea>
</form>
</section>
</template> </template>
<style scoped>
.area-input {
resize: none;
font-family: var(--bs-font-monospace);
font-size: .9em;
}
</style>

View File

@ -32,6 +32,7 @@ export const submit = async (year: number, day: number, input: string, startCall
}); });
eventSource.addEventListener('message', function (e: { data: string }) { eventSource.addEventListener('message', function (e: { data: string }) {
try {
const data = JSON.parse(e.data); const data = JSON.parse(e.data);
if (data.event == "complete") { if (data.event == "complete") {
@ -43,6 +44,11 @@ export const submit = async (year: number, day: number, input: string, startCall
else if (data.event == "stream") { else if (data.event == "stream") {
streamCallback(data.data); streamCallback(data.data);
} }
}
catch (exception) {
console.log(e.data);
console.log(exception);
}
}); });
eventSource.stream(); eventSource.stream();

View File

@ -40,9 +40,20 @@ type _LogStream = {
} }
}; };
type _FileStream = {
type: "file";
time: string;
content: {
filename: string;
size: number;
}
};
type _ErrorStream = { type _ErrorStream = {
type: "error"; type: "error";
time: string;
message: string; message: string;
}; };
export type StreamMessage = _AnswerStream | _ProgressStream | _LogStream | _ErrorStream; export type StreamMessage = _AnswerStream | _ProgressStream | _LogStream | _ErrorStream | _FileStream;

View File

@ -0,0 +1,188 @@
<script setup lang="ts">
import { DateTime } from 'luxon';
import ResultVue from '@/components/ResultVue.vue'
import { loadData } from '@/api';
import { range } from 'lodash';
import { onMounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const inAocPeriod = (date: DateTime) => {
return !(date.day > 25 || date.month != 12);
}
const currentInput = ref("");
const showLogs = ref(false);
const today = DateTime.now();
const maxYear = today.month == 12 ? today.year : today.year - 1;
const years = range(2015, maxYear + 1);
const days = range(1, 26);
const initialDay = ref(1);
const initialYear = ref(maxYear);
const currentDay = ref(1);
const currentYear = ref(maxYear);
const currentLoadMode = ref<"holt59" | "tests">("tests");
// mounted
onMounted(() => {
const query = router.currentRoute.value.query;
const day = parseInt(query.day as string);
if (day && day >= 1 && day <= 25) {
initialDay.value = day;
}
else {
initialDay.value = inAocPeriod(today) ? today.day as number : 1;
}
const year = parseInt(query.year as string);
if (year && year >= 2015 && year <= maxYear) {
initialYear.value = year;
}
else {
initialYear.value = maxYear;
}
const mode = query.data as string;
if (["tests", "holt59"].includes(mode)) {
currentLoadMode.value = mode as "tests" | "holt59";
}
currentDay.value = initialDay.value;
currentYear.value = initialYear.value;
});
// handle current day clicked
const todayClicked = () => {
const today = DateTime.now();
initialDay.value = inAocPeriod(today) ? today.day : 1;
initialYear.value = today.month == 12 ? today.year : today.year - 1;
currentDay.value = initialDay.value;
currentYear.value = initialYear.value;
};
// handle load data + load data of current day on startup
const dataLoading = ref(false);
const lastInputLoaded = ref("");
const lastModeLoaded = ref<"holt59" | "tests">("tests");
const loadDataClicked = async () => {
dataLoading.value = true;
currentInput.value = await loadData(currentYear.value, currentDay.value, currentLoadMode.value);
dataLoading.value = false;
lastModeLoaded.value = currentLoadMode.value;
lastInputLoaded.value = currentInput.value;
};
onMounted(loadDataClicked);
// handle change of day/year
watch([currentDay, currentYear], () => {
if (currentInput.value == lastInputLoaded.value && currentLoadMode.value == lastModeLoaded.value) {
loadDataClicked();
}
router.push({ name: router.currentRoute.value.name, query: { "year": currentYear.value, "day": currentDay.value, "data": currentLoadMode.value } });
});
watch([currentLoadMode], () => {
loadDataClicked();
router.push({ name: router.currentRoute.value.name, query: { "year": currentYear.value, "day": currentDay.value, "data": currentLoadMode.value } });
});
// handle submit
const resultComponent = ref<typeof ResultVue>();
const submitRunning = ref(false);
const submitClicked = async () => {
submitRunning.value = true;
await resultComponent.value!.submit(currentYear.value, currentDay.value, currentInput.value, () => { });
submitRunning.value = false;
};
</script>
<template>
<nav class="container mt-3">
<h1>Advent-Of-Code &mdash; Holt59</h1>
<hr />
</nav>
<ResultVue ref="resultComponent" :show-logs="showLogs" />
<section class="container d-flex flex-column flex-grow-1 position-relative">
<form method="POST" class="d-flex flex-column flex-grow-1">
<div class="row mb-2 align-items-center">
<div class="col-sm-2">
<button class="form-control btn btn-outline-info" @click.prevent="todayClicked"
:disabled="initialYear == currentYear && initialDay == currentDay">Today</button>
</div>
<div class="col-sm-2">
<select class="form-select" autocomplete="off" v-model="currentYear">
<option v-for="year in years" :key="year" :value="year">{{ year }}</option>
</select>
</div>
<div class="col-sm-2">
<select class="form-select" autocomplete="off" v-model="currentDay">
<option v-for="day in days" :key="day" :value="day">{{ day }}</option>
</select>
</div>
<div class="col-sm-2">
<div class="btn-group w-100">
<button class="form-control btn btn-success" id="load-data"
@click.prevent="() => { loadDataClicked() }">
<span class="spinner-border spinner-border-sm" :class="{ 'd-none': !dataLoading }"
role="status" aria-hidden="true"></span>
<span class="submit-text" :class="{ 'd-none': dataLoading }">Load ({{ currentLoadMode
}})</span></button>
<button type="button" class="btn btn-success dropdown-toggle dropdown-toggle-split"
data-bs-toggle="dropdown" aria-expanded="false">
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#"
@click.prevent="() => { currentLoadMode = 'tests' }">Tests</a>
</li>
<li><a class="dropdown-item" href="#"
@click.prevent="() => { currentLoadMode = 'holt59' }">Holt59</a>
</li>
</ul>
</div>
</div>
<div class="col-sm-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="showLogs" v-model="showLogs">
<label class="form-check-label" for="showLogs">
Show Logs
</label>
</div>
</div>
<div class="col-sm-2">
<button type="submit" class="form-control btn btn-primary" @click.prevent="submitClicked">
<span class="spinner-border spinner-border-sm" :class="{ 'd-none': !submitRunning }"
role="status" aria-hidden="true"></span>
<span class="submit-text" :class="{ 'd-none': submitRunning }">Go!</span></button>
</div>
</div>
<textarea class="area-input flex-grow-1 w-100" v-model="currentInput"></textarea>
</form>
</section>
</template>
<style scoped>
.area-input {
resize: none;
font-family: var(--bs-font-monospace);
font-size: .9em;
}
</style>

View File

@ -3,8 +3,11 @@
import * as Api from '@/api'; import * as Api from '@/api';
import type { StreamMessage } from '@/api/models'; import type { StreamMessage } from '@/api/models';
import type { DateTime } from 'luxon'; import type { DateTime } from 'luxon';
import { h, reactive, ref, type Reactive, type VNode } from 'vue'; import { h, reactive, ref, watch, type Reactive, type VNode } from 'vue';
import { AnsiUp } from 'ansi-up';
import ResultProgressLine from './ResultProgressLine.vue'; import ResultProgressLine from './ResultProgressLine.vue';
import * as boostrap from 'bootstrap';
import { instance } from "@viz-js/viz";
const props = defineProps<{ const props = defineProps<{
showLogs: boolean showLogs: boolean
@ -24,6 +27,16 @@ const contentRows = ref<Array<() => VNode>>([]);
const contentProgress: Map<number, Reactive<{ progress: number }>> = new Map(); const contentProgress: Map<number, Reactive<{ progress: number }>> = new Map();
const contentErrors = ref(""); const contentErrors = ref("");
const fileModal = ref<HTMLDivElement>();
const bootstrapFileModal = ref<boostrap.Modal>();
const currentFilename = ref("");
const currentFilecontent = ref<Element>();
watch([fileModal], () => {
// @ts-expect-error bad typing
bootstrapFileModal.value = new boostrap.Modal(fileModal.value, {});
})
const addRow = (node: () => VNode) => { const addRow = (node: () => VNode) => {
contentRows.value = contentRows.value.concat(node); contentRows.value = contentRows.value.concat(node);
} }
@ -35,6 +48,32 @@ const startCallback = (time: DateTime, commit: string) => {
addRow(() => h("p", {}, ["Computing part 1... "])); addRow(() => h("p", {}, ["Computing part 1... "]));
} }
const fileBuilders: { [extension: string]: (content: string) => Promise<Element> } = {
txt: async (content: string) => {
const ansiUp = new AnsiUp();
ansiUp.escapeForHtml = false;
const ansiHtml = ansiUp.ansi_to_html(content.replaceAll("\n", "<br/>"));
const codeElement = document.createElement("code")
codeElement.classList.add("d-block")
codeElement.style.fontSize = ".84em"
codeElement.innerHTML = ansiHtml
return codeElement
},
dot: async (content: string) => {
return instance().then(viz => {
const e = viz.renderSVGElement(content)
e.style.maxWidth = "100%";
e.style.height = "auto";
return e;
});
}
}
const maxLogs = 100;
const logCounter = ref(0);
const streamCallback = (message: StreamMessage) => { const streamCallback = (message: StreamMessage) => {
switch (message.type) { switch (message.type) {
case "progress-start": case "progress-start":
@ -49,11 +88,42 @@ const streamCallback = (message: StreamMessage) => {
contentProgress.get(message.content.counter)!.progress = 100; contentProgress.get(message.content.counter)!.progress = 100;
break; break;
case "log": case "log":
logCounter.value += 1;
if (logCounter.value < maxLogs) {
addRow(() => h("p", { "class": { "log": true, "d-none": !props.showLogs } }, [`[${message.content.level}] ${message.time}: ${message.content.message}`])) addRow(() => h("p", { "class": { "log": true, "d-none": !props.showLogs } }, [`[${message.content.level}] ${message.time}: ${message.content.message}`]))
}
else if (logCounter.value == maxLogs) {
addRow(() => h("p", { "class": { "log": true, "d-none": !props.showLogs } }, [`[${message.content.level}] ${message.time}: Too many logs, capturing stopped.`]))
}
break; break;
case "error": case "error":
contentErrors.value = contentErrors.value + message.message; contentErrors.value = contentErrors.value + message.message;
break; break;
case "file":
addRow(() => h(
"p", {}, [`File `, h('a', {
href: Api.urlFor(`file?name=${message.content.filename}`),
target: "_blank",
onClick: async (e) => {
const href = (e.target as HTMLAnchorElement).href as string;
const extension = href.substring(href.lastIndexOf(".") + 1)
console.log(extension)
if (!fileBuilders.hasOwnProperty(extension)) {
return;
}
e.preventDefault();
currentFilename.value = href.split("=")[1];
currentFilecontent.value = await fetch(href).then(r => r.text()).then(fileBuilders[extension])
bootstrapFileModal.value!.show();
}
}, [message.content.filename]), ` (${message.content.size} bytes) available.`],
));
break;
case "answer": { case "answer": {
const value = message.content.value; const value = message.content.value;
if (value.trim().indexOf("\n") >= 0) { if (value.trim().indexOf("\n") >= 0) {
@ -62,7 +132,6 @@ const streamCallback = (message: StreamMessage) => {
h('pre', {}, [value]), h('pre', {}, [value]),
`...found in ${message.content.answerTime_s} s.`] `...found in ${message.content.answerTime_s} s.`]
)); ));
} }
else { else {
addRow(() => h( addRow(() => h(
@ -79,6 +148,7 @@ const streamCallback = (message: StreamMessage) => {
} }
const submit = async (year: number, day: number, input: string) => { const submit = async (year: number, day: number, input: string) => {
logCounter.value = 0;
answer1Value.value = null; answer1Value.value = null;
answer2Value.value = null; answer2Value.value = null;
contentErrors.value = ""; contentErrors.value = "";
@ -111,18 +181,19 @@ defineExpose({ submit });
</section> </section>
<Teleport to="body"> <Teleport to="body">
<div class="modal fade" id="logsModal" tabindex="-1" aria-labelledby="logsModalLabel" aria-hidden="true"> <div class="modal fade" id="logsModal" tabindex="-1" aria-labelledby="logsModalLabel" aria-hidden="true"
ref="fileModal">
<div class="modal-dialog modal-xl"> <div class="modal-dialog modal-xl">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h1 class="modal-title fs-5" id="logsModalLabel">Logs</h1> <h1 class="modal-title fs-5" id="logsModalLabel">{{ currentFilename }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body text-center" v-html="currentFilecontent?.outerHTML">
<code class="stderr text-danger" id="data-logs"><pre>{{ '' }}</pre></code>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</Teleport> </Teleport>
</template> </template>

View File

@ -6,9 +6,23 @@ import 'bootstrap-icons/font/bootstrap-icons.css'
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import App from './App.vue' import App from './App.vue'
import MainPage from '@/components/MainPage.vue'
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
name: 'home',
component: MainPage
}
]
})
const app = createApp(App) const app = createApp(App)
app.use(createPinia()) app.use(createPinia())
app.use(router)
app.mount('#app') app.mount('#app')

View File

@ -10,6 +10,11 @@
], ],
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"lib": [
"ES2021",
"DOM",
"DOM.Iterable"
],
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": [ "@/*": [

View File

@ -769,6 +769,11 @@
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz#d1491f678ee3af899f7ae57d9c21dc52a65c7133" resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz#d1491f678ee3af899f7ae57d9c21dc52a65c7133"
integrity sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ== integrity sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==
"@viz-js/viz@^3.11.0":
version "3.11.0"
resolved "https://registry.yarnpkg.com/@viz-js/viz/-/viz-3.11.0.tgz#516a3fb644ca871762bcb86e2b41b6d107869c20"
integrity sha512-3zoKLQUqShIhTPvBAIIgJUf5wO9aY0q+Ftzw1u26KkJX1OJjT7Z5VUqgML2GIzXJYFgjqS6a2VREMwrgChuubA==
"@volar/language-core@2.4.10", "@volar/language-core@~2.4.8": "@volar/language-core@2.4.10", "@volar/language-core@~2.4.8":
version "2.4.10" version "2.4.10"
resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.4.10.tgz#7d57c29d27f7bce2fa7eb9f3a1fc053a3e28e53f" resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.4.10.tgz#7d57c29d27f7bce2fa7eb9f3a1fc053a3e28e53f"
@ -872,7 +877,7 @@
de-indent "^1.0.2" de-indent "^1.0.2"
he "^1.2.0" he "^1.2.0"
"@vue/devtools-api@^6.6.3": "@vue/devtools-api@^6.6.3", "@vue/devtools-api@^6.6.4":
version "6.6.4" version "6.6.4"
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343" resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g== integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
@ -1026,6 +1031,11 @@ ansi-styles@^6.2.1:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
ansi-up@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/ansi-up/-/ansi-up-1.0.0.tgz#235aa25b95f997291f2f6335e95485cae339a356"
integrity sha512-UItk+l+fKaa9th5dM/1uqeJGS7QfEPutfQB8kGBGUZo2GhF6u4q40bGa5+cQI7Jscs8L6FP8ue6+GflduCbLRQ==
anymatch@~3.1.2: anymatch@~3.1.2:
version "3.1.3" version "3.1.3"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
@ -2424,6 +2434,13 @@ vue-eslint-parser@^9.4.3:
lodash "^4.17.21" lodash "^4.17.21"
semver "^7.3.6" semver "^7.3.6"
vue-router@4:
version "4.5.0"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.5.0.tgz#58fc5fe374e10b6018f910328f756c3dae081f14"
integrity sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==
dependencies:
"@vue/devtools-api" "^6.6.4"
vue-tsc@^2.1.10: vue-tsc@^2.1.10:
version "2.1.10" version "2.1.10"
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.1.10.tgz#4d61a64e5fad763b8b40c1884259fd48986f0b4e" resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.1.10.tgz#4d61a64e5fad763b8b40c1884259fd48986f0b4e"