Compare commits
4 Commits
9124cc90aa
...
21d65af470
| Author | SHA1 | Date | |
|---|---|---|---|
| 21d65af470 | |||
| 4699ffe6af | |||
| b2a7787e45 | |||
| 8cb1f4142b |
@@ -70,12 +70,13 @@ async fn main() -> Result<()> {
|
||||
});
|
||||
|
||||
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||
const BODY_LIMIT: usize = 1000 * 1000 * 1000 * 5;
|
||||
let pool = PgPool::connect(&database_url).await.unwrap();
|
||||
sqlx::migrate!().run(&pool).await?;
|
||||
let file_repository = FileRepository::new(pool.clone())?;
|
||||
let routes_file = routes_file(file_repository.clone())
|
||||
.route_layer(middleware::from_fn(mw_require_auth))
|
||||
.layer(DefaultBodyLimit::disable());
|
||||
.layer(DefaultBodyLimit::max(BODY_LIMIT));
|
||||
|
||||
let user_repository = UserRepository::new(pool)?;
|
||||
let routes_auth =
|
||||
|
||||
@@ -120,6 +120,25 @@ impl FileRepository {
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
pub async fn stream_part(
|
||||
&self,
|
||||
file_id: i64,
|
||||
user_id: i64,
|
||||
from: u64,
|
||||
to: u64,
|
||||
) -> Result<impl Stream<Item = std::io::Result<Bytes>> + use<>, LoftError> {
|
||||
info!(
|
||||
"Fetching metadata of file \"{}\" from file_records",
|
||||
file_id
|
||||
);
|
||||
let record = self.get_file(file_id, user_id).await?;
|
||||
info!("Streaming chunk of file \"{}\"", file_id);
|
||||
let reader = self.op.reader(&record.storage_key).await.unwrap();
|
||||
let stream = reader.into_bytes_stream(from..to).await.unwrap();
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
pub async fn get_file(&self, file_id: i64, user_id: i64) -> Result<FileRecord, LoftError> {
|
||||
info!(
|
||||
"Fetching metadata of file \"{}\" from file_records",
|
||||
@@ -412,12 +431,14 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let _downloaded = file_repository
|
||||
let chunks: Vec<_> = file_repository
|
||||
.download_file(file_record.id, user.id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//TODO: add assert
|
||||
.unwrap()
|
||||
.collect()
|
||||
.await;
|
||||
let downloaded: Vec<u8> = chunks.into_iter().flat_map(|c| c.unwrap()).collect();
|
||||
assert_eq!(downloaded, vec![0u8; 10]);
|
||||
|
||||
truncate_file_records(&file_repository.pool).await;
|
||||
truncate_users(&user_repository.pool).await;
|
||||
@@ -435,6 +456,45 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn test_stream_part() {
|
||||
let user_repository = user_repository().await.unwrap();
|
||||
let user = user_repository
|
||||
.create_user("username", "password_hash")
|
||||
.await
|
||||
.unwrap();
|
||||
let file_repository = file_repository().await.unwrap();
|
||||
truncate_file_records(&file_repository.pool).await;
|
||||
|
||||
let file_name = "a.png";
|
||||
let file_storage_key = format!("{}-{}", file_name, uuid::Uuid::new_v4());
|
||||
let file_size = file_repository
|
||||
.upload_file(
|
||||
stream::iter(vec![Ok(Bytes::from((0u8..10).collect::<Vec<u8>>()))]),
|
||||
&file_storage_key,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let file_record = file_repository
|
||||
.create_file_record(user.id, "a.jpg", file_size, &file_storage_key)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let chunks: Vec<_> = file_repository
|
||||
.stream_part(file_record.id, user.id, 2, 6)
|
||||
.await
|
||||
.unwrap()
|
||||
.collect()
|
||||
.await;
|
||||
let partial: Vec<u8> = chunks.into_iter().flat_map(|c| c.unwrap()).collect();
|
||||
assert_eq!(partial, vec![2u8, 3, 4, 5]);
|
||||
|
||||
truncate_file_records(&file_repository.pool).await;
|
||||
truncate_users(&user_repository.pool).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn test_delete() {
|
||||
|
||||
@@ -2,6 +2,7 @@ use axum::{
|
||||
Json, Router,
|
||||
body::Body,
|
||||
extract::{Multipart, Path, State},
|
||||
http::{HeaderMap, Response, StatusCode, header},
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
};
|
||||
@@ -19,6 +20,7 @@ pub fn routes_file(file_repository: FileRepository) -> Router {
|
||||
.route("/files", get(list_files).post(upload_file))
|
||||
.route("/files/{id}", get(get_file).delete(delete_file))
|
||||
.route("/files/{id}/download", get(download_file))
|
||||
.route("/files/{id}/stream_part", get(stream_part))
|
||||
.with_state(file_repository)
|
||||
}
|
||||
|
||||
@@ -54,7 +56,6 @@ async fn upload_file(
|
||||
Err(LoftError::UndefinedErrorType)
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
async fn get_file(
|
||||
State(file_repository): State<FileRepository>,
|
||||
ctx: Ctx,
|
||||
@@ -66,7 +67,6 @@ async fn get_file(
|
||||
Ok(Json(record))
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
async fn download_file(
|
||||
State(file_repository): State<FileRepository>,
|
||||
ctx: Ctx,
|
||||
@@ -78,6 +78,49 @@ async fn download_file(
|
||||
Ok(Body::from_stream(stream))
|
||||
}
|
||||
|
||||
async fn stream_part(
|
||||
State(file_repository): State<FileRepository>,
|
||||
ctx: Ctx,
|
||||
headers: HeaderMap,
|
||||
Path(file_id): Path<u64>,
|
||||
) -> Result<impl IntoResponse, LoftError> {
|
||||
info!("stream_part");
|
||||
let file_record = file_repository
|
||||
.get_file(file_id as i64, ctx.user_id())
|
||||
.await?;
|
||||
let file_size = file_record.size as u64;
|
||||
let (start, end): (u64, u64) = parse_range(&headers, file_size);
|
||||
|
||||
let stream = file_repository
|
||||
.stream_part(file_id as i64, ctx.user_id(), start, end + 1)
|
||||
.await?;
|
||||
|
||||
let respones = Response::builder()
|
||||
.status(StatusCode::PARTIAL_CONTENT)
|
||||
.header(header::CONTENT_TYPE, file_record.file_type)
|
||||
.header(header::CONTENT_LENGTH, end - start + 1)
|
||||
.header(
|
||||
header::CONTENT_RANGE,
|
||||
format!("bytes {start}-{end}/{file_size}"),
|
||||
)
|
||||
.header(header::ACCEPT_RANGES, "bytes")
|
||||
.body(Body::from_stream(stream))
|
||||
.unwrap();
|
||||
|
||||
Ok(respones)
|
||||
}
|
||||
|
||||
fn parse_range(headers: &HeaderMap, file_size: u64) -> (u64, u64) {
|
||||
let range = headers.get(header::RANGE);
|
||||
let str = range.unwrap().to_str().unwrap();
|
||||
let strip = str.strip_prefix("bytes=").unwrap();
|
||||
let tuple = strip.split_once("-").unwrap();
|
||||
let start = tuple.0.parse::<u64>().unwrap();
|
||||
let end = tuple.1.parse::<u64>().unwrap_or(file_size - 1);
|
||||
|
||||
(start, end)
|
||||
}
|
||||
|
||||
async fn delete_file(
|
||||
State(file_repository): State<FileRepository>,
|
||||
ctx: Ctx,
|
||||
@@ -103,7 +146,7 @@ async fn list_files(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use axum::{Router, middleware};
|
||||
use axum::{Router, http::header, middleware};
|
||||
use axum_test::{
|
||||
TestServer,
|
||||
multipart::{MultipartForm, Part},
|
||||
@@ -291,6 +334,71 @@ mod tests {
|
||||
truncate(&file_repository.pool).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn test_stream_part_range() {
|
||||
let user_repository = user_repository().await;
|
||||
let token = create_test_session(&user_repository).await;
|
||||
let file_repository = file_repository().await;
|
||||
truncate(&file_repository.pool).await;
|
||||
|
||||
let server = test_server().await;
|
||||
let post_res = server
|
||||
.post("/api/files")
|
||||
.add_header(axum::http::header::COOKIE, format!("auth-token={token}"))
|
||||
.multipart(MultipartForm::new().add_part(
|
||||
"file",
|
||||
Part::bytes(b"0123456789".to_vec()).file_name("a.txt"),
|
||||
))
|
||||
.await;
|
||||
let id = post_res.json::<serde_json::Value>()["id"].as_i64().unwrap();
|
||||
|
||||
let res = server
|
||||
.get(&format!("/api/files/{id}/stream_part"))
|
||||
.add_header(axum::http::header::COOKIE, format!("auth-token={token}"))
|
||||
.add_header(axum::http::header::RANGE, "bytes=2-5")
|
||||
.await;
|
||||
|
||||
res.assert_status(axum::http::StatusCode::PARTIAL_CONTENT);
|
||||
assert_eq!(res.as_bytes(), b"2345".as_ref());
|
||||
assert_eq!(res.header(header::CONTENT_RANGE), "bytes 2-5/10");
|
||||
assert_eq!(res.header(header::CONTENT_LENGTH), "4");
|
||||
assert_eq!(res.header(header::ACCEPT_RANGES), "bytes");
|
||||
truncate(&file_repository.pool).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn test_stream_part_open_ended() {
|
||||
let user_repository = user_repository().await;
|
||||
let token = create_test_session(&user_repository).await;
|
||||
let file_repository = file_repository().await;
|
||||
truncate(&file_repository.pool).await;
|
||||
|
||||
let server = test_server().await;
|
||||
let post_res = server
|
||||
.post("/api/files")
|
||||
.add_header(axum::http::header::COOKIE, format!("auth-token={token}"))
|
||||
.multipart(MultipartForm::new().add_part(
|
||||
"file",
|
||||
Part::bytes(b"0123456789".to_vec()).file_name("a.txt"),
|
||||
))
|
||||
.await;
|
||||
let id = post_res.json::<serde_json::Value>()["id"].as_i64().unwrap();
|
||||
|
||||
let res = server
|
||||
.get(&format!("/api/files/{id}/stream_part"))
|
||||
.add_header(axum::http::header::COOKIE, format!("auth-token={token}"))
|
||||
.add_header(axum::http::header::RANGE, "bytes=0-")
|
||||
.await;
|
||||
|
||||
res.assert_status(axum::http::StatusCode::PARTIAL_CONTENT);
|
||||
assert_eq!(res.as_bytes(), b"0123456789".as_ref());
|
||||
assert_eq!(res.header(header::CONTENT_RANGE), "bytes 0-9/10");
|
||||
assert_eq!(res.header(header::CONTENT_LENGTH), "10");
|
||||
truncate(&file_repository.pool).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn test_download_file_not_found() {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{#if isVideo}
|
||||
<VideoPreview src={`/api/files/${file.id}/download`} />
|
||||
<VideoPreview src={`/api/files/${file.id}/stream_part`} />
|
||||
<div
|
||||
class="pointer-events-none absolute inset-x-0 top-0 z-10 flex items-center justify-between bg-linear-to-b from-black/70 to-transparent px-4 py-3"
|
||||
>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"prettier": "^3.8.3",
|
||||
"prettier-plugin-svelte": "^4.1.0",
|
||||
"prettier-plugin-tailwindcss": "^0.8.0"
|
||||
}
|
||||
}
|
||||
253
pnpm-lock.yaml
generated
253
pnpm-lock.yaml
generated
@@ -1,253 +0,0 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
devDependencies:
|
||||
prettier:
|
||||
specifier: ^3.8.3
|
||||
version: 3.8.3
|
||||
prettier-plugin-svelte:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0(prettier@3.8.3)(svelte@5.56.0)
|
||||
prettier-plugin-tailwindcss:
|
||||
specifier: ^0.8.0
|
||||
version: 0.8.0(prettier-plugin-svelte@4.1.0(prettier@3.8.3)(svelte@5.56.0))(prettier@3.8.3)
|
||||
|
||||
packages:
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||
|
||||
'@jridgewell/remapping@2.3.5':
|
||||
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2':
|
||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5':
|
||||
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@sveltejs/acorn-typescript@1.0.10':
|
||||
resolution: {integrity: sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==}
|
||||
peerDependencies:
|
||||
acorn: ^8.9.0
|
||||
|
||||
'@types/estree@1.0.9':
|
||||
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
||||
|
||||
'@types/trusted-types@2.0.7':
|
||||
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||
|
||||
acorn@8.16.0:
|
||||
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
aria-query@5.3.1:
|
||||
resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
axobject-query@4.1.0:
|
||||
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
clsx@2.1.1:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
devalue@5.8.1:
|
||||
resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==}
|
||||
|
||||
esm-env@1.2.2:
|
||||
resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==}
|
||||
|
||||
esrap@2.2.9:
|
||||
resolution: {integrity: sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A==}
|
||||
peerDependencies:
|
||||
'@typescript-eslint/types': ^8.2.0
|
||||
peerDependenciesMeta:
|
||||
'@typescript-eslint/types':
|
||||
optional: true
|
||||
|
||||
is-reference@3.0.3:
|
||||
resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==}
|
||||
|
||||
locate-character@3.0.0:
|
||||
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
|
||||
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
prettier-plugin-svelte@4.1.0:
|
||||
resolution: {integrity: sha512-YZkhA2Q9oOerFFG9tq+2f98WYT7Z2JgrybJrAyrB78jpsH9i/DdgplXemehuFPgsldetFNCcR/yCcYlDjPy94Q==}
|
||||
engines: {node: '>=20'}
|
||||
peerDependencies:
|
||||
prettier: ^3.0.0
|
||||
svelte: ^5.0.0
|
||||
|
||||
prettier-plugin-tailwindcss@0.8.0:
|
||||
resolution: {integrity: sha512-V8ITGH87yuBDF6JpEZTOVlUz/saAwqb8f3HRgUj8Lh+tGCcrmorhsLpYqzygwFwK0PE2Ib6Mv3M7T/uE2tZV1g==}
|
||||
engines: {node: '>=20.19'}
|
||||
peerDependencies:
|
||||
'@ianvs/prettier-plugin-sort-imports': '*'
|
||||
'@prettier/plugin-hermes': '*'
|
||||
'@prettier/plugin-oxc': '*'
|
||||
'@prettier/plugin-pug': '*'
|
||||
'@shopify/prettier-plugin-liquid': '*'
|
||||
'@trivago/prettier-plugin-sort-imports': '*'
|
||||
'@zackad/prettier-plugin-twig': '*'
|
||||
prettier: ^3.0
|
||||
prettier-plugin-astro: '*'
|
||||
prettier-plugin-css-order: '*'
|
||||
prettier-plugin-jsdoc: '*'
|
||||
prettier-plugin-marko: '*'
|
||||
prettier-plugin-multiline-arrays: '*'
|
||||
prettier-plugin-organize-attributes: '*'
|
||||
prettier-plugin-organize-imports: '*'
|
||||
prettier-plugin-sort-imports: '*'
|
||||
prettier-plugin-svelte: '*'
|
||||
peerDependenciesMeta:
|
||||
'@ianvs/prettier-plugin-sort-imports':
|
||||
optional: true
|
||||
'@prettier/plugin-hermes':
|
||||
optional: true
|
||||
'@prettier/plugin-oxc':
|
||||
optional: true
|
||||
'@prettier/plugin-pug':
|
||||
optional: true
|
||||
'@shopify/prettier-plugin-liquid':
|
||||
optional: true
|
||||
'@trivago/prettier-plugin-sort-imports':
|
||||
optional: true
|
||||
'@zackad/prettier-plugin-twig':
|
||||
optional: true
|
||||
prettier-plugin-astro:
|
||||
optional: true
|
||||
prettier-plugin-css-order:
|
||||
optional: true
|
||||
prettier-plugin-jsdoc:
|
||||
optional: true
|
||||
prettier-plugin-marko:
|
||||
optional: true
|
||||
prettier-plugin-multiline-arrays:
|
||||
optional: true
|
||||
prettier-plugin-organize-attributes:
|
||||
optional: true
|
||||
prettier-plugin-organize-imports:
|
||||
optional: true
|
||||
prettier-plugin-sort-imports:
|
||||
optional: true
|
||||
prettier-plugin-svelte:
|
||||
optional: true
|
||||
|
||||
prettier@3.8.3:
|
||||
resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
svelte@5.56.0:
|
||||
resolution: {integrity: sha512-kTXr26t1bchFp28ROrb957LtbujpBmBDibmqMGziVpUs7awBi96TGgX6SovrA8BNoEUDVRK2Fb9FkeYlGspoVg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
zimmerframe@1.1.4:
|
||||
resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
|
||||
'@jridgewell/remapping@2.3.5':
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2': {}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@sveltejs/acorn-typescript@1.0.10(acorn@8.16.0)':
|
||||
dependencies:
|
||||
acorn: 8.16.0
|
||||
|
||||
'@types/estree@1.0.9': {}
|
||||
|
||||
'@types/trusted-types@2.0.7': {}
|
||||
|
||||
acorn@8.16.0: {}
|
||||
|
||||
aria-query@5.3.1: {}
|
||||
|
||||
axobject-query@4.1.0: {}
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
devalue@5.8.1: {}
|
||||
|
||||
esm-env@1.2.2: {}
|
||||
|
||||
esrap@2.2.9:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
is-reference@3.0.3:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.9
|
||||
|
||||
locate-character@3.0.0: {}
|
||||
|
||||
magic-string@0.30.21:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
prettier-plugin-svelte@4.1.0(prettier@3.8.3)(svelte@5.56.0):
|
||||
dependencies:
|
||||
prettier: 3.8.3
|
||||
svelte: 5.56.0
|
||||
|
||||
prettier-plugin-tailwindcss@0.8.0(prettier-plugin-svelte@4.1.0(prettier@3.8.3)(svelte@5.56.0))(prettier@3.8.3):
|
||||
dependencies:
|
||||
prettier: 3.8.3
|
||||
optionalDependencies:
|
||||
prettier-plugin-svelte: 4.1.0(prettier@3.8.3)(svelte@5.56.0)
|
||||
|
||||
prettier@3.8.3: {}
|
||||
|
||||
svelte@5.56.0:
|
||||
dependencies:
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
'@sveltejs/acorn-typescript': 1.0.10(acorn@8.16.0)
|
||||
'@types/estree': 1.0.9
|
||||
'@types/trusted-types': 2.0.7
|
||||
acorn: 8.16.0
|
||||
aria-query: 5.3.1
|
||||
axobject-query: 4.1.0
|
||||
clsx: 2.1.1
|
||||
devalue: 5.8.1
|
||||
esm-env: 1.2.2
|
||||
esrap: 2.2.9
|
||||
is-reference: 3.0.3
|
||||
locate-character: 3.0.0
|
||||
magic-string: 0.30.21
|
||||
zimmerframe: 1.1.4
|
||||
transitivePeerDependencies:
|
||||
- '@typescript-eslint/types'
|
||||
|
||||
zimmerframe@1.1.4: {}
|
||||
Reference in New Issue
Block a user