Basic Spacedrop working with rspc! (#601)

This commit is contained in:
Oscar Beaumont 2023-03-09 22:23:17 +08:00 committed by GitHub
parent f98d7ad6e0
commit 7c5f760fbd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 243 additions and 158 deletions

View file

@ -21,7 +21,8 @@
"@tanstack/react-query": "^4.24.4",
"@tauri-apps/api": "1.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"vite-plugin-html": "^3.2.0"
},
"devDependencies": {
"@sd/config": "workspace:*",

View file

@ -1,7 +1,6 @@
// WARNING: BE CAREFUL SAVING THIS FILE WITH A FORMATTER ENABLED. The import order is important and goes against prettier's recommendations.
import React, { Suspense } from 'react';
import ReactDOM from 'react-dom/client';
// THIS MUST GO BEFORE importing the App
import '~/patches';
import App from './App';

View file

@ -1,7 +1,8 @@
import { relativeAliasResolver } from '@sd/config/vite';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
import svgr from 'vite-plugin-svgr';
import { createHtmlPlugin } from 'vite-plugin-html';
import svg from 'vite-plugin-svgr';
import tsconfigPaths from 'vite-tsconfig-paths';
import { name, version } from './package.json';
@ -13,10 +14,9 @@ export default defineConfig({
plugins: [
tsconfigPaths(),
react(),
svgr({
svgrOptions: {
icon: true
}
svg({ svgrOptions: { icon: true } }),
createHtmlPlugin({
minify: true
})
],
css: {

View file

@ -3,7 +3,6 @@ import React, { Suspense } from 'react';
import ReactDOM from 'react-dom/client';
import '@sd/ui/style';
import '~/patches';
// THIS MUST GO BEFORE importing the App
import '~/patches';
import App from './App';

View file

@ -1 +1 @@
module.exports = require('../../packages/ui/tailwind.config.js')('web');
module.exports = require('@sd/ui/tailwind')('web');

View file

@ -24,6 +24,11 @@ export default defineConfig({
brotliSize: true
})
],
css: {
modules: {
localsConvention: 'camelCaseOnly'
}
},
resolve: {
alias: [relativeAliasResolver]
},

View file

@ -11,6 +11,7 @@ use crate::{
job::JobManager,
library::LibraryManager,
node::{NodeConfig, NodeConfigManager},
p2p::P2PManager,
util::secure_temp_keystore::SecureTempKeystore,
};
@ -33,6 +34,7 @@ pub struct Ctx {
pub config: Arc<NodeConfigManager>,
pub jobs: Arc<JobManager>,
pub event_bus: broadcast::Sender<CoreEvent>,
pub p2p: Arc<P2PManager>,
pub secure_temp_keystore: Arc<SecureTempKeystore>,
}
@ -42,6 +44,7 @@ mod keys;
mod libraries;
mod locations;
mod nodes;
mod p2p;
mod tags;
pub mod utils;
pub mod volumes;
@ -97,6 +100,7 @@ pub(crate) fn mount() -> Arc<Router> {
.yolo_merge("locations.", locations::mount())
.yolo_merge("files.", files::mount())
.yolo_merge("jobs.", jobs::mount())
.yolo_merge("p2p.", p2p::mount())
// TODO: Scope the invalidate queries to a specific library (filtered server side)
.subscription("invalidateQuery", |t| {
t(|ctx, _: ()| {

View file

@ -1,78 +1,49 @@
// use std::collections::HashMap;
use rspc::Type;
use sd_p2p::PeerId;
use serde::Deserialize;
use std::path::PathBuf;
// use p2p::PeerId;
// use rspc::Type;
// use serde::Deserialize;
use crate::p2p::P2PEvent;
// use super::{LibraryArgs, RouterBuilder};
use super::RouterBuilder;
// #[derive(Type, Deserialize)]
// pub struct AcceptPairingRequestArgs {
// pub peer_id: PeerId,
// pub preshared_key: String,
// }
pub(crate) fn mount() -> RouterBuilder {
RouterBuilder::new()
.subscription("events", |t| {
t(|ctx, _: ()| {
let mut rx = ctx.p2p.subscribe();
async_stream::stream! {
// TODO: Don't block subscription start
for peer in ctx.p2p.manager.get_discovered_peers().await {
yield P2PEvent::DiscoveredPeer {
peer_id: peer.peer_id,
metadata: peer.metadata,
};
}
// pub(crate) fn mount() -> RouterBuilder {
// RouterBuilder::new()
// .query("getNodes", |ctx, arg: LibraryArgs<()>| async move {
// let (_, library) = arg.get_library(&ctx).await?;
// // TODO: Don't block subscription start
// for peer in ctx.p2p_manager.get_connected_peers().await.unwrap() {
// // TODO: Send to frontend
// }
// Ok(
// library.db.node().find_many(vec![]).exec().await?, // TODO: Make this work
// // .into_iter()
// // .filter_map(|v| {
// // if v.id == ctx.node_local_id {
// // None
// // } else {
// // Some(v.into())
// // }
// // })
// // .collect::<Vec<LibraryNode>>()
// )
// })
// .query("connectedPeers", |ctx, _: ()| async move {
// ctx.p2p
// .nm
// .connected_peers()
// .into_iter()
// .map(|(_, v)| (v.id, v.metadata))
// .collect::<HashMap<_, _>>()
// })
// .query("discoveredPeers", |ctx, _: ()| async move {
// ctx.p2p
// .nm
// .discovered_peers()
// .into_iter()
// // TODO: Make this better
// .map(|(_, v)| v)
// .collect::<Vec<_>>()
// })
// .mutation("pairNode", |ctx, arg: LibraryArgs<PeerId>| async move {
// let (peer_id, library) = arg.get_library(&ctx).await?;
// let preshared_key = ctx.p2p.pair(&library, peer_id).await.unwrap();
while let Ok(event) = rx.recv().await {
yield event;
}
}
})
})
.mutation("spacedrop", |t| {
#[derive(Type, Deserialize)]
pub struct SpacedropArgs {
peer_id: PeerId,
file_path: String,
}
// // TODO: These aren't library queries so they can't be invalidated with the current system. We can fix this with the normalised cache!
// // invalidate_query!(ctx, "p2p.discoveredPeers": (), ());
// // invalidate_query!(ctx, "p2p.connectedPeers": (), ());
// Ok(preshared_key)
// })
// .mutation(
// "unpairNode",
// |_, _: LibraryArgs<PeerId>| async move { todo!() },
// )
// .mutation(
// "acceptPairingRequest",
// |ctx, arg: AcceptPairingRequestArgs| async move {
// ctx.p2p
// .pairing_requests
// .lock()
// .unwrap()
// .remove(&arg.peer_id)
// .unwrap()
// .send(Ok(arg.preshared_key))
// .unwrap(); // TODO: Remove unwrap
// },
// )
// }
t(|ctx, args: SpacedropArgs| async move {
ctx.p2p
.big_bad_spacedrop(args.peer_id, PathBuf::from(args.file_path))
.await;
})
})
}

View file

@ -195,6 +195,7 @@ impl Node {
library_manager: Arc::clone(&self.library_manager),
config: Arc::clone(&self.config),
jobs: Arc::clone(&self.jobs),
p2p: Arc::clone(&self.p2p),
event_bus: self.event_bus.0.clone(),
secure_temp_keystore: Arc::clone(&self.secure_temp_keystore),
}

View file

@ -1,10 +1,16 @@
use std::{path::PathBuf, sync::Arc, time::Instant};
use sd_p2p::{Event, Manager, PeerId};
use rspc::Type;
use sd_p2p::{
spaceblock::{BlockSize, TransferRequest},
Event, Manager, PeerId,
};
use sd_sync::CRDTOperation;
use serde::Serialize;
use tokio::{
fs::File,
io::{AsyncReadExt, AsyncWriteExt, BufReader},
sync::broadcast,
};
use tracing::{debug, error, info};
use uuid::Uuid;
@ -16,8 +22,20 @@ use crate::{
use super::{Header, PeerMetadata};
/// TODO: P2P event for the frontend
#[derive(Debug, Clone, Type, Serialize)]
#[serde(tag = "type")]
pub enum P2PEvent {
DiscoveredPeer {
peer_id: PeerId,
metadata: PeerMetadata,
},
// TODO: Expire peer + connection/disconnect
}
pub struct P2PManager {
manager: Arc<Manager<PeerMetadata>>,
events: broadcast::Sender<P2PEvent>,
pub manager: Arc<Manager<PeerMetadata>>,
}
impl P2PManager {
@ -51,6 +69,8 @@ impl P2PManager {
manager.listen_addrs().await
);
let (events_tx, _) = broadcast::channel(100);
let events = events_tx.clone();
tokio::spawn(async move {
while let Some(event) = stream.next().await {
match event {
@ -59,7 +79,17 @@ impl P2PManager {
"Discovered peer by id '{}' with address '{:?}' and metadata: {:?}",
event.peer_id, event.addresses, event.metadata
);
event.dial().await; // We connect to everyone we find on the network. Your app will probs wanna restrict this!
events_tx
.send(P2PEvent::DiscoveredPeer {
peer_id: event.peer_id.clone(),
metadata: event.metadata.clone(),
})
.map_err(|_| error!("Failed to send event to p2p event stream!"))
.ok();
// TODO: Don't just connect to everyone when we find them. We should only do it if we know them.
event.dial().await;
}
Event::PeerMessage(mut event) => {
tokio::spawn(async move {
@ -69,26 +99,21 @@ impl P2PManager {
Header::Ping => {
debug!("Received ping from peer '{}'", event.peer_id);
}
Header::Spacedrop => {
let file_length = event.stream.read_u8().await.unwrap();
Header::Spacedrop(req) => {
info!("Received Spacedrop from peer '{}' for file '{}' with file length '{}'", event.peer_id, req.name, req.size);
// TODO: Ask the user if they wanna reject/accept it
info!("Received Spacedrop from peer '{}' with file length '{file_length}'", event.peer_id);
// TODO: Deal with binary data. Deal with blocking based on `req.block_size`, etc
let mut s = String::new();
event.stream.read_to_string(&mut s).await.unwrap();
// let mut buf = Vec::with_capacity(file_length as usize); // TODO: DOS attack
// loop {
// let n = event.stream.read(&mut buf).await.unwrap();;
// }
// // TODO: Store this to a file on disk.
println!(
"Recieved file content '{}' through Spacedrop!",
s // String::from_utf8(buf).unwrap()
"Recieved file '{}' with content '{}' through Spacedrop!",
req.name, s
);
// TODO: Save to the filesystem
}
Header::Sync(library_id) => {
let buf_len = event.stream.read_u8().await.unwrap();
@ -119,7 +144,7 @@ impl P2PManager {
// https://docs.rs/ctrlc/latest/ctrlc/
// https://docs.rs/system_shutdown/latest/system_shutdown/
let this = Arc::new(Self { manager });
let this = Arc::new(Self { events, manager });
// TODO: Probs remove this
tokio::spawn({
@ -156,6 +181,10 @@ impl P2PManager {
this
}
pub fn subscribe(&self) -> broadcast::Receiver<P2PEvent> {
self.events.subscribe()
}
#[allow(unused)] // TODO: Remove `allow(unused)` once integrated
pub async fn broadcast_sync_events(&self, library_id: Uuid, event: Vec<CRDTOperation>) {
let mut head_buf = Header::Sync(library_id).to_bytes();
@ -173,31 +202,31 @@ impl P2PManager {
pub async fn big_bad_spacedrop(&self, peer_id: PeerId, path: PathBuf) {
let mut stream = self.manager.stream(peer_id).await.unwrap(); // TODO: handle providing incorrect peer id
let file = File::open(path).await.unwrap();
let file_length = file.metadata().await.unwrap().len();
let file = File::open(&path).await.unwrap();
let metadata = file.metadata().await.unwrap();
let mut reader = BufReader::new(file);
stream
.write_all(&Header::Spacedrop.to_bytes()) // TODO: Proper Spaceblock Header
.await
.unwrap();
stream
.write_u8(file_length as u8) // TODO: This is obviously gonna be an int overflow. Fix that. Use `u64` in proper Spaceblock Header
.write_all(
&Header::Spacedrop(TransferRequest {
name: path.file_name().unwrap().to_str().unwrap().to_string(), // TODO: Encode this as bytes instead
size: metadata.len(),
block_size: BlockSize::from_size(metadata.len()),
})
.to_bytes(),
)
.await
.unwrap();
debug!("Starting Spacedrop to peer '{peer_id}'");
let i = Instant::now();
// TODO: Replace this with the Spaceblock `Block` system
let mut buffer = Vec::new();
reader.read_to_end(&mut buffer).await.unwrap();
println!("READ {:?}", buffer);
stream.write_all(&buffer).await.unwrap();
// io::copy(&mut reader, &mut stream).await.unwrap(); // TODO: Use Spaceblock protocol!
debug!(
"Finished Spacedrop to peer '{peer_id}' after '{:?}",
i.elapsed()

View file

@ -1,13 +1,13 @@
use tokio::io::AsyncReadExt;
use uuid::Uuid;
use sd_p2p::spacetime::SpaceTimeStream;
use sd_p2p::{spaceblock::TransferRequest, spacetime::SpaceTimeStream};
/// TODO
#[derive(Debug, PartialEq, Eq)]
pub enum Header {
Ping,
Spacedrop,
Spacedrop(TransferRequest),
Sync(Uuid),
}
@ -16,7 +16,7 @@ impl Header {
let discriminator = stream.read_u8().await.map_err(|_| ())?; // TODO: Error handling
match discriminator {
0 => Ok(Self::Spacedrop),
0 => Ok(Self::Spacedrop(TransferRequest::from_stream(stream).await?)),
1 => Ok(Self::Ping),
2 => {
let mut uuid = [0u8; 16];
@ -29,7 +29,11 @@ impl Header {
pub fn to_bytes(&self) -> Vec<u8> {
match self {
Self::Spacedrop => vec![0],
Self::Spacedrop(transfer_request) => {
let mut bytes = vec![0];
bytes.extend_from_slice(&transfer_request.to_bytes());
bytes
}
Self::Ping => vec![1],
Self::Sync(uuid) => {
let mut bytes = vec![2];

View file

@ -5,7 +5,7 @@ mod manager;
mod manager_stream;
mod mdns;
mod peer;
pub(crate) mod spaceblock;
pub mod spaceblock;
pub mod spacetime;
mod utils;

View file

@ -8,7 +8,12 @@ use std::{
path::{Path, PathBuf},
};
use tokio::io::AsyncReadExt;
use crate::spacetime::SpaceTimeStream;
/// TODO
#[derive(Debug, PartialEq, Eq)]
pub struct BlockSize(i64);
impl BlockSize {
@ -21,19 +26,47 @@ impl BlockSize {
}
/// TODO
pub struct TransferRequest<'a> {
name: &'a str,
size: u64,
#[derive(Debug, PartialEq, Eq)]
pub struct TransferRequest {
pub name: String,
pub size: u64,
// TODO: Include file permissions
block_size: u64,
pub block_size: BlockSize,
}
impl TransferRequest {
pub async fn from_stream(stream: &mut SpaceTimeStream) -> Result<Self, ()> {
let name_len = stream.read_u8().await.map_err(|_| ())?; // TODO: Error handling
let mut name = vec![0u8; name_len as usize];
stream.read_exact(&mut name).await.map_err(|_| ())?; // TODO: Error handling
let name = String::from_utf8(name).map_err(|_| ())?; // TODO: Error handling
let size = stream.read_u8().await.map_err(|_| ())? as u64; // TODO: Error handling
let block_size = BlockSize::from_size(size); // TODO: Get from stream: stream.read_u8().await.map_err(|_| ())?; // TODO: Error handling
Ok(Self {
name,
size,
block_size,
})
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut buf = Vec::new();
buf.push(self.name.len() as u8); // TODO: This being a `u8` isn't going to scale to a name bigger than 255 bytes lmao
buf.extend(self.name.as_bytes());
buf.push(self.size as u8); // TODO: This being a `u8` isn't going to scale to files bigger than 255 bytes lmao
// buf.push(&self.block_size.to_be_bytes()); // TODO: Do this as well
buf
}
}
/// TODO
pub struct Block<'a> {
// TODO: File content, checksum, source location so it can be resent!
offset: i64,
size: i64,
data: &'a [u8],
pub offset: i64,
pub size: i64,
pub data: &'a [u8],
// TODO: Checksum?
}

View file

@ -3,8 +3,9 @@ import Mega from '@sd/assets/images/Mega.png';
import iCloud from '@sd/assets/images/iCloud.png';
import clsx from 'clsx';
import { DeviceMobile, HardDrives, Icon, Laptop, Star, User } from 'phosphor-react';
import { useRef } from 'react';
import { useRef, useState } from 'react';
import { tw } from '@sd/ui';
import { PeerMetadata, useBridgeMutation, useBridgeSubscription } from '~/../packages/client/src';
import { SubtleButton, SubtleButtonContainer } from '~/components/SubtleButton';
import { OperatingSystem } from '~/util/Platform';
import { SearchBar } from './Explorer/TopBar';
@ -83,11 +84,66 @@ function DropItem(props: DropItemProps) {
);
}
// TODO: This will be removed and properly hooked up to the UI in the future
function TemporarySpacedropDemo() {
const [[discoveredPeers], setDiscoveredPeer] = useState([new Map<string, PeerMetadata>()]);
const doSpacedrop = useBridgeMutation('p2p.spacedrop');
useBridgeSubscription(['p2p.events'], {
onData(data) {
if (data.type === 'DiscoveredPeer') {
setDiscoveredPeer([discoveredPeers.set(data.peer_id, data.metadata)]);
}
}
});
console.log(discoveredPeers);
// TODO: Input select
return (
<form
onSubmit={(e) => {
e.preventDefault();
doSpacedrop.mutate({
peer_id: e.currentTarget.targetPeer.value,
file_path: e.currentTarget.filePath.value
});
}}
>
<h1 className="mt-4 text-4xl">Spacedrop Demo</h1>
<p>
Note: Right now the file must be less than 255 bytes long and only contain UTF-8 chars.
Create a txt file in Vscode to test (note macOS TextEdit cause that is rtf by default)
</p>
<select id="targetPeer" name="targetPeer" className="my-2 w-full text-black">
{[...discoveredPeers.entries()].map(([peerId, metdata]) => (
<option key={peerId} value={peerId}>
{metdata.name}
</option>
))}
</select>
<input
name="filePath"
placeholder="file path"
value="/Users/oscar/Desktop/sd/demo.txt"
onChange={() => {}}
className="my-2 w-full p-2 text-black"
/>
<input
type="submit"
value="Full send it!"
className="mt-4 h-10 w-32 rounded-full bg-red-500"
/>
</form>
);
}
export default () => {
const searchRef = useRef<HTMLInputElement>(null);
return (
<>
<TemporarySpacedropDemo />
<PageLayout.DragChildren>
<div className="flex h-8 w-full flex-row items-center justify-center pt-3">
<SearchBar className="ml-[13px]" ref={searchRef} />

View file

@ -70,6 +70,7 @@ export type Procedures = {
{ key: "locations.relink", input: LibraryArgs<string>, result: null } |
{ key: "locations.update", input: LibraryArgs<LocationUpdateArgs>, result: null } |
{ key: "nodes.tokenizeSensitiveKey", input: TokenizeKeyArgs, result: TokenizeResponse } |
{ key: "p2p.spacedrop", input: SpacedropArgs, result: null } |
{ key: "tags.assign", input: LibraryArgs<TagAssignArgs>, result: null } |
{ key: "tags.create", input: LibraryArgs<TagCreateArgs>, result: Tag } |
{ key: "tags.delete", input: LibraryArgs<number>, result: null } |
@ -77,7 +78,8 @@ export type Procedures = {
subscriptions:
{ key: "invalidateQuery", input: never, result: InvalidateOperationEvent } |
{ key: "jobs.newThumbnail", input: LibraryArgs<null>, result: string } |
{ key: "locations.online", input: never, result: number[][] }
{ key: "locations.online", input: never, result: number[][] } |
{ key: "p2p.events", input: never, result: P2PEvent }
};
/**
@ -220,6 +222,17 @@ export type Object = { id: number, pub_id: number[], name: string | null, extens
export type ObjectValidatorArgs = { id: number, path: string }
/**
* Represents the operating system which the remote peer is running.
* This is not used internally and predominantly is designed to be used for display purposes by the embedding application.
*/
export type OperatingSystem = "Windows" | "Linux" | "MacOS" | "Ios" | "Android" | { Other: string }
/**
* TODO: P2P event for the frontend
*/
export type P2PEvent = { type: "DiscoveredPeer", peer_id: string, metadata: PeerMetadata }
/**
* These parameters define the password-hashing level.
*
@ -227,6 +240,8 @@ export type ObjectValidatorArgs = { id: number, path: string }
*/
export type Params = "Standard" | "Hardened" | "Paranoid"
export type PeerMetadata = { name: string, operating_system: OperatingSystem | null, version: string | null, email: string | null, img_url: string | null }
export type RestoreBackupArgs = { password: string, secret_key: string, path: string }
export type RuleKind = "AcceptFilesByGlob" | "RejectFilesByGlob" | "AcceptIfChildrenDirectoriesArePresent" | "RejectIfChildrenDirectoriesArePresent"
@ -242,6 +257,8 @@ export type SetFavoriteArgs = { id: number, favorite: boolean }
export type SetNoteArgs = { id: number, note: string | null }
export type SpacedropArgs = { peer_id: string, file_path: string }
export type Statistics = { id: number, date_captured: string, total_object_count: number, library_db_size: string, total_bytes_used: string, total_bytes_capacity: string, total_unique_bytes: string, total_bytes_free: string, preview_media_bytes: string }
/**

View file

@ -54,6 +54,7 @@ importers:
sass: ^1.55.0
typescript: ^4.8.4
vite: ^4.0.4
vite-plugin-html: ^3.2.0
vite-plugin-svgr: ^2.2.1
vite-tsconfig-paths: ^4.0.3
dependencies:
@ -66,6 +67,7 @@ importers:
'@tauri-apps/api': 1.2.0
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
vite-plugin-html: 3.2.0_vite@4.0.4
devDependencies:
'@sd/config': link:../../packages/config
'@tauri-apps/cli': 1.2.3
@ -4249,7 +4251,6 @@ packages:
dependencies:
'@jridgewell/gen-mapping': 0.3.2
'@jridgewell/trace-mapping': 0.3.17
dev: true
/@jridgewell/sourcemap-codec/1.4.14:
resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==}
@ -5711,7 +5712,6 @@ packages:
dependencies:
estree-walker: 2.0.2
picomatch: 2.3.1
dev: true
/@rollup/pluginutils/5.0.2:
resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==}
@ -8806,7 +8806,6 @@ packages:
resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==}
engines: {node: '>=0.4.0'}
hasBin: true
dev: true
/address/1.2.2:
resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==}
@ -9737,7 +9736,6 @@ packages:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
dependencies:
balanced-match: 1.0.2
dev: true
/braces/2.3.2:
resolution: {integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==}
@ -10047,7 +10045,6 @@ packages:
dependencies:
pascal-case: 3.1.2
tslib: 2.4.1
dev: true
/camelcase-css/2.0.1:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
@ -10249,7 +10246,6 @@ packages:
engines: {node: '>= 10.0'}
dependencies:
source-map: 0.6.1
dev: true
/clean-stack/2.2.0:
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
@ -10407,7 +10403,6 @@ packages:
/colorette/2.0.19:
resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==}
dev: true
/combined-stream/1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
@ -10432,7 +10427,6 @@ packages:
/commander/2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
dev: true
/commander/4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
@ -10455,7 +10449,6 @@ packages:
/commander/8.3.0:
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
engines: {node: '>= 12'}
dev: true
/commander/9.5.0:
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
@ -10538,7 +10531,6 @@ packages:
/connect-history-api-fallback/1.6.0:
resolution: {integrity: sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==}
engines: {node: '>=0.8'}
dev: true
/connect/3.7.0:
resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==}
@ -10553,7 +10545,6 @@ packages:
/consola/2.15.3:
resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==}
dev: true
/console-browserify/1.2.0:
resolution: {integrity: sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==}
@ -10992,7 +10983,6 @@ packages:
domhandler: 4.3.1
domutils: 2.8.0
nth-check: 2.1.1
dev: true
/css-select/5.1.0:
resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
@ -11347,7 +11337,6 @@ packages:
domelementtype: 2.3.0
domhandler: 4.3.1
entities: 2.2.0
dev: true
/dom-serializer/2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
@ -11373,7 +11362,6 @@ packages:
engines: {node: '>= 4'}
dependencies:
domelementtype: 2.3.0
dev: true
/domhandler/5.0.3:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
@ -11387,7 +11375,6 @@ packages:
dom-serializer: 1.4.1
domelementtype: 2.3.0
domhandler: 4.3.1
dev: true
/domutils/3.0.1:
resolution: {integrity: sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==}
@ -11401,7 +11388,6 @@ packages:
dependencies:
no-case: 3.0.4
tslib: 2.4.1
dev: true
/dot-prop/5.3.0:
resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==}
@ -11417,12 +11403,10 @@ packages:
/dotenv-expand/8.0.3:
resolution: {integrity: sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg==}
engines: {node: '>=12'}
dev: true
/dotenv/16.0.3:
resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==}
engines: {node: '>=12'}
dev: true
/dotenv/8.6.0:
resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==}
@ -11451,7 +11435,6 @@ packages:
hasBin: true
dependencies:
jake: 10.8.5
dev: true
/electron-to-chromium/1.4.284:
resolution: {integrity: sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==}
@ -11516,7 +11499,6 @@ packages:
/entities/2.2.0:
resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
dev: true
/entities/4.4.0:
resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==}
@ -11970,7 +11952,6 @@ packages:
/estree-walker/2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
dev: true
/esutils/1.0.0:
resolution: {integrity: sha512-x/iYH53X3quDwfHRz4y8rn4XcEwwCJeWsul9pF1zldMbGtgOtMNBEOuYWwB1EQlK2LRa1fev3YAgym/RElp5Cg==}
@ -12430,7 +12411,6 @@ packages:
resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
dependencies:
minimatch: 5.1.6
dev: true
/fill-range/4.0.0:
resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==}
@ -12800,7 +12780,6 @@ packages:
graceful-fs: 4.2.10
jsonfile: 6.1.0
universalify: 2.0.0
dev: true
/fs-extra/11.1.0:
resolution: {integrity: sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==}
@ -13391,7 +13370,6 @@ packages:
/he/1.2.0:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
dev: true
/hermes-estree/0.8.0:
resolution: {integrity: sha512-W6JDAOLZ5pMPMjEiQGLCXSSV7pIBEgRR5zGkxgmzGSXHOxqV5dC/M1Zevqpbm9TZDE5tu358qZf8Vkzmsc+u7Q==}
@ -13476,7 +13454,6 @@ packages:
param-case: 3.0.4
relateurl: 0.2.7
terser: 5.16.1
dev: true
/html-tags/3.2.0:
resolution: {integrity: sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==}
@ -14292,7 +14269,6 @@ packages:
chalk: 4.1.2
filelist: 1.0.4
minimatch: 3.1.2
dev: true
/javascript-natural-sort/0.7.1:
resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==}
@ -14859,7 +14835,6 @@ packages:
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
dependencies:
tslib: 2.4.1
dev: true
/lowercase-keys/1.0.1:
resolution: {integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==}
@ -15546,7 +15521,6 @@ packages:
engines: {node: '>=10'}
dependencies:
brace-expansion: 2.0.1
dev: true
/minimist/1.2.7:
resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==}
@ -15758,7 +15732,6 @@ packages:
dependencies:
lower-case: 2.0.2
tslib: 2.4.1
dev: true
/nocache/3.0.4:
resolution: {integrity: sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw==}
@ -15803,7 +15776,6 @@ packages:
dependencies:
css-select: 4.3.0
he: 1.2.0
dev: true
/node-int64/0.4.0:
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
@ -16271,7 +16243,6 @@ packages:
dependencies:
dot-case: 3.0.4
tslib: 2.4.1
dev: true
/parent-module/1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
@ -16366,7 +16337,6 @@ packages:
dependencies:
no-case: 3.0.4
tslib: 2.4.1
dev: true
/pascalcase/0.1.1:
resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==}
@ -16447,7 +16417,6 @@ packages:
/pathe/0.2.0:
resolution: {integrity: sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==}
dev: true
/pbkdf2/3.1.2:
resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==}
@ -18062,7 +18031,6 @@ packages:
/relateurl/0.2.7:
resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==}
engines: {node: '>= 0.10'}
dev: true
/remark-external-links/8.0.0:
resolution: {integrity: sha512-5vPSX0kHoSsqtdftSHhIYofVINC8qmp0nctkeU9YoJwV3YfiBRiI6cbFRJ0oI/1F9xS+bopXG0m2KS8VFscuKA==}
@ -19512,7 +19480,6 @@ packages:
acorn: 8.8.2
commander: 2.20.3
source-map-support: 0.5.21
dev: true
/test-exclude/6.0.0:
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
@ -20808,8 +20775,7 @@ packages:
html-minifier-terser: 6.1.0
node-html-parser: 5.4.2
pathe: 0.2.0
vite: 4.0.4
dev: true
vite: 4.0.4_sass@1.57.1
/vite-plugin-markdown/2.1.0_vite@4.0.4:
resolution: {integrity: sha512-eWLlrWzYZXEX3/HaXZo/KLjRpO72IUhbgaoFrbwB07ueXi6QfwqrgdZQfUcXTSofJCkN7GhErMC1K1RTAE0gGQ==}