add basic views into stacks

this adds a way to view the timeline, branches and stacks, create stacks and more. none of this is pretty, but we're just exercising the server APIs
This commit is contained in:
Scott Chacon 2024-09-23 16:26:25 +02:00
parent 4c11a96456
commit d2444de8c5
14 changed files with 877 additions and 28 deletions

View File

@ -23,6 +23,9 @@
"vite": "catalog:"
},
"dependencies": {
"@sentry/sveltekit": "^8.9.2"
"@sentry/sveltekit": "^8.9.2",
"highlight.js": "^11.10.0",
"marked": "^10.0.0",
"moment": "^2.30.1"
}
}

View File

@ -23,9 +23,13 @@
<h2>GitButler</h2>
</a>
<div>
<a href="/user">User</a>
|
<a href="/downloads">Downloads</a>
{#if $token}
|
<a href="/projects">Projects</a>
|
<a href="/user">User</a>
{/if}
</div>
<div class="nav__right">
<button

View File

@ -52,11 +52,13 @@ a:hover {
h1 {
font-size: 2rem;
text-align: center;
}
h2 {
font-size: 1rem;
font-size: 1.8rem;
margin: 0;
margin-bottom: 8px;
padding: 0;
}
pre {

View File

@ -31,6 +31,7 @@
<div class="app">
<Navigation />
<main>
{@render children()}
</main>
@ -53,7 +54,7 @@
flex-direction: column;
padding: 1rem;
width: 100%;
max-width: 64rem;
max-width: 84rem;
margin: 0 auto;
}

View File

@ -0,0 +1,52 @@
<script lang="ts">
import moment from 'moment';
import { onMount } from 'svelte';
import { env } from '$env/dynamic/public';
let state = 'loading';
let projects: any = {};
onMount(() => {
let key = localStorage.getItem('gb_access_token');
if (key) {
fetch(env.PUBLIC_APP_HOST + 'api/projects', {
method: 'GET',
headers: {
'X-AUTH-TOKEN': key || ''
}
})
.then(async (response) => await response.json())
.then((data) => {
console.log(data);
projects = data;
state = 'loaded';
setTimeout(() => {
let dtime = document.querySelectorAll('.dtime');
dtime.forEach((element) => {
console.log(element.innerHTML);
element.innerHTML = moment(element.innerHTML).fromNow();
});
}, 100);
});
} else {
state = 'unauthorized';
}
});
</script>
{#if state === 'loading'}
<p>Loading...</p>
{:else if state === 'unauthorized'}
<p>Unauthorized</p>
{:else}
{#each projects as project}
<div>
<h2><a href="/projects/{project.repository_id}">{project.name}</a></h2>
<p>{project.repository_id}</p>
<p>{project.description}</p>
<p>Created: <span class="dtime">{project.created_at}</span></p>
<p>Updated: <span class="dtime">{project.updated_at}</span></p>
</div>
<hr />
{/each}
{/if}

View File

@ -0,0 +1,157 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { env } from '$env/dynamic/public';
let state = 'loading';
let timeline: any = {};
let patchStacks: any = {};
let project: any = {};
let projectId: string;
export let data: any;
function createPatchStack(branch: string, sha: string) {
let key = localStorage.getItem('gb_access_token');
let opts = {
method: 'POST',
headers: {
'X-AUTH-TOKEN': key || '',
'Content-Type': 'application/json'
},
body: JSON.stringify({
project_id: projectId,
branch_id: branch,
oplog_sha: sha
})
};
if (key) {
fetch(env.PUBLIC_APP_HOST + 'api/patch_stack', opts)
.then(async (response) => await response.json())
.then((data) => {
console.log(data);
goto('/projects/' + projectId + '/branches/' + data.branch_id + '/stack');
});
} else {
state = 'unauthorized';
}
}
onMount(() => {
let key = localStorage.getItem('gb_access_token');
projectId = data.projectId;
console.log(projectId);
if (key) {
fetch(env.PUBLIC_APP_HOST + 'api/projects/' + projectId, {
method: 'GET',
headers: {
'X-AUTH-TOKEN': key || ''
}
})
.then(async (response) => await response.json())
.then((data) => {
console.log(data);
project = data;
state = 'loaded';
});
fetch(env.PUBLIC_APP_HOST + 'api/patch_stack/' + projectId, {
method: 'GET',
headers: {
'X-AUTH-TOKEN': key || ''
}
})
.then(async (response) => await response.json())
.then((data) => {
console.log(data);
patchStacks = data;
});
fetch(env.PUBLIC_APP_HOST + 'api/timeline/' + projectId, {
method: 'GET',
headers: {
'X-AUTH-TOKEN': key || ''
}
})
.then(async (response) => await response.json())
.then((data) => {
console.log(data);
timeline = data;
state = 'loaded';
});
} else {
state = 'unauthorized';
}
});
</script>
{#if state === 'loading'}
<p>Loading...</p>
{:else if state === 'unauthorized'}
<p>Unauthorized</p>
{:else}
<h2>Project</h2>
<div>{project.name}</div>
<div class="columns">
<div class="column">
<h2>Patch Stacks</h2>
{#each patchStacks as stack}
<div>
{stack.title}<br />
<a href="/projects/{data.projectId}/branches/{stack.branch_id}/stack">{stack.branch_id}</a
><br />
{stack.stack_size} patches<br />
{stack.contributors}<br />
v{stack.version}<br />
{stack.created_at}<br />
</div>
<hr />
{/each}
</div>
<div class="column">
<h2>Timeline</h2>
{#each timeline as event}
<div class="event">
<pre>{event.sha}</pre>
<div>{event.time}</div>
<pre>{event.message}</pre>
<div>{event.trailers}</div>
{#if Object.keys(event.files).length > 0}
<h3>Branches</h3>
{#each Object.keys(event.files) as branch}
{#if event.branch_data.branches[branch]}
<h3>Branch {event.branch_data.branches[branch].name}</h3>
<button on:click={() => createPatchStack(branch, event.sha)}
>Create Patch Stack</button
>
{#each Object.keys(event.files[branch]) as file}
<li>{file} ({event.files[branch][file]})</li>
{/each}
{/if}
{/each}
{/if}
</div>
<hr />
{/each}
</div>
</div>
{/if}
<style>
h2 {
margin-bottom: 1rem;
}
.event {
margin-bottom: 1rem;
}
hr {
margin: 1rem 0;
}
.columns {
display: flex;
}
.column {
flex: 1;
padding: 1rem;
}
</style>

View File

@ -0,0 +1,3 @@
export const load: any = ({ params }) => {
return { projectId: params.projectId };
};

View File

@ -0,0 +1,122 @@
<script lang="ts">
import moment from 'moment';
import { onMount } from 'svelte';
import { env } from '$env/dynamic/public';
// load moment
let state = 'loading';
let stackData: any = {};
export let data: any;
onMount(() => {
let key = localStorage.getItem('gb_access_token');
let projectId = data.projectId;
let branchId = data.branchId;
if (key) {
fetch(env.PUBLIC_APP_HOST + 'api/patch_stack/' + projectId + '/' + branchId, {
method: 'GET',
headers: {
'X-AUTH-TOKEN': key || ''
}
})
.then(async (response) => await response.json())
.then((data) => {
console.log(data);
stackData = data;
state = 'loaded';
// moment all the .dtime elements
// wait a second
setTimeout(() => {
let dtime = document.querySelectorAll('.dtime');
dtime.forEach((element) => {
console.log(element.innerHTML);
element.innerHTML = moment(element.innerHTML).fromNow();
});
}, 100);
});
} else {
state = 'unauthorized';
}
});
</script>
{#if state === 'loading'}
<p>Loading...</p>
{:else if state === 'unauthorized'}
<p>Unauthorized</p>
{:else}
<div><a href="/projects/{data.projectId}">project</a></div>
<h1>Patch Stack</h1>
<div class="columns">
<div class="column">
Title: <strong>{stackData.title}</strong><br />
Branch: <code>{stackData.branch_id}</code><br />
Stack UUID: <code>{stackData.uuid}</code><br />
Updated: <span class="dtime">{stackData.created_at}</span><br />
</div>
<div class="column">
Stack Size: {stackData.stack_size}<br />
{#if stackData.version > 1}
Version: <a href="/projects/{data.projectId}/branches/{stackData.branch_id}/versions"
>{stackData.version}</a
><br />
{:else}
Version: {stackData.version}<br />
{/if}
Contributors: {stackData.contributors}<br />
</div>
</div>
<hr />
<h2>Patches</h2>
{#each stackData.patches as patch}
<div class="columns patch">
<div class="column">
<div>Title: <strong>{patch.title}</strong></div>
<div>Change Id: <code><a href="./stack/{patch.change_id}">{patch.change_id}</a></code></div>
<div>Commit: <code>{patch.commit_sha}</code></div>
<div>Version: {patch.version}</div>
<div><strong>Files:</strong></div>
{#each patch.statistics.files as file}
<div><code>{file}</code></div>
{/each}
</div>
<div class="column">
<div>Created: <span class="dtime">{patch.created_at}</span></div>
<div>Contributors: {patch.contributors}</div>
<div>
Additions: {patch.statistics.lines - patch.statistics.deletions}, Deletions: {patch
.statistics.deletions}, Files: {patch.statistics.file_count}
</div>
</div>
</div>
{/each}
{/if}
<style>
hr {
margin: 10px 0;
}
h2 {
font-size: 1.5rem;
}
.columns {
display: flex;
}
.column {
flex: 1;
}
.column div {
margin: 4px 0;
}
.patch {
background-color: #fff;
border: 1px solid #ccc;
padding: 15px 20px;
margin: 10px 0;
border-radius: 10px;
}
</style>

View File

@ -0,0 +1,6 @@
export const load: any = ({ params }) => {
return {
projectId: params.projectId,
branchId: params.branchId
};
};

View File

@ -0,0 +1,371 @@
<script lang="ts">
import hljs from 'highlight.js';
import { marked } from 'marked';
import { onMount } from 'svelte';
import { env } from '$env/dynamic/public';
let state = 'loading';
let patch: any = {};
let stack: any = {};
let key: any = '';
let uuid: any = '';
export let data: any;
onMount(() => {
key = localStorage.getItem('gb_access_token');
let projectId = data.projectId;
let branchId = data.branchId;
let changeId = data.changeId;
if (key) {
fetch(env.PUBLIC_APP_HOST + 'api/patch_stack/' + projectId + '/' + branchId, {
method: 'GET',
headers: {
'X-AUTH-TOKEN': key || ''
}
})
.then(async (response) => await response.json())
.then((data) => {
console.log(data);
stack = data;
uuid = data.uuid;
fetchPatch(data.uuid, changeId, key);
});
} else {
state = 'unauthorized';
}
});
function fetchPatch(uuid: string, changeId: string, key: string) {
fetch(env.PUBLIC_APP_HOST + 'api/patch_stack/' + uuid + '/patch/' + changeId, {
method: 'GET',
headers: {
'X-AUTH-TOKEN': key || ''
}
})
.then(async (response) => await response.json())
.then((data) => {
console.log(data);
patch = data;
state = 'loaded';
// wait a second
setTimeout(() => {
console.log('Highlighting code');
hljs.highlightAll();
// render markdowns
let markdowns = document.querySelectorAll('.markdown');
markdowns.forEach((markdown) => {
markdown.innerHTML = marked(markdown.innerHTML);
});
}, 10);
});
}
function createSectionPost(position: number) {
let opts = {
method: 'POST',
headers: {
'X-AUTH-TOKEN': key || '',
'Content-Type': 'application/json'
},
body: JSON.stringify({
type: 'text',
text: '# new section',
position: position - 1
})
};
if (key) {
fetch(
env.PUBLIC_APP_HOST + 'api/patch_stack/' + uuid + '/patch/' + data.changeId + '/section',
opts
)
.then(async (response) => await response.json())
.then((data) => {
console.log(data);
});
}
}
function deleteSectionPost(code: string) {
let opts = {
method: 'DELETE',
headers: {
'X-AUTH-TOKEN': key || '',
'Content-Type': 'application/json'
}
};
if (key) {
fetch(
env.PUBLIC_APP_HOST +
'api/patch_stack/' +
uuid +
'/patch/' +
data.changeId +
'/section/' +
code,
opts
)
.then(async (response) => await response.json())
.then((data) => {
console.log(data);
});
}
}
function deleteSection(code: string) {
console.log('Adding section at position', code);
deleteSectionPost(code);
updatePatch();
}
function addSection(position: number) {
console.log('Adding section at position', position);
createSectionPost(position);
updatePatch();
}
function orderSectionPatch(order: Array) {
let opts = {
method: 'PATCH',
headers: {
'X-AUTH-TOKEN': key || '',
'Content-Type': 'application/json'
},
body: JSON.stringify({
section_order: order
})
};
if (key) {
fetch(env.PUBLIC_APP_HOST + 'api/patch_stack/' + uuid + '/patch/' + data.changeId, opts)
.then(async (response) => await response.json())
.then((data) => {
console.log(data);
});
}
}
function moveSection(position: number, change: number) {
console.log('Moving section at position', position, 'by', change);
let ids = patch.sections.map((section: any) => section.identifier);
// reorder ids array to move item in position to swap with item change off
let temp = ids[position];
ids[position] = ids[position + change];
ids[position + change] = temp;
// convert ids array to comma separated string
orderSectionPatch(ids);
console.log(ids);
updatePatch();
}
function editSection(code: string) {
console.log('Editing section', code);
let editor = document.querySelector('.edit-' + code);
if (editor) {
editor.style.display = 'block';
let display = document.querySelector('.display-' + code);
if (display) {
display.style.display = 'none';
}
}
}
function saveSection(code: string) {
console.log('Saving section', code);
let editor = document.querySelector('.edit-' + code);
if (editor) {
let text = editor.querySelector('textarea').value;
let opts = {
method: 'PATCH',
headers: {
'X-AUTH-TOKEN': key || '',
'Content-Type': 'application/json'
},
body: JSON.stringify({
text: text
})
};
if (key) {
fetch(
env.PUBLIC_APP_HOST +
'api/patch_stack/' +
uuid +
'/patch/' +
data.changeId +
'/section/' +
code,
opts
)
.then(async (response) => await response.json())
.then((data) => {
console.log(data);
});
}
editor.style.display = 'none';
let display = document.querySelector('.display-' + code);
if (display) {
display.style.display = 'block';
display.innerHTML = text;
updatePatch();
}
}
}
function updatePatch() {
setTimeout(() => {
fetchPatch(uuid, data.changeId, key);
}, 500);
}
</script>
{#if state === 'loading'}
<p>Loading...</p>
{:else if state === 'unauthorized'}
<p>Unauthorized</p>
{:else}
<h2>Patch Stack: <a href="../stack">{stack.title}</a></h2>
{#each stack.patches as stackPatch}
<div>
<code
><a href="/projects/{data.projectId}/branches/{data.branchId}/stack/{stackPatch.change_id}"
>{stackPatch.change_id.substr(0, 8)}</a
></code
>:
{#if patch.change_id === stackPatch.change_id}
<strong>{stackPatch.title}</strong>
{:else}
{stackPatch.title}
{/if}
</div>
{/each}
<hr />
<h2>Patch</h2>
<div class="columns">
<div class="column">
<div>Title: <strong>{patch.title}</strong></div>
{#if patch.description}
<div>Desc: {patch.description}</div>
{/if}
<div>Change Id: <code>{patch.change_id}</code></div>
<div>Commit: <code>{patch.commit_sha}</code></div>
</div>
<div class="column">
<div>Patch Version: {patch.version}</div>
<div>Stack Position: {patch.position + 1}/{stack.stack_size}</div>
<div>Contributors: {patch.contributors}</div>
<div>
Additions: {patch.statistics.lines - patch.statistics.deletions}, Deletions: {patch
.statistics.deletions}, Files: {patch.statistics.file_count}
</div>
</div>
</div>
<div class="columns">
<div class="column outline">
<h3>Outline</h3>
<div class="sections">
{#each patch.sections as section}
{#if section.section_type === 'diff'}
<div><a href="#section-{section.id}">{section.new_path}</a></div>
{:else}
<div><a href="#section-{section.id}">{section.title}</a></div>
{/if}
{/each}
</div>
</div>
<div class="column">
<div class="patch">
{#each patch.sections as section}
<div id="section-{section.id}">
{#if section.section_type === 'diff'}
<div class="right">
<a class="action" on:click={() => addSection(section.position)}>add</a>
[<a class="action" on:click={() => moveSection(section.position, -1)}>up</a>
<a class="action" on:click={() => moveSection(section.position, 1)}>down</a>]
</div>
<div>
<strong>{section.new_path}</strong>
</div>
<div><pre><code>{section.diff_patch}</code></pre></div>
{:else}
<div class="right">
<a class="action" on:click={() => addSection(section.position)}>add</a>
[
<a class="action" on:click={() => editSection(section.code)}>edit</a>] [
<a class="action" on:click={() => deleteSection(section.code)}>del</a>] [
<a class="action" on:click={() => moveSection(section.position, -1)}>up</a>
<a class="action" on:click={() => moveSection(section.position, 1)}>down</a>
]
</div>
<div class="editor edit-{section.code}">
<textarea class="editing">{section.data.text}</textarea>
<button on:click={() => saveSection(section.code)}>Save</button>
</div>
<div class="markdown display-{section.code}">{section.data.text}</div>
{/if}
</div>
{/each}
<div class="right">
<a class="action" on:click={() => addSection(patch.sections.length)}>add</a>
</div>
</div>
</div>
</div>
{/if}
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/default.min.css"
/>
<style>
hr {
margin: 1rem 0;
}
code {
background-color: #f4f4f4;
padding: 0.2rem 0.4rem;
border-radius: 4px;
}
strong {
font-weight: bold;
}
.columns {
display: flex;
}
.column {
flex: 1;
padding: 1rem;
}
.outline {
max-width: 250px;
}
.right {
display: flex;
flex-direction: row;
justify-content: flex-end;
gap: 5px;
color: #888;
}
.action {
cursor: pointer;
color: #999;
}
.sections {
display: flex;
flex-direction: column;
gap: 5px;
}
.editing {
width: 100%;
height: 100px;
font-family: monospace;
font-size: large;
}
.editor {
display: none;
}
.patch {
background-color: #ffffff;
border-radius: 10px;
padding: 10px 20px;
}
</style>

View File

@ -0,0 +1,7 @@
export const load: any = ({ params }) => {
return {
projectId: params.projectId,
branchId: params.branchId,
changeId: params.changeId
};
};

View File

@ -0,0 +1,117 @@
<script lang="ts">
import moment from 'moment';
import { onMount } from 'svelte';
import { env } from '$env/dynamic/public';
// load moment
let state = 'loading';
let stackData: any = {};
export let data: any;
onMount(() => {
let key = localStorage.getItem('gb_access_token');
let projectId = data.projectId;
let branchId = data.branchId;
if (key) {
fetch(
env.PUBLIC_APP_HOST +
'api/patch_stack/' +
projectId +
'?branch_id=' +
branchId +
'&status=all',
{
method: 'GET',
headers: {
'X-AUTH-TOKEN': key || ''
}
}
)
.then(async (response) => await response.json())
.then((data) => {
console.log(data);
stackData = data;
state = 'loaded';
// moment all the .dtime elements
// wait a second
setTimeout(() => {
let dtime = document.querySelectorAll('.dtime');
dtime.forEach((element) => {
console.log(element.innerHTML);
element.innerHTML = moment(element.innerHTML).fromNow();
});
}, 100);
});
} else {
state = 'unauthorized';
}
});
</script>
{#if state === 'loading'}
<p>Loading...</p>
{:else if state === 'unauthorized'}
<p>Unauthorized</p>
{:else}
<div><a href="/projects/{data.projectId}">project</a></div>
<h1>Stack History</h1>
{#each stackData as stack}
<div class="columns">
<div class="column">
Title: <strong>{stack.title}</strong><br />
Branch: <code>{stack.branch_id}</code><br />
Stack UUID: <code>{stack.uuid}</code><br />
Updated: <span class="dtime">{stack.created_at}</span><br />
</div>
<div class="column">
Stack Size: {stack.stack_size}<br />
Version: {stack.version}<br />
Contributors: {stack.contributors}<br />
</div>
</div>
<div>
Patches:
<ul>
{#each stack.patches as patch}
<li>
<code style="background-color:#{patch.change_id.substr(0, 6)}"
>{patch.change_id.substr(0, 6)}</code
>:
<code style="background-color:#{patch.commit_sha.substr(0, 6)}" <code
>{patch.commit_sha.substr(0, 6)}</code
>: {patch.title} : v{patch.version}
</li>
{/each}
</ul>
</div>
<hr />
{/each}
{/if}
<style>
hr {
margin: 10px 0;
}
h2 {
font-size: 1.5rem;
}
.columns {
display: flex;
}
.column {
flex: 1;
}
.column div {
margin: 4px 0;
}
.patch {
background-color: #fff;
border: 1px solid #ccc;
padding: 15px 20px;
margin: 10px 0;
border-radius: 10px;
}
</style>

View File

@ -0,0 +1,6 @@
export const load: any = ({ params }) => {
return {
projectId: params.projectId,
branchId: params.branchId
};
};

View File

@ -4,28 +4,6 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
catalogs:
default:
vite:
specifier: 5.2.13
version: 5.2.13
svelte:
'@sveltejs/adapter-static':
specifier: 3.0.4
version: 3.0.4
'@sveltejs/kit':
specifier: 2.5.25
version: 2.5.25
'@sveltejs/vite-plugin-svelte':
specifier: 4.0.0-next.6
version: 4.0.0-next.6
svelte:
specifier: 5.0.0-next.243
version: 5.0.0-next.243
svelte-check:
specifier: 4.0.1
version: 4.0.1
importers:
.:
@ -295,6 +273,15 @@ importers:
'@sentry/sveltekit':
specifier: ^8.9.2
version: 8.9.2(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.25.0)(@sveltejs/kit@2.5.25(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.243)(vite@5.2.13(@types/node@22.3.0)))(svelte@5.0.0-next.243)(vite@5.2.13(@types/node@22.3.0)))(svelte@5.0.0-next.243)
highlight.js:
specifier: ^11.10.0
version: 11.10.0
marked:
specifier: ^10.0.0
version: 10.0.0
moment:
specifier: ^2.30.1
version: 2.30.1
devDependencies:
'@fontsource/fira-mono':
specifier: ^4.5.10
@ -4130,6 +4117,10 @@ packages:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
highlight.js@11.10.0:
resolution: {integrity: sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==}
engines: {node: '>=12.0.0'}
hosted-git-info@7.0.2:
resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==}
engines: {node: ^16.14.0 || >=18.0.0}
@ -4848,6 +4839,9 @@ packages:
module-details-from-path@1.0.3:
resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==}
moment@2.30.1:
resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
@ -11385,6 +11379,8 @@ snapshots:
he@1.2.0: {}
highlight.js@11.10.0: {}
hosted-git-info@7.0.2:
dependencies:
lru-cache: 10.2.2
@ -12101,6 +12097,8 @@ snapshots:
module-details-from-path@1.0.3: {}
moment@2.30.1: {}
mri@1.2.0: {}
mrmime@2.0.0: {}