From 40695684cf30bb1d54d7c4d99dda61abd1230820 Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Wed, 18 Feb 2026 05:49:19 +0900 Subject: [PATCH 1/8] feat(api): saveFile --- src/routes/+page.server.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/routes/+page.server.ts diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts new file mode 100644 index 0000000..9cc4ad6 --- /dev/null +++ b/src/routes/+page.server.ts @@ -0,0 +1,33 @@ +import { fail } from '@sveltejs/kit'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import type { Actions } from './$types'; + +export const actions = { + saveFile: async (event) => { + const data = await event.request.json(); + + const token = event.cookies.get('token'); // TODO choose token cookie name + + // TODO check token + if (token === undefined || token.length === 0) { + return fail(403, { success: false, errorMsg: 'Unauthorized access!' }); + } + const projectId: number = data.project; // TODO choose how project id is passed + + // TODO check user is allow to modify project + // TODO resolve project path from projectId + + const projectPath = '/tmp/example/' + projectId; + + const filename = data.filename; + const filePath = path.resolve(projectPath, filename); + if (!filePath.startsWith(projectPath)) { + return fail(403, { success: false, errorMsg: 'Unauthorized access!' }); + } + + fs.writeFileSync(filePath, data.fileContent); + return { success: true }; + }, +} satisfies Actions; From 3b4d06aa621620044eeb281e35c739e3f5497625 Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Wed, 18 Feb 2026 05:50:13 +0900 Subject: [PATCH 2/8] feat(api): saveFile --- src/routes/+page.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/routes/+page.ts b/src/routes/+page.ts index a72419a..e69de29 100644 --- a/src/routes/+page.ts +++ b/src/routes/+page.ts @@ -1,3 +0,0 @@ -// since there's no dynamic data here, we can prerender -// it so that it gets served as a static asset in production -export const prerender = true; From eb87594f0407a8aa36b88010a71ac2dfa7a42281 Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Sun, 8 Mar 2026 03:30:09 +0900 Subject: [PATCH 3/8] feat(back): filesystem complete --- .env.example | 9 +- eslint.config.js | 1 - package.json | 3 + pnpm-lock.yaml | 22 ++ src/demo.spec.ts | 45 ++- src/hooks.server.ts | 30 +- .../http/api/repository/base.repository.ts | 2 +- src/modules/http/client.ts | 3 +- src/modules/http/index.ts | 2 +- .../http/middlewares/auth.middleware.ts | 3 +- .../http/middlewares/logger.middleware.ts | 3 +- src/modules/project/save/save-handler.ts | 5 +- src/routes/+page.server.ts | 33 --- src/routes/fs/+page.server.ts | 270 ++++++++++++++++++ src/routes/fs/+page.svelte | 178 ++++++++++++ src/routes/loadProject/+page.server.ts | 63 ++++ src/routes/loadProject/+page.svelte | 20 ++ src/utils/http-client/index.ts | 1 - .../http-client.ts => http/client.ts} | 54 ++-- src/utils/http/index.ts | 6 + src/utils/index.ts | 1 + src/utils/server-api/clients.ts | 19 ++ src/utils/server-api/guards/auth.guard.ts | 37 +++ src/utils/server-api/guards/error.guard.ts | 33 +++ src/utils/server-api/guards/params.guard.ts | 32 +++ src/utils/server-api/index.ts | 8 + src/utils/server-api/repository.ts | 73 +++++ src/utils/server-api/types.ts | 9 + src/utils/server-api/utils.ts | 32 +++ svelte.config.js | 1 + 30 files changed, 928 insertions(+), 70 deletions(-) create mode 100644 src/routes/fs/+page.server.ts create mode 100644 src/routes/fs/+page.svelte create mode 100644 src/routes/loadProject/+page.server.ts create mode 100644 src/routes/loadProject/+page.svelte delete mode 100644 src/utils/http-client/index.ts rename src/utils/{http-client/http-client.ts => http/client.ts} (62%) create mode 100644 src/utils/http/index.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/server-api/clients.ts create mode 100644 src/utils/server-api/guards/auth.guard.ts create mode 100644 src/utils/server-api/guards/error.guard.ts create mode 100644 src/utils/server-api/guards/params.guard.ts create mode 100644 src/utils/server-api/index.ts create mode 100644 src/utils/server-api/repository.ts create mode 100644 src/utils/server-api/types.ts create mode 100644 src/utils/server-api/utils.ts diff --git a/.env.example b/.env.example index 99cbcd5..213e132 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,8 @@ -VITE_API_BASE_URL=http://localhost:3000 +# Leave empty for local environment / no api authentication +API_URL=http://localhost:3000 + +# Set to production +NODE_ENV=development + +# Leaving this empty will generate a new unique random session secret at start +SESSION_SECRET= diff --git a/eslint.config.js b/eslint.config.js index d394cb0..5fa6710 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -23,7 +23,6 @@ export default [ }, { rules: { - 'no-console': 'error', 'svelte/no-unused-svelte-ignore': 'off', }, }, diff --git a/package.json b/package.json index e791142..32a53cb 100644 --- a/package.json +++ b/package.json @@ -108,5 +108,8 @@ "src/**/*.{ts,svelte}": [ "eslint --fix" ] + }, + "dependencies": { + "svelte-kit-sessions": "^0.4.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a0302f..8bd02c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -147,6 +147,10 @@ catalogs: importers: .: + dependencies: + svelte-kit-sessions: + specifier: ^0.4.0 + version: 0.4.0(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)))(svelte@5.48.0) devDependencies: '@alexanderniebuhr/prettier-plugin-unocss': specifier: catalog:css @@ -703,6 +707,10 @@ packages: resolution: {integrity: sha512-eOgAX+eQpHvD/H4BMILc4tZ85XviTlwr/51RKkKUHozVVthj5avUPKP+4N4vcTUrqSscl2atTh9NbNTuvoBN0A==} engines: {node: '>=18.0.0'} + '@isaacs/ttlcache@1.4.1': + resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==} + engines: {node: '>=12'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -2794,6 +2802,12 @@ packages: svelte: optional: true + svelte-kit-sessions@0.4.0: + resolution: {integrity: sha512-cWjHwd+EGIuZ0p8CxSqE5EMOT8EUsoYfAnbE8QB+r6FonroYiMvTLUgv8b9dVLC55Yw3UtTntjUaZ5fKJF3XOA==} + peerDependencies: + '@sveltejs/kit': ^1.0.0 || ^2.0.0 + svelte: ^5.1.13 + svelte-sonner@1.0.7: resolution: {integrity: sha512-1EUFYmd7q/xfs2qCHwJzGPh9n5VJ3X6QjBN10fof2vxgy8fYE7kVfZ7uGnd7i6fQaWIr5KvXcwYXE/cmTEjk5A==} peerDependencies: @@ -3546,6 +3560,8 @@ snapshots: transitivePeerDependencies: - babel-plugin-macros + '@isaacs/ttlcache@1.4.1': {} + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -5626,6 +5642,12 @@ snapshots: optionalDependencies: svelte: 5.48.0 + svelte-kit-sessions@0.4.0(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)))(svelte@5.48.0): + dependencies: + '@isaacs/ttlcache': 1.4.1 + '@sveltejs/kit': 2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) + svelte: 5.48.0 + svelte-sonner@1.0.7(svelte@5.48.0): dependencies: runed: 0.28.0(svelte@5.48.0) diff --git a/src/demo.spec.ts b/src/demo.spec.ts index e2efa47..f80e516 100644 --- a/src/demo.spec.ts +++ b/src/demo.spec.ts @@ -1,5 +1,44 @@ -import { expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; -it('expect 1 + 2 = 3', async () => { - expect(1 + 2).equal(3); +import { load } from './routes/loadProject/+page.server'; + +describe('load', () => { + it('local project load and session cookie set', async () => { + const cookies = { + get: vi.fn(), + set: vi.fn(), + delete: vi.fn(), + }; + + const session = { + setData: vi.fn(), + save: vi.fn().mockImplementation(async () => { + cookies.set('session', 'abc123', { + path: '/', + httpOnly: true, + }); + }), + }; + + const event = { + cookies, + url: { + searchParams: { + get: (param: string) => (param === 'projectPath' ? '/tmp' : null), + }, + }, + locals: { session }, + } as any; + + await expect(load(event)).rejects.toMatchObject({ + status: 307, + location: '/', + }); + + expect(session.setData).toHaveBeenCalledWith({ path: '/tmp' }); + expect(cookies.set).toHaveBeenCalledWith('session', 'abc123', { + path: '/', + httpOnly: true, + }); + }); }); diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 917ce46..acc60a4 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,5 +1,31 @@ +import { env } from '$env/dynamic/private'; import { paraglideMiddleware } from '$lib/paraglide/server'; -import type { Handle } from '@sveltejs/kit'; +import { type Handle, redirect } from '@sveltejs/kit'; +import { sequence } from '@sveltejs/kit/hooks'; +import * as crypto from 'node:crypto'; +import { sveltekitSessionHandle } from 'svelte-kit-sessions'; + +declare module 'svelte-kit-sessions' { + interface SessionData { + path: string; + } +} + +if (!env.SESSION_SECRET) { + env.SESSION_SECRET = crypto.randomBytes(20).toString('hex'); + console.log(`SESSION_SECRET not found, generating a temporary one: ${env.SESSION_SECRET}`); +} + +const sessionHandle = sveltekitSessionHandle({ + secret: env.SESSION_SECRET, +}); + +const checkAuthorizationHandle: Handle = async ({ event, resolve }) => { + if (!event.locals.session.data.path && event.url.pathname !== '/loadProject') { + throw redirect(302, '/loadProject'); + } + return resolve(event); +}; const handleParaglide: Handle = ({ event, resolve }) => paraglideMiddleware(event.request, ({ request, locale }) => { @@ -10,4 +36,4 @@ const handleParaglide: Handle = ({ event, resolve }) => }); }); -export const handle: Handle = handleParaglide; +export const handle: Handle = sequence(sessionHandle, handleParaglide, checkAuthorizationHandle); diff --git a/src/modules/http/api/repository/base.repository.ts b/src/modules/http/api/repository/base.repository.ts index f3b01af..0974d8e 100644 --- a/src/modules/http/api/repository/base.repository.ts +++ b/src/modules/http/api/repository/base.repository.ts @@ -1,4 +1,4 @@ -import { type HttpClient, type RequestOptions } from '../../../../utils/http-client'; +import { type HttpClient, type RequestOptions } from '@utils/http'; export class BaseRepository { private readonly _client: HttpClient; diff --git a/src/modules/http/client.ts b/src/modules/http/client.ts index 113bab2..af605ae 100644 --- a/src/modules/http/client.ts +++ b/src/modules/http/client.ts @@ -1,4 +1,5 @@ -import { HttpClient } from '../../utils/http-client'; +import { HttpClient } from '@utils/http'; + import { AuthMiddleware } from './middlewares/auth.middleware'; import { LoggerMiddleware } from './middlewares/logger.middleware'; diff --git a/src/modules/http/index.ts b/src/modules/http/index.ts index b9705d9..b0e8587 100644 --- a/src/modules/http/index.ts +++ b/src/modules/http/index.ts @@ -1,2 +1,2 @@ -export { type RequestOptions } from '../../utils/http-client'; +export { type RequestOptions } from '@utils/http'; export * from './api'; diff --git a/src/modules/http/middlewares/auth.middleware.ts b/src/modules/http/middlewares/auth.middleware.ts index 998a3f6..7a3818a 100644 --- a/src/modules/http/middlewares/auth.middleware.ts +++ b/src/modules/http/middlewares/auth.middleware.ts @@ -1,5 +1,6 @@ +import type { MiddlewareNext, MiddlewareParams } from '@utils/http'; + import { authStore } from '../../../stores/auth.store'; -import type { MiddlewareNext, MiddlewareParams } from '../../../utils/http-client'; let accessToken: string | null = null; diff --git a/src/modules/http/middlewares/logger.middleware.ts b/src/modules/http/middlewares/logger.middleware.ts index ce8af21..722ebdc 100644 --- a/src/modules/http/middlewares/logger.middleware.ts +++ b/src/modules/http/middlewares/logger.middleware.ts @@ -1,7 +1,6 @@ +import type { MiddlewareNext, MiddlewareParams } from '@utils/http'; import { toast } from 'svelte-sonner'; -import type { MiddlewareNext, MiddlewareParams } from '../../../utils/http-client'; - export const LoggerMiddleware = async (params: MiddlewareParams, next: MiddlewareNext) => { const res = await next(params); if (res.ok) return res; diff --git a/src/modules/project/save/save-handler.ts b/src/modules/project/save/save-handler.ts index 9352a5a..718626c 100644 --- a/src/modules/project/save/save-handler.ts +++ b/src/modules/project/save/save-handler.ts @@ -1,4 +1,5 @@ -import { type FileSystemFile } from '../../../utils/file-system'; +import { type FileSystemFile } from '@utils/file-system'; + import type { ISave } from './save.type'; export class SaveHandler { @@ -21,7 +22,7 @@ export class SaveHandler { enableAutoSave(): void { if (this._autoSaveStatus !== null) return; - this._autoSaveStatus = setInterval(async () => { + this._autoSaveStatus = window.setInterval(async () => { await this.save(); }, this._autoSaveInterval); } diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 9cc4ad6..e69de29 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -1,33 +0,0 @@ -import { fail } from '@sveltejs/kit'; -import * as fs from 'node:fs'; -import * as path from 'node:path'; - -import type { Actions } from './$types'; - -export const actions = { - saveFile: async (event) => { - const data = await event.request.json(); - - const token = event.cookies.get('token'); // TODO choose token cookie name - - // TODO check token - if (token === undefined || token.length === 0) { - return fail(403, { success: false, errorMsg: 'Unauthorized access!' }); - } - const projectId: number = data.project; // TODO choose how project id is passed - - // TODO check user is allow to modify project - // TODO resolve project path from projectId - - const projectPath = '/tmp/example/' + projectId; - - const filename = data.filename; - const filePath = path.resolve(projectPath, filename); - if (!filePath.startsWith(projectPath)) { - return fail(403, { success: false, errorMsg: 'Unauthorized access!' }); - } - - fs.writeFileSync(filePath, data.fileContent); - return { success: true }; - }, -} satisfies Actions; diff --git a/src/routes/fs/+page.server.ts b/src/routes/fs/+page.server.ts new file mode 100644 index 0000000..0068972 --- /dev/null +++ b/src/routes/fs/+page.server.ts @@ -0,0 +1,270 @@ +import { fail } from '@sveltejs/kit'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import type { Actions } from './$types'; + +function checkPathIsInsideDir(filePath: string, projectPath: string) { + if (!filePath.startsWith(projectPath)) { + throw `Path ${filePath} outside of directory`; + } +} + +function checkPathExists(filePath: string) { + if (!fs.existsSync(filePath)) { + throw `Path ${filePath} should exist`; + } +} + +function checkPathNotExists(filePath: string) { + if (fs.existsSync(filePath)) { + throw `Path ${filePath} should not exist`; + } +} + +function checkPathIsFile(filePath: string) { + let stats: fs.Stats; + try { + stats = fs.lstatSync(filePath); + } catch { + throw `Path ${filePath} does not exist`; + } + if (!stats.isFile()) { + throw `Path ${filePath} is not a file`; + } +} + +function checkPathIsDir(filePath: string) { + let stats: fs.Stats; + try { + stats = fs.lstatSync(filePath); + } catch { + throw `Path ${filePath} does not exist`; + } + if (!stats.isDirectory()) { + throw `Path ${filePath} is not a directory`; + } +} + +function checkPathIsWritable(filePath: string) { + try { + fs.accessSync(filePath, fs.constants.W_OK); + } catch { + throw `Path ${filePath} writable`; + } +} + +function checkPathIsReadable(filePath: string) { + try { + fs.accessSync(filePath, fs.constants.R_OK); + } catch { + throw `Path ${filePath} writable`; + } +} + +function readDirContent( + absoluteDirPath: string, + recursive: boolean, +): { files: string[]; directories: {} } { + const dirContent: { files: string[]; directories: { [key: string]: any } } = { + files: [], + directories: {}, + }; + fs.readdirSync(absoluteDirPath, { withFileTypes: true, recursive: false }).forEach((item) => { + if (item.isFile()) { + dirContent.files.push(item.name); + } else if (item.isDirectory()) { + dirContent.directories[item.name] = recursive + ? readDirContent(absoluteDirPath + '/' + item.name, recursive) + : {}; + } + }); + return dirContent; +} + +export const actions = { + readFile: async ({ request, locals }) => { + const data = await request.json(); + + if (!data.filePath) { + return fail(403, { success: false, errorMsg: "Missing arg: 'filePath'" }); + } + + try { + const absoluteFilePath = path.resolve(locals.session.data.path, './' + data.filePath); + checkPathIsInsideDir(absoluteFilePath, locals.session.data.path); + checkPathExists(absoluteFilePath); + checkPathIsFile(absoluteFilePath); + checkPathIsReadable(absoluteFilePath); + return { success: true, fileContent: fs.readFileSync(absoluteFilePath).toString() }; + } catch (e) { + return fail(403, { success: false, errorMsg: e }); + } + }, + readDir: async ({ request, locals }) => { + const data = await request.json(); + + let dirPath = '/'; + if (data.dirPath) { + dirPath = data.dirPath; + } + + try { + const absoluteDirPath = path.resolve(locals.session.data.path, './' + dirPath); + checkPathIsInsideDir(absoluteDirPath, locals.session.data.path); + checkPathExists(absoluteDirPath); + checkPathIsDir(absoluteDirPath); + const dirContent = readDirContent(absoluteDirPath, false); + return { + success: true, + dirContent, + }; + } catch (e) { + return fail(403, { success: false, errorMsg: e }); + } + }, + + readDirRec: async ({ request, locals }) => { + const data = await request.json(); + + let dirPath = '/'; + if (data.dirPath) { + dirPath = data.dirPath; + } + + try { + const absoluteDirPath = path.resolve(locals.session.data.path, './' + dirPath); + checkPathIsInsideDir(absoluteDirPath, locals.session.data.path); + checkPathExists(absoluteDirPath); + checkPathIsDir(absoluteDirPath); + const dirContent = readDirContent(absoluteDirPath, true); + return { success: true, dirContent }; + } catch (e) { + return fail(403, { success: false, errorMsg: e }); + } + }, + + writeFile: async ({ request, locals }) => { + const data = await request.json(); + + if (!data.filePath) { + return fail(403, { success: false, errorMsg: "Missing arg: 'filePath'" }); + } + if (!data.fileContent) { + return fail(403, { success: false, errorMsg: "Missing arg: 'fileContent'" }); + } + + try { + const absoluteFilePath = path.resolve(locals.session.data.path, './' + data.filePath); + checkPathIsInsideDir(absoluteFilePath, locals.session.data.path); + if (fs.existsSync(absoluteFilePath)) { + checkPathIsFile(absoluteFilePath); + checkPathIsWritable(absoluteFilePath); + } else { + const folderPath = path.dirname(absoluteFilePath); + checkPathIsWritable(folderPath); + } + fs.writeFileSync(absoluteFilePath, data.fileContent, { flush: true }); + } catch (e) { + return fail(403, { success: false, errorMsg: e }); + } + return { success: true }; + }, + + deleteFile: async ({ request, locals }) => { + const data = await request.json(); + + if (!data.filePath) { + return fail(403, { success: false, errorMsg: "Missing arg: 'filePath'" }); + } + + try { + const absoluteFilePath = path.resolve(locals.session.data.path, './' + data.filePath); + checkPathIsInsideDir(absoluteFilePath, locals.session.data.path); + checkPathExists(absoluteFilePath); + checkPathIsFile(absoluteFilePath); + checkPathIsWritable(absoluteFilePath); + fs.rmSync(absoluteFilePath); + } catch (e) { + return fail(403, { success: false, errorMsg: e }); + } + return { success: true }; + }, + + renameFile: async ({ request, locals }) => { + const data = await request.json(); + + if (!data.filePath) { + return fail(403, { success: false, errorMsg: "Missing arg: 'filePath'" }); + } + if (!data.newFilePath) { + return fail(403, { success: false, errorMsg: "Missing arg: 'newFilePath'" }); + } + + try { + const absoluteFilePath = path.resolve(locals.session.data.path, './' + data.filePath); + const absoluteNewFilePath = path.resolve(locals.session.data.path, './' + data.newFilePath); + checkPathIsInsideDir(absoluteFilePath, locals.session.data.path); + checkPathIsInsideDir(absoluteNewFilePath, locals.session.data.path); + checkPathExists(absoluteFilePath); + checkPathIsFile(absoluteFilePath); + checkPathIsWritable(absoluteFilePath); + const newFolderPath = path.dirname(absoluteNewFilePath); + checkPathExists(newFolderPath); + checkPathIsWritable(newFolderPath); + checkPathNotExists(absoluteNewFilePath); + fs.renameSync(absoluteFilePath, absoluteNewFilePath); + } catch (e) { + return fail(403, { success: false, errorMsg: e }); + } + return { success: true }; + }, + + createDir: async ({ request, locals }) => { + const data = await request.json(); + + if (!data.dirPath) { + return fail(403, { success: false, errorMsg: "Missing arg: 'dirPath'" }); + } + + try { + const absoluteFilePath = path.resolve(locals.session.data.path, './' + data.dirPath); + checkPathIsInsideDir(absoluteFilePath, locals.session.data.path); + checkPathNotExists(absoluteFilePath); + + fs.mkdirSync(absoluteFilePath, { recursive: true }); + } catch (e) { + return fail(403, { success: false, errorMsg: e }); + } + return { success: true }; + }, + + renameDir: async ({ request, locals }) => { + const data = await request.json(); + + if (!data.dirPath) { + return fail(403, { success: false, errorMsg: "Missing arg: 'dirPath'" }); + } + if (!data.newDirPath) { + return fail(403, { success: false, errorMsg: "Missing arg: 'newDirPath'" }); + } + + try { + const absoluteDirPath = path.resolve(locals.session.data.path, './' + data.dirPath); + const absoluteNewDirPath = path.resolve(locals.session.data.path, './' + data.newDirPath); + checkPathIsInsideDir(absoluteDirPath, locals.session.data.path); + checkPathIsInsideDir(absoluteNewDirPath, locals.session.data.path); + checkPathExists(absoluteDirPath); + checkPathIsDir(absoluteDirPath); + checkPathIsWritable(absoluteDirPath); + const newFolderPath = path.dirname(absoluteNewDirPath); + checkPathExists(newFolderPath); + checkPathIsWritable(newFolderPath); + checkPathNotExists(absoluteNewDirPath); + fs.renameSync(absoluteDirPath, absoluteNewDirPath); + } catch (e) { + return fail(403, { success: false, errorMsg: e }); + } + return { success: true }; + }, +} satisfies Actions; diff --git a/src/routes/fs/+page.svelte b/src/routes/fs/+page.svelte new file mode 100644 index 0000000..304fe0c --- /dev/null +++ b/src/routes/fs/+page.svelte @@ -0,0 +1,178 @@ + + +
+
+
+ + Logo + +
+
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const response = await fetch('/fs?/readFile', { + method: 'POST', + body: JSON.stringify({ filePath: formData.get('filePath') }), + }); + const result = deserialize(await response.text()); + if (result.type === 'success' && result.data) { + readFileResult = result.data.fileContent; + } + }} + > + + +
+ {readFileResult} +
+
+
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const response = await fetch('/fs?/readDir', { + method: 'POST', + body: JSON.stringify({ dirPath: formData.get('dirPath') }), + }); + const result = deserialize(await response.text()); + readDirResult = ''; + if (result.type === 'success' && result.data) { + readDirResult += 'Files: ' + result.data.dirContent.files.join(' ') + '\n'; + readDirResult += 'Directories: '; + Object.keys(result.data.dirContent.directories).forEach((dir) => { + readDirResult += dir + ' '; + }); + } + }} + > + + +
+ {readDirResult} +
+
+
{ + function renderReadDirResult(dirContent, deep = 0) { + let res = ''; + res += ' '.repeat(deep) + '| Files: ' + dirContent.files.join(' ') + '\n'; + res += ' '.repeat(deep) + '| Directories:\n'; + Object.entries(dirContent.directories).forEach(([dirName, content]) => { + res += + ' '.repeat(deep) + + '|--- ' + + dirName + + '\n' + + renderReadDirResult(content, deep + 8); + }); + return res; + } + + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const response = await fetch('/fs?/readDirRec', { + method: 'POST', + body: JSON.stringify({ dirPath: formData.get('dirPath') }), + }); + const result = deserialize(await response.text()); + readDirRecResult = ''; + if (result.type === 'success' && result.data) { + readDirRecResult = renderReadDirResult(result.data.dirContent); + } + }} + > + + +
+ {readDirRecResult} +
+
+
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + fetch('/fs?/writeFile', { + method: 'POST', + body: JSON.stringify({ + filePath: formData.get('filePath'), + fileContent: formData.get('fileContent'), + }), + }); + }} + > + + + +
+
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + fetch('/fs?/deleteFile', { + method: 'POST', + body: JSON.stringify({ filePath: formData.get('filePath') }), + }); + }} + > + + +
+
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + fetch('/fs?/renameFile', { + method: 'POST', + body: JSON.stringify({ + filePath: formData.get('filePath'), + newFilePath: formData.get('newFilePath'), + }), + }); + }} + > + + + +
+
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + fetch('/fs?/createDir', { + method: 'POST', + body: JSON.stringify({ dirPath: formData.get('dirPath') }), + }); + }} + > + + +
+
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + fetch('/fs?/renameDir', { + method: 'POST', + body: JSON.stringify({ + dirPath: formData.get('dirPath'), + newDirPath: formData.get('newDirPath'), + }), + }); + }} + > + + + +
+
+
+
+
diff --git a/src/routes/loadProject/+page.server.ts b/src/routes/loadProject/+page.server.ts new file mode 100644 index 0000000..4e109bb --- /dev/null +++ b/src/routes/loadProject/+page.server.ts @@ -0,0 +1,63 @@ +import { env } from '$env/dynamic/private'; +import { redirect } from '@sveltejs/kit'; +import { authGuard } from '@utils/server-api'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ url, cookies, locals }) => { + const projectPath = url.searchParams.get('projectPath'); + const projectId = url.searchParams.get('projectId'); + let absoluteProjectPath: string = ''; + if (projectPath) { + if (env.API_URL) { + return { success: false, errorMsg: 'Cannot load local project if API_URL is present' }; + } + absoluteProjectPath = projectPath; + } else if (projectId) { + if (!env.API_URL) { + throw new Error('Missing API_URL'); + } + const accessToken = cookies.get('accessToken'); + + if (accessToken === undefined || accessToken.length === 0) { + return { success: false, errorMsg: 'Unauthorized access' }; + } + + const serverProjectPath = await authGuard(async (httpClient) => { + return await httpClient.post(`/editor/projects/${projectId}`); + }, cookies); + if (serverProjectPath.status !== 200) { + return { success: false, errorMsg: 'Cannot retrieve project from API' }; + } + absoluteProjectPath = (await serverProjectPath.json())['projectPath']; + } else { + return { success: false, errorMsg: 'No project provided' }; + } + + try { + const stats = fs.lstatSync(absoluteProjectPath); + if (!stats.isDirectory()) { + return { success: false, errorMsg: `Project folder ${projectPath} is not a folder` }; + } + } catch { + return { success: false, errorMsg: `Project folder ${projectPath} does not exist` }; + } + try { + fs.accessSync(absoluteProjectPath, fs.constants.W_OK); + } catch { + return { + success: false, + errorMsg: `Project folder ${projectPath} does not have the good rights`, + }; + } + absoluteProjectPath = path.resolve(absoluteProjectPath); + + const session = locals.session; + + await session.setData({ path: absoluteProjectPath }); + await session.save(); + + redirect(307, '/'); +}; diff --git a/src/routes/loadProject/+page.svelte b/src/routes/loadProject/+page.svelte new file mode 100644 index 0000000..585933a --- /dev/null +++ b/src/routes/loadProject/+page.svelte @@ -0,0 +1,20 @@ + + +
+
+
+ + Logo + +
+ {data.success ? 'Project loading' : 'Error: ' + data.errorMsg} +
+
+
+
diff --git a/src/utils/http-client/index.ts b/src/utils/http-client/index.ts deleted file mode 100644 index 29d4d49..0000000 --- a/src/utils/http-client/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './http-client'; diff --git a/src/utils/http-client/http-client.ts b/src/utils/http/client.ts similarity index 62% rename from src/utils/http-client/http-client.ts rename to src/utils/http/client.ts index 47b766e..b74bf1a 100644 --- a/src/utils/http-client/http-client.ts +++ b/src/utils/http/client.ts @@ -6,38 +6,44 @@ export interface MiddlewareParams { options: RequestOptions; } -export type MiddlewareNext = (params?: MiddlewareParams) => Promise; +export type FullResponse = Response & { content: any }; + +export type MiddlewareNext = (params?: MiddlewareParams) => Promise; export type Middleware = ( params: MiddlewareParams, next: MiddlewareNext, -) => Promise | undefined; +) => Promise | undefined; -type BaseRequest = (path: string, options: RequestOptions) => Promise; +type BaseRequest = (path: string, options: RequestOptions) => Promise; export class HttpClient { private readonly _baseUrl: string; private readonly _baseOptions: RequestOptions; private readonly _middlewares: Middleware[]; - constructor(baseUrl: string) { + constructor(baseUrl: string, options?: RequestOptions) { this._baseUrl = baseUrl; - this._baseOptions = {}; + this._baseOptions = options ?? { + headers: { + 'Content-Type': 'application/json', + }, + }; this._middlewares = []; } - get(path: string, options?: RequestOptions): Promise { + get(path: string, options?: RequestOptions): Promise { return this._applyMiddlewares(path, options, (newPath, newOptions) => { - return fetch(newPath, { + return this._request(newPath, { ...newOptions, method: 'GET', }); }); } - post(path: string, body?: any, options?: RequestOptions): Promise { + post(path: string, body?: string, options?: RequestOptions): Promise { return this._applyMiddlewares(path, options, (newPath, newOptions) => { - return fetch(newPath, { + return this._request(newPath, { ...newOptions, method: 'POST', body: body, @@ -45,9 +51,9 @@ export class HttpClient { }); } - put(path: string, body?: any, options?: RequestOptions): Promise { + put(path: string, body?: string, options?: RequestOptions): Promise { return this._applyMiddlewares(path, options, (newPath, newOptions) => { - return fetch(newPath, { + return this._request(newPath, { ...newOptions, method: 'PUT', body: body, @@ -55,9 +61,9 @@ export class HttpClient { }); } - patch(path: string, body?: any, options?: RequestOptions): Promise { - return this._applyMiddlewares(path, options, (newPath, newOptions) => { - return fetch(newPath, { + patch(path: string, body?: string, options?: RequestOptions): Promise { + return this._applyMiddlewares(path, options, async (newPath, newOptions) => { + return this._request(newPath, { ...newOptions, method: 'PATCH', body: body, @@ -65,12 +71,12 @@ export class HttpClient { }); } - delete(path: string, options?: RequestOptions): Promise { + delete(path: string, options?: RequestOptions): Promise { return this._applyMiddlewares(path, options, (newPath, newOptions) => { - return fetch(newPath, { + return this._request(newPath, { ...newOptions, method: 'DELETE', - }); + }) as Promise; }); } @@ -79,11 +85,17 @@ export class HttpClient { return this; } + private async _request(path: string, request: RequestInit): Promise { + const res = (await fetch(path, request)) as FullResponse; + res.content = null; + return res; + } + private _applyMiddlewares( path: string, options: RequestOptions | undefined, callback: BaseRequest, - ): Promise { + ): Promise { const baseParams = { path, fullPath: this._getUrl(path), @@ -93,14 +105,14 @@ export class HttpClient { }, }; const middlewares = this._middlewares.slice(); - let response: Response; + let response: FullResponse; - const execution = async (params?: MiddlewareParams): Promise => { + const execution = async (params?: MiddlewareParams): Promise => { if (!params) params = baseParams; const middleware = middlewares.shift(); - if (!middleware) response = (await callback(params.fullPath, params.options)) as Response; + if (!middleware) response = (await callback(params.fullPath, params.options)) as FullResponse; else response = (await middleware(params, execution)) ?? response; return response; diff --git a/src/utils/http/index.ts b/src/utils/http/index.ts new file mode 100644 index 0000000..2d6ee12 --- /dev/null +++ b/src/utils/http/index.ts @@ -0,0 +1,6 @@ +export { + HttpClient, + type MiddlewareNext, + type MiddlewareParams, + type RequestOptions, +} from './client'; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..c202386 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1 @@ +export * from './http'; diff --git a/src/utils/server-api/clients.ts b/src/utils/server-api/clients.ts new file mode 100644 index 0000000..70e744a --- /dev/null +++ b/src/utils/server-api/clients.ts @@ -0,0 +1,19 @@ +import { env } from '$env/dynamic/private'; +import { HttpClient } from '@utils/http'; + +import { Repository } from './repository'; + +const client = new HttpClient(env.API_URL ?? ''); + +export const serverApi = new Repository(client); + +export const withAuth = (token: string) => { + return new Repository( + new HttpClient(env.API_URL ?? '', { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }), + ); +}; diff --git a/src/utils/server-api/guards/auth.guard.ts b/src/utils/server-api/guards/auth.guard.ts new file mode 100644 index 0000000..18b44a7 --- /dev/null +++ b/src/utils/server-api/guards/auth.guard.ts @@ -0,0 +1,37 @@ +import { type Cookies, json } from '@sveltejs/kit'; +import type { Repository } from '@utils/server-api'; + +import { serverApi, withAuth } from '../clients'; +import type { RefreshTokenInput, Token } from '../types'; +import { setTokensInCookies } from '../utils'; +import { errorGuard } from './error.guard'; + +export const authGuard = async ( + callback: (httpClient: Repository) => Promise, + cookies: Cookies, +): Promise => { + return errorGuard(async () => { + let accessToken = cookies.get('accessToken'); + + if (!accessToken) { + const refreshToken = cookies.get('refreshToken'); + + if (!refreshToken) { + return json({ error: 'Unauthorized', message: 'No token found' }, { status: 401 }); + } + + try { + const tokens = await serverApi.post('/auth/refresh-token', { + refreshToken, + }); + + setTokensInCookies(cookies, tokens); + accessToken = tokens.accessToken; + } catch { + return json({ error: 'Unauthorized', message: 'Invalid refresh token' }, { status: 401 }); + } + } + + return callback(withAuth(accessToken)); + }); +}; diff --git a/src/utils/server-api/guards/error.guard.ts b/src/utils/server-api/guards/error.guard.ts new file mode 100644 index 0000000..95b6e29 --- /dev/null +++ b/src/utils/server-api/guards/error.guard.ts @@ -0,0 +1,33 @@ +import { json } from '@sveltejs/kit'; +import { STATUS_CODES } from 'node:http'; + +export const errorGuard = async (callback: () => Promise): Promise => { + try { + return await callback(); + } catch (error: any) { + const data: + | { + statusCode: number; + path: string; + error: { + message: string | string[]; + timestamp: string; + cause?: { + message: string; + }; + }; + } + | undefined = error?.cause; + + const statusCode = data?.statusCode ?? 500; + + return json( + { + error: STATUS_CODES[statusCode] || 'Unknown error', + message: data?.error?.message || 'Unknown error', + cause: data?.error?.cause?.message || undefined, + }, + { status: statusCode }, + ); + } +}; diff --git a/src/utils/server-api/guards/params.guard.ts b/src/utils/server-api/guards/params.guard.ts new file mode 100644 index 0000000..2073c67 --- /dev/null +++ b/src/utils/server-api/guards/params.guard.ts @@ -0,0 +1,32 @@ +import { type RequestEvent, json } from '@sveltejs/kit'; + +export const parseParams = (event: RequestEvent, params: string[], regex: RegExp) => { + const searchParams = event.url.pathname.match(regex)?.slice(1); + + const res: Record = {}; + let i = 0; + + for (const param of params) { + res[param] = searchParams?.[i] ?? null; + i++; + } + + return res; +}; + +export const paramsGuard = async ( + event: RequestEvent, + rawParams: string[], + regex: RegExp, + callback: (params: Record) => Promise, +): Promise => { + const params = parseParams(event, rawParams, regex); + + for (const param in params) { + if (!params[param]) { + return json({ error: 'Missing required parameters' }, { status: 400 }); + } + } + + return callback(params as Record); +}; diff --git a/src/utils/server-api/index.ts b/src/utils/server-api/index.ts new file mode 100644 index 0000000..fcdf960 --- /dev/null +++ b/src/utils/server-api/index.ts @@ -0,0 +1,8 @@ +export { serverApi } from './clients'; +export { Repository } from './repository'; +export type { Token } from './types'; +export { resetTokensInCookies, setTokensInCookies } from './utils'; + +export { authGuard } from './guards/auth.guard'; +export { errorGuard } from './guards/error.guard'; +export { paramsGuard } from './guards/params.guard'; diff --git a/src/utils/server-api/repository.ts b/src/utils/server-api/repository.ts new file mode 100644 index 0000000..88a4f27 --- /dev/null +++ b/src/utils/server-api/repository.ts @@ -0,0 +1,73 @@ +import type { HttpClient, RequestOptions } from '@utils/http'; + +export class Repository { + private readonly _client: HttpClient; + + constructor(client: HttpClient) { + this._client = client; + } + + get(path: string, options?: RequestOptions): Promise { + return this.runRequest('get', path, options); + } + + post( + path: string, + body?: I, + options?: RequestOptions, + ): Promise { + return this.runRequestBody('post', path, body, options); + } + + put( + path: string, + body?: I, + options?: RequestOptions, + ): Promise { + return this.runRequestBody('put', path, body, options); + } + + patch( + path: string, + body?: I, + options?: RequestOptions, + ): Promise { + return this.runRequestBody('patch', path, body, options); + } + + delete(path: string, options?: RequestOptions): Promise { + return this.runRequest('delete', path, options); + } + + private async runRequest( + request: 'get' | 'delete', + path: string, + options?: RequestOptions, + ): Promise { + const res = await this._client[request](path, options); + if (!res.ok) + throw new Error(`Request failed with status code ${res.status}`, { + cause: res, + }); + return (await res.json()) as R; + } + + private async runRequestBody( + request: 'post' | 'put' | 'patch', + path: string, + body?: I, + options?: RequestOptions, + ): Promise { + const res = await this._client[request]( + path, + body === undefined ? undefined : JSON.stringify(body), + options, + ); + const data = (await res.json()) as R; + if (!res.ok) + throw new Error(`Request failed with status code ${res.status}`, { + cause: data, + }); + return data; + } +} diff --git a/src/utils/server-api/types.ts b/src/utils/server-api/types.ts new file mode 100644 index 0000000..8bd14d9 --- /dev/null +++ b/src/utils/server-api/types.ts @@ -0,0 +1,9 @@ +export interface Token { + accessToken: string; + refreshToken: string; + tokenExpiresAt: string; +} + +export interface RefreshTokenInput { + refreshToken: string; +} diff --git a/src/utils/server-api/utils.ts b/src/utils/server-api/utils.ts new file mode 100644 index 0000000..cad5ba9 --- /dev/null +++ b/src/utils/server-api/utils.ts @@ -0,0 +1,32 @@ +import { env } from '$env/dynamic/private'; +import type { Cookies, RequestEvent } from '@sveltejs/kit'; + +import type { Token } from './types'; + +export const setTokensInCookies = ( + cookies: Cookies, + { accessToken, refreshToken, tokenExpiresAt }: Token, +) => { + cookies.set('accessToken', accessToken, { + httpOnly: true, + secure: env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/', + expires: new Date(tokenExpiresAt), + }); + + cookies.set('refreshToken', refreshToken, { + httpOnly: true, + secure: env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/', + maxAge: 60 * 60 * 24 * 30, // 30 days + }); +}; + +export const resetTokensInCookies = (event: RequestEvent) => { + const { cookies } = event; + + cookies.delete('accessToken', { path: '/' }); + cookies.delete('refreshToken', { path: '/' }); +}; diff --git a/svelte.config.js b/svelte.config.js index 0eab45a..0c50cd8 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -10,5 +10,6 @@ export default { }, kit: { adapter: adapter(), + alias: { '@utils/*': './src/utils/*' }, }, }; From 26a27ab7943c39b2d51c90a654f09d01774b78f5 Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Sun, 8 Mar 2026 18:01:15 +0900 Subject: [PATCH 4/8] fix(back): reviews --- package.json | 2 +- pnpm-lock.yaml | 5 +- pnpm-workspace.yaml | 1 + src/app.d.ts | 6 ++ src/hooks.server.ts | 10 +-- src/{utils/http => lib/server}/client.ts | 0 .../server}/utils/server-api/clients.ts | 0 .../utils/server-api/guards/auth.guard.ts | 2 +- .../utils/server-api/guards/error.guard.ts | 0 .../utils/server-api/guards/params.guard.ts | 0 .../server}/utils/server-api/index.ts | 0 .../server}/utils/server-api/repository.ts | 0 .../server}/utils/server-api/types.ts | 0 .../server}/utils/server-api/utils.ts | 0 .../file-system/file-system-directory.ts | 0 .../file-system/file-system-file.ts | 0 .../file-system/file-system-handler.ts | 0 .../file-system/file-system-manager.ts | 0 .../utils-client}/file-system/index.ts | 0 .../utils-client}/local-file-system/index.ts | 0 .../local-file-system/local-file-system.ts | 0 src/{ => lib}/utils/http/index.ts | 2 +- src/{ => lib}/utils/index.ts | 0 src/modules/http/api/client.ts | 6 -- src/modules/http/api/index.ts | 1 - .../http/api/repository/base.repository.ts | 29 --------- .../component-storage.repository.ts | 7 --- src/modules/http/client.ts | 9 --- src/modules/http/index.ts | 2 - .../http/middlewares/auth.middleware.ts | 19 ------ .../http/middlewares/logger.middleware.ts | 18 ------ src/modules/project/project-manager.ts | 63 ------------------- src/modules/project/project.ts | 16 ----- src/modules/project/save/save-handler.ts | 35 ----------- src/modules/project/save/save.type.ts | 1 - .../+page.server.ts | 9 +-- .../+page.svelte | 0 .../load-project/load-project.spec.ts} | 2 +- svelte.config.js | 6 +- 39 files changed, 24 insertions(+), 227 deletions(-) rename src/{utils/http => lib/server}/client.ts (100%) rename src/{ => lib/server}/utils/server-api/clients.ts (100%) rename src/{ => lib/server}/utils/server-api/guards/auth.guard.ts (95%) rename src/{ => lib/server}/utils/server-api/guards/error.guard.ts (100%) rename src/{ => lib/server}/utils/server-api/guards/params.guard.ts (100%) rename src/{ => lib/server}/utils/server-api/index.ts (100%) rename src/{ => lib/server}/utils/server-api/repository.ts (100%) rename src/{ => lib/server}/utils/server-api/types.ts (100%) rename src/{ => lib/server}/utils/server-api/utils.ts (100%) rename src/{utils => lib/utils-client}/file-system/file-system-directory.ts (100%) rename src/{utils => lib/utils-client}/file-system/file-system-file.ts (100%) rename src/{utils => lib/utils-client}/file-system/file-system-handler.ts (100%) rename src/{utils => lib/utils-client}/file-system/file-system-manager.ts (100%) rename src/{utils => lib/utils-client}/file-system/index.ts (100%) rename src/{utils => lib/utils-client}/local-file-system/index.ts (100%) rename src/{utils => lib/utils-client}/local-file-system/local-file-system.ts (100%) rename src/{ => lib}/utils/http/index.ts (75%) rename src/{ => lib}/utils/index.ts (100%) delete mode 100644 src/modules/http/api/client.ts delete mode 100644 src/modules/http/api/index.ts delete mode 100644 src/modules/http/api/repository/base.repository.ts delete mode 100644 src/modules/http/api/repository/repositories/component-storage.repository.ts delete mode 100644 src/modules/http/client.ts delete mode 100644 src/modules/http/index.ts delete mode 100644 src/modules/http/middlewares/auth.middleware.ts delete mode 100644 src/modules/http/middlewares/logger.middleware.ts delete mode 100644 src/modules/project/project-manager.ts delete mode 100644 src/modules/project/project.ts delete mode 100644 src/modules/project/save/save-handler.ts delete mode 100644 src/modules/project/save/save.type.ts rename src/routes/{loadProject => load-project}/+page.server.ts (85%) rename src/routes/{loadProject => load-project}/+page.svelte (100%) rename src/{demo.spec.ts => routes/load-project/load-project.spec.ts} (94%) diff --git a/package.json b/package.json index 32a53cb..b65d3bc 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,6 @@ ] }, "dependencies": { - "svelte-kit-sessions": "^0.4.0" + "svelte-kit-sessions": "catalog:core" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8bd02c1..ab55f10 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,6 +60,9 @@ catalogs: svelte-check: specifier: ^4.3.5 version: 4.3.5 + svelte-kit-sessions: + specifier: ^0.4.0 + version: 0.4.0 vite: specifier: ^7.3.1 version: 7.3.1 @@ -149,7 +152,7 @@ importers: .: dependencies: svelte-kit-sessions: - specifier: ^0.4.0 + specifier: catalog:core version: 0.4.0(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)))(svelte@5.48.0) devDependencies: '@alexanderniebuhr/prettier-plugin-unocss': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index cb86f5d..beede95 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -21,6 +21,7 @@ catalogs: svelte: ^5.48.0 svelte-check: ^4.3.5 vite: ^7.3.1 + svelte-kit-sessions: "^0.4.0" css: '@alexanderniebuhr/prettier-plugin-unocss': ^0.0.4 '@unocss/extractor-svelte': ^66.6.3 diff --git a/src/app.d.ts b/src/app.d.ts index 520c421..a43c4ed 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -10,4 +10,10 @@ declare global { } } +declare module 'svelte-kit-sessions' { + interface SessionData { + path: string; + } +} + export {}; diff --git a/src/hooks.server.ts b/src/hooks.server.ts index acc60a4..22b59c5 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -5,12 +5,6 @@ import { sequence } from '@sveltejs/kit/hooks'; import * as crypto from 'node:crypto'; import { sveltekitSessionHandle } from 'svelte-kit-sessions'; -declare module 'svelte-kit-sessions' { - interface SessionData { - path: string; - } -} - if (!env.SESSION_SECRET) { env.SESSION_SECRET = crypto.randomBytes(20).toString('hex'); console.log(`SESSION_SECRET not found, generating a temporary one: ${env.SESSION_SECRET}`); @@ -21,8 +15,8 @@ const sessionHandle = sveltekitSessionHandle({ }); const checkAuthorizationHandle: Handle = async ({ event, resolve }) => { - if (!event.locals.session.data.path && event.url.pathname !== '/loadProject') { - throw redirect(302, '/loadProject'); + if (!event.locals.session.data.path && event.url.pathname !== '/load-project') { + throw redirect(302, '/load-project'); } return resolve(event); }; diff --git a/src/utils/http/client.ts b/src/lib/server/client.ts similarity index 100% rename from src/utils/http/client.ts rename to src/lib/server/client.ts diff --git a/src/utils/server-api/clients.ts b/src/lib/server/utils/server-api/clients.ts similarity index 100% rename from src/utils/server-api/clients.ts rename to src/lib/server/utils/server-api/clients.ts diff --git a/src/utils/server-api/guards/auth.guard.ts b/src/lib/server/utils/server-api/guards/auth.guard.ts similarity index 95% rename from src/utils/server-api/guards/auth.guard.ts rename to src/lib/server/utils/server-api/guards/auth.guard.ts index 18b44a7..ef0372c 100644 --- a/src/utils/server-api/guards/auth.guard.ts +++ b/src/lib/server/utils/server-api/guards/auth.guard.ts @@ -1,7 +1,7 @@ import { type Cookies, json } from '@sveltejs/kit'; -import type { Repository } from '@utils/server-api'; import { serverApi, withAuth } from '../clients'; +import type { Repository } from '../index'; import type { RefreshTokenInput, Token } from '../types'; import { setTokensInCookies } from '../utils'; import { errorGuard } from './error.guard'; diff --git a/src/utils/server-api/guards/error.guard.ts b/src/lib/server/utils/server-api/guards/error.guard.ts similarity index 100% rename from src/utils/server-api/guards/error.guard.ts rename to src/lib/server/utils/server-api/guards/error.guard.ts diff --git a/src/utils/server-api/guards/params.guard.ts b/src/lib/server/utils/server-api/guards/params.guard.ts similarity index 100% rename from src/utils/server-api/guards/params.guard.ts rename to src/lib/server/utils/server-api/guards/params.guard.ts diff --git a/src/utils/server-api/index.ts b/src/lib/server/utils/server-api/index.ts similarity index 100% rename from src/utils/server-api/index.ts rename to src/lib/server/utils/server-api/index.ts diff --git a/src/utils/server-api/repository.ts b/src/lib/server/utils/server-api/repository.ts similarity index 100% rename from src/utils/server-api/repository.ts rename to src/lib/server/utils/server-api/repository.ts diff --git a/src/utils/server-api/types.ts b/src/lib/server/utils/server-api/types.ts similarity index 100% rename from src/utils/server-api/types.ts rename to src/lib/server/utils/server-api/types.ts diff --git a/src/utils/server-api/utils.ts b/src/lib/server/utils/server-api/utils.ts similarity index 100% rename from src/utils/server-api/utils.ts rename to src/lib/server/utils/server-api/utils.ts diff --git a/src/utils/file-system/file-system-directory.ts b/src/lib/utils-client/file-system/file-system-directory.ts similarity index 100% rename from src/utils/file-system/file-system-directory.ts rename to src/lib/utils-client/file-system/file-system-directory.ts diff --git a/src/utils/file-system/file-system-file.ts b/src/lib/utils-client/file-system/file-system-file.ts similarity index 100% rename from src/utils/file-system/file-system-file.ts rename to src/lib/utils-client/file-system/file-system-file.ts diff --git a/src/utils/file-system/file-system-handler.ts b/src/lib/utils-client/file-system/file-system-handler.ts similarity index 100% rename from src/utils/file-system/file-system-handler.ts rename to src/lib/utils-client/file-system/file-system-handler.ts diff --git a/src/utils/file-system/file-system-manager.ts b/src/lib/utils-client/file-system/file-system-manager.ts similarity index 100% rename from src/utils/file-system/file-system-manager.ts rename to src/lib/utils-client/file-system/file-system-manager.ts diff --git a/src/utils/file-system/index.ts b/src/lib/utils-client/file-system/index.ts similarity index 100% rename from src/utils/file-system/index.ts rename to src/lib/utils-client/file-system/index.ts diff --git a/src/utils/local-file-system/index.ts b/src/lib/utils-client/local-file-system/index.ts similarity index 100% rename from src/utils/local-file-system/index.ts rename to src/lib/utils-client/local-file-system/index.ts diff --git a/src/utils/local-file-system/local-file-system.ts b/src/lib/utils-client/local-file-system/local-file-system.ts similarity index 100% rename from src/utils/local-file-system/local-file-system.ts rename to src/lib/utils-client/local-file-system/local-file-system.ts diff --git a/src/utils/http/index.ts b/src/lib/utils/http/index.ts similarity index 75% rename from src/utils/http/index.ts rename to src/lib/utils/http/index.ts index 2d6ee12..866345e 100644 --- a/src/utils/http/index.ts +++ b/src/lib/utils/http/index.ts @@ -3,4 +3,4 @@ export { type MiddlewareNext, type MiddlewareParams, type RequestOptions, -} from './client'; +} from '../../server/client'; diff --git a/src/utils/index.ts b/src/lib/utils/index.ts similarity index 100% rename from src/utils/index.ts rename to src/lib/utils/index.ts diff --git a/src/modules/http/api/client.ts b/src/modules/http/api/client.ts deleted file mode 100644 index 4d96e60..0000000 --- a/src/modules/http/api/client.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { client } from '../client'; -import { ComponentStorageRepository } from './repository/repositories/component-storage.repository'; - -export const api = { - componentStorage: new ComponentStorageRepository(client), -}; diff --git a/src/modules/http/api/index.ts b/src/modules/http/api/index.ts deleted file mode 100644 index 658b70d..0000000 --- a/src/modules/http/api/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { api } from './client'; diff --git a/src/modules/http/api/repository/base.repository.ts b/src/modules/http/api/repository/base.repository.ts deleted file mode 100644 index 0974d8e..0000000 --- a/src/modules/http/api/repository/base.repository.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { type HttpClient, type RequestOptions } from '@utils/http'; - -export class BaseRepository { - private readonly _client: HttpClient; - - constructor(client: HttpClient) { - this._client = client; - } - - protected get(path: string, options?: RequestOptions): Promise { - return this._client.get(path, options); - } - - protected post(path: string, body: object, options?: RequestOptions): Promise { - return this._client.post(path, JSON.stringify(body), options); - } - - protected put(path: string, body: object, options?: RequestOptions): Promise { - return this._client.put(path, JSON.stringify(body), options); - } - - protected patch(path: string, body: object, options?: RequestOptions): Promise { - return this._client.patch(path, JSON.stringify(body), options); - } - - protected delete(path: string, options?: RequestOptions): Promise { - return this._client.delete(path, options); - } -} diff --git a/src/modules/http/api/repository/repositories/component-storage.repository.ts b/src/modules/http/api/repository/repositories/component-storage.repository.ts deleted file mode 100644 index 1d54e28..0000000 --- a/src/modules/http/api/repository/repositories/component-storage.repository.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { BaseRepository } from '../base.repository'; - -export class ComponentStorageRepository extends BaseRepository { - getManifest(componentCode: string): Promise { - return this.get(`/components/storage/${componentCode}/manifest`); - } -} diff --git a/src/modules/http/client.ts b/src/modules/http/client.ts deleted file mode 100644 index af605ae..0000000 --- a/src/modules/http/client.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { HttpClient } from '@utils/http'; - -import { AuthMiddleware } from './middlewares/auth.middleware'; -import { LoggerMiddleware } from './middlewares/logger.middleware'; - -export const client = new HttpClient(import.meta.env.VITE_API_BASE_URL).useMiddlewares( - AuthMiddleware, - LoggerMiddleware, -); diff --git a/src/modules/http/index.ts b/src/modules/http/index.ts deleted file mode 100644 index b0e8587..0000000 --- a/src/modules/http/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { type RequestOptions } from '@utils/http'; -export * from './api'; diff --git a/src/modules/http/middlewares/auth.middleware.ts b/src/modules/http/middlewares/auth.middleware.ts deleted file mode 100644 index 7a3818a..0000000 --- a/src/modules/http/middlewares/auth.middleware.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { MiddlewareNext, MiddlewareParams } from '@utils/http'; - -import { authStore } from '../../../stores/auth.store'; - -let accessToken: string | null = null; - -authStore.subscribe((auth) => { - accessToken = auth.accessToken; -}); - -export const AuthMiddleware = async (params: MiddlewareParams, next: MiddlewareNext) => { - if (accessToken) { - params.options.headers = { - ...params.options.headers, - Authorization: `Bearer ${accessToken}`, - }; - } - return next(params); -}; diff --git a/src/modules/http/middlewares/logger.middleware.ts b/src/modules/http/middlewares/logger.middleware.ts deleted file mode 100644 index 722ebdc..0000000 --- a/src/modules/http/middlewares/logger.middleware.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { MiddlewareNext, MiddlewareParams } from '@utils/http'; -import { toast } from 'svelte-sonner'; - -export const LoggerMiddleware = async (params: MiddlewareParams, next: MiddlewareNext) => { - const res = await next(params); - if (res.ok) return res; - const content = await res.json(); - const message = content?.error?.message - ? Array.isArray(content.error.message) - ? content.error.message.map((err: string) => `- ${err}`).join('\n') - : content.error.message - : 'Unknown error'; - toast.error(`An error occured :\n${message}`); - - throw new Error(message, { - cause: res, - }); -}; diff --git a/src/modules/project/project-manager.ts b/src/modules/project/project-manager.ts deleted file mode 100644 index 087349b..0000000 --- a/src/modules/project/project-manager.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { FileSystemManager } from '../../utils/file-system'; -import { LocalFileSystem } from '../../utils/local-file-system'; -import { Project } from './project'; -import { SaveHandler } from './save/save-handler'; - -export class ProjectManager { - private _currentId: string | null = null; - private _current: Project | null = null; - private _fs: FileSystemManager; - - constructor() { - this._fs = new FileSystemManager('projects'); - } - - getProject(): Project { - if (!this._current) throw new Error('No project loaded'); - return this._current; - } - - async create(name: string): Promise { - if (await this._fs.directoryExist(name)) - throw new Error(`Project named "${name}" already exists`); - - await this._loadProject(name); - await this._postLoad(); - } - - async loadFromSave(name: string): Promise { - if (!(await this._fs.directoryExist(name))) - throw new Error(`Project named "${name}" does not exists`); - - await this._loadProject(name); - await this._postLoad(); - } - - async loadFromLocal(name: string): Promise { - if (await this._fs.directoryExist(name)) - throw new Error(`Project named "${name}" already exists`); - - await this._loadProject(name); - const localFs = new LocalFileSystem('projects'); - await localFs.askFileAndCache(`${name}/save.nfps`); - await this._postLoad(); - } - - close(): void { - this._currentId = null; - this._current = null; - } - - private async _loadProject(name: string): Promise { - this._currentId = name; - const dir = await this._fs.getDirectory(name); - - const save = new SaveHandler(await dir.getFile('save.nfps')); - this._current = new Project(dir, save); - } - - private async _postLoad(): Promise { - await this._current?.save.fetchSave(); - this._current?.save.enableAutoSave(); - } -} diff --git a/src/modules/project/project.ts b/src/modules/project/project.ts deleted file mode 100644 index b5184fa..0000000 --- a/src/modules/project/project.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { type FileSystemDirectory } from '../../utils/file-system'; -import type { SaveHandler } from './save/save-handler'; - -export class Project { - private readonly _fs: FileSystemDirectory; - private readonly _saveHandler: SaveHandler; - - constructor(fs: FileSystemDirectory, saveHandler: SaveHandler) { - this._fs = fs; - this._saveHandler = saveHandler; - } - - get save(): SaveHandler { - return this._saveHandler; - } -} diff --git a/src/modules/project/save/save-handler.ts b/src/modules/project/save/save-handler.ts deleted file mode 100644 index 718626c..0000000 --- a/src/modules/project/save/save-handler.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { type FileSystemFile } from '@utils/file-system'; - -import type { ISave } from './save.type'; - -export class SaveHandler { - public data!: ISave; - private _fs!: FileSystemFile; - private _autoSaveStatus: number | null = null; - private _autoSaveInterval: number = 10 * 1000; - - constructor(fs: FileSystemFile) { - this._fs = fs; - } - - async fetchSave(): Promise { - this.data = await this._fs.readJson(); - } - - async save(): Promise { - await this._fs.writeJson(this.data); - } - - enableAutoSave(): void { - if (this._autoSaveStatus !== null) return; - this._autoSaveStatus = window.setInterval(async () => { - await this.save(); - }, this._autoSaveInterval); - } - - disableAutoSave(): void { - if (this._autoSaveStatus === null) return; - clearInterval(this._autoSaveStatus); - this._autoSaveStatus = null; - } -} diff --git a/src/modules/project/save/save.type.ts b/src/modules/project/save/save.type.ts deleted file mode 100644 index ff1937d..0000000 --- a/src/modules/project/save/save.type.ts +++ /dev/null @@ -1 +0,0 @@ -export interface ISave {} diff --git a/src/routes/loadProject/+page.server.ts b/src/routes/load-project/+page.server.ts similarity index 85% rename from src/routes/loadProject/+page.server.ts rename to src/routes/load-project/+page.server.ts index 4e109bb..f2607a1 100644 --- a/src/routes/loadProject/+page.server.ts +++ b/src/routes/load-project/+page.server.ts @@ -1,6 +1,6 @@ import { env } from '$env/dynamic/private'; import { redirect } from '@sveltejs/kit'; -import { authGuard } from '@utils/server-api'; +import { authGuard } from '@utils-server/server-api'; import * as fs from 'node:fs'; import * as path from 'node:path'; @@ -19,14 +19,9 @@ export const load: PageServerLoad = async ({ url, cookies, locals }) => { if (!env.API_URL) { throw new Error('Missing API_URL'); } - const accessToken = cookies.get('accessToken'); - - if (accessToken === undefined || accessToken.length === 0) { - return { success: false, errorMsg: 'Unauthorized access' }; - } const serverProjectPath = await authGuard(async (httpClient) => { - return await httpClient.post(`/editor/projects/${projectId}`); + return await httpClient.post(`${env.API_URL}/editor/projects/${projectId}`); }, cookies); if (serverProjectPath.status !== 200) { return { success: false, errorMsg: 'Cannot retrieve project from API' }; diff --git a/src/routes/loadProject/+page.svelte b/src/routes/load-project/+page.svelte similarity index 100% rename from src/routes/loadProject/+page.svelte rename to src/routes/load-project/+page.svelte diff --git a/src/demo.spec.ts b/src/routes/load-project/load-project.spec.ts similarity index 94% rename from src/demo.spec.ts rename to src/routes/load-project/load-project.spec.ts index f80e516..a5456ec 100644 --- a/src/demo.spec.ts +++ b/src/routes/load-project/load-project.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; -import { load } from './routes/loadProject/+page.server'; +import { load } from './+page.server'; describe('load', () => { it('local project load and session cookie set', async () => { diff --git a/svelte.config.js b/svelte.config.js index 0c50cd8..045e4bd 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -10,6 +10,10 @@ export default { }, kit: { adapter: adapter(), - alias: { '@utils/*': './src/utils/*' }, + alias: { + '@utils/*': './src/lib/utils/*', + '@utils-client/*': './src/lib/utils-client/*', + '@utils-server/*': './src/lib/server/utils/*', + }, }, }; From 9179454a22892d2fde3a1661a86f6fdf9c4e04f6 Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Tue, 10 Mar 2026 15:38:55 +0900 Subject: [PATCH 5/8] refactor(back): generic fs --- .../utils/file-system/fileSystemError.ts | 7 + .../utils/file-system/projectDirectory.ts | 125 +++++++++ .../server/utils/file-system/projectFile.ts | 128 +++++++++ src/routes/fs/+page.server.ts | 249 ++++++------------ 4 files changed, 339 insertions(+), 170 deletions(-) create mode 100644 src/lib/server/utils/file-system/fileSystemError.ts create mode 100644 src/lib/server/utils/file-system/projectDirectory.ts create mode 100644 src/lib/server/utils/file-system/projectFile.ts diff --git a/src/lib/server/utils/file-system/fileSystemError.ts b/src/lib/server/utils/file-system/fileSystemError.ts new file mode 100644 index 0000000..00de3d5 --- /dev/null +++ b/src/lib/server/utils/file-system/fileSystemError.ts @@ -0,0 +1,7 @@ +export class FileSystemError extends Error { + message: string; + constructor(message: string) { + super(); + this.message = message; + } +} diff --git a/src/lib/server/utils/file-system/projectDirectory.ts b/src/lib/server/utils/file-system/projectDirectory.ts new file mode 100644 index 0000000..329ae57 --- /dev/null +++ b/src/lib/server/utils/file-system/projectDirectory.ts @@ -0,0 +1,125 @@ +import { FileSystemError } from '@utils-server/file-system/fileSystemError'; +import fs from 'node:fs'; +import path from 'node:path'; + +export class ProjectDirectory { + private path: string; + private readonly projectPath: string; + + constructor(dirPath: string, projectPath: string) { + this.path = path.resolve(projectPath, './' + dirPath); + this.projectPath = projectPath; + } + + read(recursive: boolean = false): { files: string[]; directories: {} } { + this._checkPathIsInsideProject(); + this._checkPathExists(); + this._checkPathIsDir(); + this._checkPathIsReadable(); + return this._readDirContent(this.path, recursive); + } + + create(): void { + this._checkPathIsInsideProject(); + this._checkPathNotExists(); + + fs.mkdirSync(this.path, { recursive: true }); + } + + delete(recursive: boolean = false): void { + this._checkPathIsInsideProject(); + this._checkPathExists(); + this._checkPathIsDir(); + if (!recursive) { + this._checkDirIsEmpty(); + } + fs.rmSync(this.path, { recursive: recursive }); + } + + rename(newPath: string): void { + const absoluteNewDirPath = path.resolve(this.projectPath, './' + newPath); + this._checkPathIsInsideProject(); + this._checkPathIsInsideProject(absoluteNewDirPath); + this._checkPathExists(); + this._checkPathIsDir(); + this._checkPathIsWritable(); + const newFolderPath = path.dirname(absoluteNewDirPath); + this._checkPathExists(newFolderPath); + this._checkPathIsWritable(newFolderPath); + this._checkPathNotExists(absoluteNewDirPath); + fs.renameSync(this.path, absoluteNewDirPath); + this.path = absoluteNewDirPath; + } + + private _checkPathIsInsideProject(path: string = this.path) { + if (!path.startsWith(this.projectPath)) { + throw new FileSystemError(`Path ${path} is outside of the project directory`); + } + } + + private _checkPathExists(path: string = this.path) { + if (!fs.existsSync(path)) { + throw new FileSystemError(`Path ${path} should exist`); + } + } + + private _checkPathNotExists(path: string = this.path) { + if (fs.existsSync(path)) { + throw new FileSystemError(`Path ${path} should not exist`); + } + } + + private _checkPathIsDir(path: string = this.path) { + let stats: fs.Stats; + try { + stats = fs.lstatSync(path); + } catch { + throw new FileSystemError(`Path ${path} does not exist`); + } + if (!stats.isDirectory()) { + throw new FileSystemError(`Path ${path} is not a directory`); + } + } + + private _checkPathIsWritable(path: string = this.path) { + try { + fs.accessSync(path, fs.constants.W_OK); + } catch { + throw new FileSystemError(`Path ${path} writable`); + } + } + + private _checkPathIsReadable(path: string = this.path) { + try { + fs.accessSync(path, fs.constants.R_OK); + } catch { + throw new FileSystemError(`Path ${path} writable`); + } + } + + private _checkDirIsEmpty(path: string = this.path) { + if (fs.readdirSync(path).length > 0) { + throw new FileSystemError(`Directory ${path} is not empty`); + } + } + + private _readDirContent( + path: string = this.path, + recursive: boolean = false, + ): { files: string[]; directories: {} } { + const dirContent: { files: string[]; directories: { [key: string]: any } } = { + files: [], + directories: {}, + }; + fs.readdirSync(path, { withFileTypes: true, recursive: false }).forEach((item) => { + if (item.isFile()) { + dirContent.files.push(item.name); + } else if (item.isDirectory()) { + dirContent.directories[item.name] = recursive + ? this._readDirContent(path + '/' + item.name, recursive) + : {}; + } + }); + return dirContent; + } +} diff --git a/src/lib/server/utils/file-system/projectFile.ts b/src/lib/server/utils/file-system/projectFile.ts new file mode 100644 index 0000000..2a3fbf4 --- /dev/null +++ b/src/lib/server/utils/file-system/projectFile.ts @@ -0,0 +1,128 @@ +import { FileSystemError } from '@utils-server/file-system/fileSystemError'; +import fs from 'node:fs'; +import path from 'node:path'; + +export class ProjectFile { + private path: string; + private readonly projectPath: string; + + constructor(filePath: string, projectPath: string) { + this.path = path.resolve(projectPath, './' + filePath); + this.projectPath = projectPath; + } + + read(): string { + this._checkPathIsInsideProject(); + this._checkPathExists(); + this._checkPathIsFile(); + this._checkPathIsReadable(); + return fs.readFileSync(this.path).toString(); + } + + readJson(): T { + const raw = this.read(); + return JSON.parse(raw) as T; + } + + write(text: string): void { + this._checkPathIsInsideProject(); + try { + this._checkPathExists(); + } catch { + this._checkPathIsFile(); + this._checkPathIsWritable(); + fs.writeFileSync(this.path, text, { flush: true }); + return; + } + const folderPath = path.dirname(this.path); + this._checkPathExists(folderPath); + this._checkPathIsDir(folderPath); + this._checkPathIsWritable(folderPath); + fs.writeFileSync(this.path, text, { flush: true }); + } + + writeJson(content: any): void { + const raw = JSON.stringify(content); + this.write(raw); + } + + delete(): void { + this._checkPathIsInsideProject(); + this._checkPathExists(); + this._checkPathIsFile(); + this._checkPathIsWritable(); + fs.rmSync(this.path); + } + + rename(newPath: string): void { + const absoluteNewPath = path.resolve(this.projectPath, './' + newPath); + this._checkPathIsInsideProject(newPath); + this._checkPathExists(); + this._checkPathIsFile(); + this._checkPathIsWritable(); + const newFolderPath = path.dirname(absoluteNewPath); + this._checkPathExists(newFolderPath); + this._checkPathIsWritable(newFolderPath); + this._checkPathNotExists(absoluteNewPath); + fs.renameSync(this.path, absoluteNewPath); + this.path = absoluteNewPath; + } + + private _checkPathIsInsideProject(path: string = this.path) { + if (!path.startsWith(this.projectPath)) { + throw new FileSystemError(`Path ${path} is outside of the project directory`); + } + } + + private _checkPathExists(path: string = this.path) { + if (!fs.existsSync(path)) { + throw new FileSystemError(`Path ${path} should exist`); + } + } + + private _checkPathNotExists(path: string = this.path) { + if (fs.existsSync(path)) { + throw new FileSystemError(`Path ${path} should not exist`); + } + } + + private _checkPathIsFile(path: string = this.path) { + let stats: fs.Stats; + try { + stats = fs.lstatSync(path); + } catch { + throw new FileSystemError(`Path ${path} does not exist`); + } + if (!stats.isFile()) { + throw new FileSystemError(`Path ${path} is not a this.path`); + } + } + + private _checkPathIsDir(path: string = this.path) { + let stats: fs.Stats; + try { + stats = fs.lstatSync(path); + } catch { + throw new FileSystemError(`Path ${path} does not exist`); + } + if (!stats.isDirectory()) { + throw new FileSystemError(`Path ${path} is not a directory`); + } + } + + private _checkPathIsWritable(path: string = this.path) { + try { + fs.accessSync(path, fs.constants.W_OK); + } catch { + throw new FileSystemError(`Path ${path} writable`); + } + } + + private _checkPathIsReadable(path: string = this.path) { + try { + fs.accessSync(path, fs.constants.R_OK); + } catch { + throw new FileSystemError(`Path ${path} writable`); + } + } +} diff --git a/src/routes/fs/+page.server.ts b/src/routes/fs/+page.server.ts index 0068972..f587ebe 100644 --- a/src/routes/fs/+page.server.ts +++ b/src/routes/fs/+page.server.ts @@ -1,87 +1,10 @@ import { fail } from '@sveltejs/kit'; -import * as fs from 'node:fs'; -import * as path from 'node:path'; +import { FileSystemError } from '@utils-server/file-system/fileSystemError'; +import { ProjectDirectory } from '@utils-server/file-system/projectDirectory'; +import { ProjectFile } from '@utils-server/file-system/projectFile'; import type { Actions } from './$types'; -function checkPathIsInsideDir(filePath: string, projectPath: string) { - if (!filePath.startsWith(projectPath)) { - throw `Path ${filePath} outside of directory`; - } -} - -function checkPathExists(filePath: string) { - if (!fs.existsSync(filePath)) { - throw `Path ${filePath} should exist`; - } -} - -function checkPathNotExists(filePath: string) { - if (fs.existsSync(filePath)) { - throw `Path ${filePath} should not exist`; - } -} - -function checkPathIsFile(filePath: string) { - let stats: fs.Stats; - try { - stats = fs.lstatSync(filePath); - } catch { - throw `Path ${filePath} does not exist`; - } - if (!stats.isFile()) { - throw `Path ${filePath} is not a file`; - } -} - -function checkPathIsDir(filePath: string) { - let stats: fs.Stats; - try { - stats = fs.lstatSync(filePath); - } catch { - throw `Path ${filePath} does not exist`; - } - if (!stats.isDirectory()) { - throw `Path ${filePath} is not a directory`; - } -} - -function checkPathIsWritable(filePath: string) { - try { - fs.accessSync(filePath, fs.constants.W_OK); - } catch { - throw `Path ${filePath} writable`; - } -} - -function checkPathIsReadable(filePath: string) { - try { - fs.accessSync(filePath, fs.constants.R_OK); - } catch { - throw `Path ${filePath} writable`; - } -} - -function readDirContent( - absoluteDirPath: string, - recursive: boolean, -): { files: string[]; directories: {} } { - const dirContent: { files: string[]; directories: { [key: string]: any } } = { - files: [], - directories: {}, - }; - fs.readdirSync(absoluteDirPath, { withFileTypes: true, recursive: false }).forEach((item) => { - if (item.isFile()) { - dirContent.files.push(item.name); - } else if (item.isDirectory()) { - dirContent.directories[item.name] = recursive - ? readDirContent(absoluteDirPath + '/' + item.name, recursive) - : {}; - } - }); - return dirContent; -} - export const actions = { readFile: async ({ request, locals }) => { const data = await request.json(); @@ -91,59 +14,53 @@ export const actions = { } try { - const absoluteFilePath = path.resolve(locals.session.data.path, './' + data.filePath); - checkPathIsInsideDir(absoluteFilePath, locals.session.data.path); - checkPathExists(absoluteFilePath); - checkPathIsFile(absoluteFilePath); - checkPathIsReadable(absoluteFilePath); - return { success: true, fileContent: fs.readFileSync(absoluteFilePath).toString() }; - } catch (e) { - return fail(403, { success: false, errorMsg: e }); + return { + success: true, + fileContent: new ProjectFile(data.filePath, locals.session.data.path).read(), + }; + } catch (e: unknown) { + if (e instanceof FileSystemError) { + return fail(403, { success: false, errorMsg: e.message }); + } + throw e; } }, readDir: async ({ request, locals }) => { const data = await request.json(); - let dirPath = '/'; - if (data.dirPath) { - dirPath = data.dirPath; - } - try { - const absoluteDirPath = path.resolve(locals.session.data.path, './' + dirPath); - checkPathIsInsideDir(absoluteDirPath, locals.session.data.path); - checkPathExists(absoluteDirPath); - checkPathIsDir(absoluteDirPath); - const dirContent = readDirContent(absoluteDirPath, false); return { success: true, - dirContent, + dirContent: new ProjectDirectory( + data.dirPath ? data.dirPath : '/', + locals.session.data.path, + ).read(), }; - } catch (e) { - return fail(403, { success: false, errorMsg: e }); + } catch (e: unknown) { + if (e instanceof FileSystemError) { + return fail(403, { success: false, errorMsg: e.message }); + } + throw e; } }, - readDirRec: async ({ request, locals }) => { const data = await request.json(); - let dirPath = '/'; - if (data.dirPath) { - dirPath = data.dirPath; - } - try { - const absoluteDirPath = path.resolve(locals.session.data.path, './' + dirPath); - checkPathIsInsideDir(absoluteDirPath, locals.session.data.path); - checkPathExists(absoluteDirPath); - checkPathIsDir(absoluteDirPath); - const dirContent = readDirContent(absoluteDirPath, true); - return { success: true, dirContent }; - } catch (e) { - return fail(403, { success: false, errorMsg: e }); + return { + success: true, + dirContent: new ProjectDirectory( + data.dirPath ? data.dirPath : '/', + locals.session.data.path, + ).read(true), + }; + } catch (e: unknown) { + if (e instanceof FileSystemError) { + return fail(403, { success: false, errorMsg: e.message }); + } + throw e; } }, - writeFile: async ({ request, locals }) => { const data = await request.json(); @@ -155,18 +72,12 @@ export const actions = { } try { - const absoluteFilePath = path.resolve(locals.session.data.path, './' + data.filePath); - checkPathIsInsideDir(absoluteFilePath, locals.session.data.path); - if (fs.existsSync(absoluteFilePath)) { - checkPathIsFile(absoluteFilePath); - checkPathIsWritable(absoluteFilePath); - } else { - const folderPath = path.dirname(absoluteFilePath); - checkPathIsWritable(folderPath); + new ProjectFile(data.filePath, locals.session.data.path).write(data.fileContent); + } catch (e: unknown) { + if (e instanceof FileSystemError) { + return fail(403, { success: false, errorMsg: e.message }); } - fs.writeFileSync(absoluteFilePath, data.fileContent, { flush: true }); - } catch (e) { - return fail(403, { success: false, errorMsg: e }); + throw e; } return { success: true }; }, @@ -179,14 +90,12 @@ export const actions = { } try { - const absoluteFilePath = path.resolve(locals.session.data.path, './' + data.filePath); - checkPathIsInsideDir(absoluteFilePath, locals.session.data.path); - checkPathExists(absoluteFilePath); - checkPathIsFile(absoluteFilePath); - checkPathIsWritable(absoluteFilePath); - fs.rmSync(absoluteFilePath); - } catch (e) { - return fail(403, { success: false, errorMsg: e }); + new ProjectFile(data.filePath, locals.session.data.path).delete(); + } catch (e: unknown) { + if (e instanceof FileSystemError) { + return fail(403, { success: false, errorMsg: e.message }); + } + throw e; } return { success: true }; }, @@ -202,20 +111,12 @@ export const actions = { } try { - const absoluteFilePath = path.resolve(locals.session.data.path, './' + data.filePath); - const absoluteNewFilePath = path.resolve(locals.session.data.path, './' + data.newFilePath); - checkPathIsInsideDir(absoluteFilePath, locals.session.data.path); - checkPathIsInsideDir(absoluteNewFilePath, locals.session.data.path); - checkPathExists(absoluteFilePath); - checkPathIsFile(absoluteFilePath); - checkPathIsWritable(absoluteFilePath); - const newFolderPath = path.dirname(absoluteNewFilePath); - checkPathExists(newFolderPath); - checkPathIsWritable(newFolderPath); - checkPathNotExists(absoluteNewFilePath); - fs.renameSync(absoluteFilePath, absoluteNewFilePath); - } catch (e) { - return fail(403, { success: false, errorMsg: e }); + new ProjectFile(data.filePath, locals.session.data.path).rename(data.newFilePath); + } catch (e: unknown) { + if (e instanceof FileSystemError) { + return fail(403, { success: false, errorMsg: e.message }); + } + throw e; } return { success: true }; }, @@ -228,13 +129,12 @@ export const actions = { } try { - const absoluteFilePath = path.resolve(locals.session.data.path, './' + data.dirPath); - checkPathIsInsideDir(absoluteFilePath, locals.session.data.path); - checkPathNotExists(absoluteFilePath); - - fs.mkdirSync(absoluteFilePath, { recursive: true }); - } catch (e) { - return fail(403, { success: false, errorMsg: e }); + new ProjectDirectory(data.dirPath, locals.session.data.path).create(); + } catch (e: unknown) { + if (e instanceof FileSystemError) { + return fail(403, { success: false, errorMsg: e.message }); + } + throw e; } return { success: true }; }, @@ -250,20 +150,29 @@ export const actions = { } try { - const absoluteDirPath = path.resolve(locals.session.data.path, './' + data.dirPath); - const absoluteNewDirPath = path.resolve(locals.session.data.path, './' + data.newDirPath); - checkPathIsInsideDir(absoluteDirPath, locals.session.data.path); - checkPathIsInsideDir(absoluteNewDirPath, locals.session.data.path); - checkPathExists(absoluteDirPath); - checkPathIsDir(absoluteDirPath); - checkPathIsWritable(absoluteDirPath); - const newFolderPath = path.dirname(absoluteNewDirPath); - checkPathExists(newFolderPath); - checkPathIsWritable(newFolderPath); - checkPathNotExists(absoluteNewDirPath); - fs.renameSync(absoluteDirPath, absoluteNewDirPath); - } catch (e) { - return fail(403, { success: false, errorMsg: e }); + new ProjectDirectory(data.dirPath, locals.session.data.path).rename(data.newDirPath); + } catch (e: unknown) { + if (e instanceof FileSystemError) { + return fail(403, { success: false, errorMsg: e.message }); + } + throw e; + } + return { success: true }; + }, + deleteDir: async ({ request, locals }) => { + const data = await request.json(); + + if (!data.dirPath) { + return fail(403, { success: false, errorMsg: "Missing arg: 'dirPath'" }); + } + + try { + new ProjectDirectory(data.dirPath, locals.session.data.path).delete(data.recursive === true); + } catch (e: unknown) { + if (e instanceof FileSystemError) { + return fail(403, { success: false, errorMsg: e.message }); + } + throw e; } return { success: true }; }, From 3ed13897782b1e7d638f5ee580384c8f367f7e23 Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Tue, 10 Mar 2026 16:43:26 +0900 Subject: [PATCH 6/8] fix(back): fs naming convention --- .../{fileSystemError.ts => file-system-error.ts} | 0 .../{projectDirectory.ts => project-directory.ts} | 0 .../utils/file-system/{projectFile.ts => project-file.ts} | 0 src/routes/fs/+page.server.ts | 6 +++--- 4 files changed, 3 insertions(+), 3 deletions(-) rename src/lib/server/utils/file-system/{fileSystemError.ts => file-system-error.ts} (100%) rename src/lib/server/utils/file-system/{projectDirectory.ts => project-directory.ts} (100%) rename src/lib/server/utils/file-system/{projectFile.ts => project-file.ts} (100%) diff --git a/src/lib/server/utils/file-system/fileSystemError.ts b/src/lib/server/utils/file-system/file-system-error.ts similarity index 100% rename from src/lib/server/utils/file-system/fileSystemError.ts rename to src/lib/server/utils/file-system/file-system-error.ts diff --git a/src/lib/server/utils/file-system/projectDirectory.ts b/src/lib/server/utils/file-system/project-directory.ts similarity index 100% rename from src/lib/server/utils/file-system/projectDirectory.ts rename to src/lib/server/utils/file-system/project-directory.ts diff --git a/src/lib/server/utils/file-system/projectFile.ts b/src/lib/server/utils/file-system/project-file.ts similarity index 100% rename from src/lib/server/utils/file-system/projectFile.ts rename to src/lib/server/utils/file-system/project-file.ts diff --git a/src/routes/fs/+page.server.ts b/src/routes/fs/+page.server.ts index f587ebe..f425c50 100644 --- a/src/routes/fs/+page.server.ts +++ b/src/routes/fs/+page.server.ts @@ -1,7 +1,7 @@ import { fail } from '@sveltejs/kit'; -import { FileSystemError } from '@utils-server/file-system/fileSystemError'; -import { ProjectDirectory } from '@utils-server/file-system/projectDirectory'; -import { ProjectFile } from '@utils-server/file-system/projectFile'; +import { FileSystemError } from '@utils-server/file-system/file-system-error'; +import { ProjectDirectory } from '@utils-server/file-system/project-directory'; +import { ProjectFile } from '@utils-server/file-system/project-file'; import type { Actions } from './$types'; From 43491591f2983e0ba99f2c46c62fd1b73649f0e2 Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Tue, 10 Mar 2026 16:46:41 +0900 Subject: [PATCH 7/8] fix(back): fs naming convention --- src/lib/server/utils/file-system/project-directory.ts | 2 +- src/lib/server/utils/file-system/project-file.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/server/utils/file-system/project-directory.ts b/src/lib/server/utils/file-system/project-directory.ts index 329ae57..92645d5 100644 --- a/src/lib/server/utils/file-system/project-directory.ts +++ b/src/lib/server/utils/file-system/project-directory.ts @@ -1,4 +1,4 @@ -import { FileSystemError } from '@utils-server/file-system/fileSystemError'; +import { FileSystemError } from '@utils-server/file-system/file-system-error'; import fs from 'node:fs'; import path from 'node:path'; diff --git a/src/lib/server/utils/file-system/project-file.ts b/src/lib/server/utils/file-system/project-file.ts index 2a3fbf4..1535f71 100644 --- a/src/lib/server/utils/file-system/project-file.ts +++ b/src/lib/server/utils/file-system/project-file.ts @@ -1,4 +1,4 @@ -import { FileSystemError } from '@utils-server/file-system/fileSystemError'; +import { FileSystemError } from '@utils-server/file-system/file-system-error'; import fs from 'node:fs'; import path from 'node:path'; From ce404634d3b5c649a7c8c1137732ad255a5c5b49 Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Tue, 10 Mar 2026 23:03:24 +0900 Subject: [PATCH 8/8] fix: pnpm lock --- pnpm-lock.yaml | 240 ++++++++++++++++++++++++++----------------------- 1 file changed, 128 insertions(+), 112 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab55f10..b42ca1b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,10 +15,10 @@ catalogs: ci: '@commitlint/cli': specifier: ^20.4.2 - version: 20.4.2 + version: 20.4.3 '@commitlint/config-conventional': specifier: ^20.4.2 - version: 20.4.2 + version: 20.4.3 '@favware/cliff-jumper': specifier: ^6.0.0 version: 6.0.0 @@ -72,7 +72,7 @@ catalogs: version: 0.0.4 '@unocss/extractor-svelte': specifier: ^66.6.3 - version: 66.6.3 + version: 66.6.6 '@unocss/preset-icons': specifier: ^66.6.0 version: 66.6.0 @@ -95,7 +95,7 @@ catalogs: version: 1.2.4 '@iconify-json/material-icon-theme': specifier: ^1.2.51 - version: 1.2.51 + version: 1.2.55 '@iconify-json/solar': specifier: ^1.2.5 version: 1.2.5 @@ -123,7 +123,7 @@ catalogs: version: 3.8.1 prettier-plugin-svelte: specifier: ^3.5.0 - version: 3.5.0 + version: 3.5.1 typescript-eslint: specifier: ^8.53.1 version: 8.53.1 @@ -153,17 +153,17 @@ importers: dependencies: svelte-kit-sessions: specifier: catalog:core - version: 0.4.0(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)))(svelte@5.48.0) + version: 0.4.0(@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)))(svelte@5.48.0) devDependencies: '@alexanderniebuhr/prettier-plugin-unocss': specifier: catalog:css version: 0.0.4 '@commitlint/cli': specifier: catalog:ci - version: 20.4.2(@types/node@25.0.10)(typescript@5.9.3) + version: 20.4.3(@types/node@25.0.10)(typescript@5.9.3) '@commitlint/config-conventional': specifier: catalog:ci - version: 20.4.2 + version: 20.4.3 '@favware/cliff-jumper': specifier: catalog:ci version: 6.0.0 @@ -172,7 +172,7 @@ importers: version: 1.2.4 '@iconify-json/material-icon-theme': specifier: catalog:icons - version: 1.2.51 + version: 1.2.55 '@iconify-json/solar': specifier: catalog:icons version: 1.2.5 @@ -202,13 +202,13 @@ importers: version: 6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) '@trivago/prettier-plugin-sort-imports': specifier: catalog:lint - version: 6.0.2(prettier-plugin-svelte@3.5.0(prettier@3.8.1)(svelte@5.48.0))(prettier@3.8.1)(svelte@5.48.0) + version: 6.0.2(prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.48.0))(prettier@3.8.1)(svelte@5.48.0) '@tsconfig/svelte': specifier: catalog:build version: 5.0.8 '@unocss/extractor-svelte': specifier: catalog:css - version: 66.6.3 + version: 66.6.6 '@unocss/preset-icons': specifier: catalog:css version: 66.6.0 @@ -256,7 +256,7 @@ importers: version: 3.8.1 prettier-plugin-svelte: specifier: catalog:lint - version: 3.5.0(prettier@3.8.1)(svelte@5.48.0) + version: 3.5.1(prettier@3.8.1)(svelte@5.48.0) svelte: specifier: catalog:core version: 5.48.0 @@ -362,73 +362,73 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@commitlint/cli@20.4.2': - resolution: {integrity: sha512-YjYSX2yj/WsVoxh9mNiymfFS2ADbg2EK4+1WAsMuckwKMCqJ5PDG0CJU/8GvmHWcv4VRB2V02KqSiecRksWqZQ==} + '@commitlint/cli@20.4.3': + resolution: {integrity: sha512-Z37EMoDT7+Upg500vlr/vZrgRsb6Xc5JAA3Tv7BYbobnN/ZpqUeZnSLggBg2+1O+NptRDtyujr2DD1CPV2qwhA==} engines: {node: '>=v18'} hasBin: true - '@commitlint/config-conventional@20.4.2': - resolution: {integrity: sha512-rwkTF55q7Q+6dpSKUmJoScV0f3EpDlWKw2UPzklkLS4o5krMN1tPWAVOgHRtyUTMneIapLeQwaCjn44Td6OzBQ==} + '@commitlint/config-conventional@20.4.3': + resolution: {integrity: sha512-9RtLySbYQAs8yEqWEqhSZo9nYhbm57jx7qHXtgRmv/nmeQIjjMcwf6Dl+y5UZcGWgWx435TAYBURONaJIuCjWg==} engines: {node: '>=v18'} - '@commitlint/config-validator@20.4.0': - resolution: {integrity: sha512-zShmKTF+sqyNOfAE0vKcqnpvVpG0YX8F9G/ZIQHI2CoKyK+PSdladXMSns400aZ5/QZs+0fN75B//3Q5CHw++w==} + '@commitlint/config-validator@20.4.3': + resolution: {integrity: sha512-jCZpZFkcSL3ZEdL5zgUzFRdytv3xPo8iukTe9VA+QGus/BGhpp1xXSVu2B006GLLb2gYUAEGEqv64kTlpZNgmA==} engines: {node: '>=v18'} - '@commitlint/ensure@20.4.1': - resolution: {integrity: sha512-WLQqaFx1pBooiVvBrA1YfJNFqZF8wS/YGOtr5RzApDbV9tQ52qT5VkTsY65hFTnXhW8PcDfZLaknfJTmPejmlw==} + '@commitlint/ensure@20.4.3': + resolution: {integrity: sha512-WcXGKBNn0wBKpX8VlXgxqedyrLxedIlLBCMvdamLnJFEbUGJ9JZmBVx4vhLV3ZyA8uONGOb+CzW0Y9HDbQ+ONQ==} engines: {node: '>=v18'} '@commitlint/execute-rule@20.0.0': resolution: {integrity: sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==} engines: {node: '>=v18'} - '@commitlint/format@20.4.0': - resolution: {integrity: sha512-i3ki3WR0rgolFVX6r64poBHXM1t8qlFel1G1eCBvVgntE3fCJitmzSvH5JD/KVJN/snz6TfaX2CLdON7+s4WVQ==} + '@commitlint/format@20.4.3': + resolution: {integrity: sha512-UDJVErjLbNghop6j111rsHJYGw6MjCKAi95K0GT2yf4eeiDHy3JDRLWYWEjIaFgO+r+dQSkuqgJ1CdMTtrvHsA==} engines: {node: '>=v18'} - '@commitlint/is-ignored@20.4.1': - resolution: {integrity: sha512-In5EO4JR1lNsAv1oOBBO24V9ND1IqdAJDKZiEpdfjDl2HMasAcT7oA+5BKONv1pRoLG380DGPE2W2RIcUwdgLA==} + '@commitlint/is-ignored@20.4.3': + resolution: {integrity: sha512-W5VQKZ7fdJ1X3Tko+h87YZaqRMGN1KvQKXyCM8xFdxzMIf1KCZgN4uLz3osLB1zsFcVS4ZswHY64LI26/9ACag==} engines: {node: '>=v18'} - '@commitlint/lint@20.4.2': - resolution: {integrity: sha512-buquzNRtFng6xjXvBU1abY/WPEEjCgUipNQrNmIWe8QuJ6LWLtei/LDBAzEe5ASm45+Q9L2Xi3/GVvlj50GAug==} + '@commitlint/lint@20.4.3': + resolution: {integrity: sha512-CYOXL23e+nRKij81+d0+dymtIi7Owl9QzvblJYbEfInON/4MaETNSLFDI74LDu+YJ0ML5HZyw9Vhp9QpckwQ0A==} engines: {node: '>=v18'} - '@commitlint/load@20.4.0': - resolution: {integrity: sha512-Dauup/GfjwffBXRJUdlX/YRKfSVXsXZLnINXKz0VZkXdKDcaEILAi9oflHGbfydonJnJAbXEbF3nXPm9rm3G6A==} + '@commitlint/load@20.4.3': + resolution: {integrity: sha512-3cdJOUVP+VcgHa7bhJoWS+Z8mBNXB5aLWMBu7Q7uX8PSeWDzdbrBlR33J1MGGf7r1PZDp+mPPiFktk031PgdRw==} engines: {node: '>=v18'} - '@commitlint/message@20.4.0': - resolution: {integrity: sha512-B5lGtvHgiLAIsK5nLINzVW0bN5hXv+EW35sKhYHE8F7V9Uz1fR4tx3wt7mobA5UNhZKUNgB/+ldVMQE6IHZRyA==} + '@commitlint/message@20.4.3': + resolution: {integrity: sha512-6akwCYrzcrFcTYz9GyUaWlhisY4lmQ3KvrnabmhoeAV8nRH4dXJAh4+EUQ3uArtxxKQkvxJS78hNX2EU3USgxQ==} engines: {node: '>=v18'} - '@commitlint/parse@20.4.1': - resolution: {integrity: sha512-XNtZjeRcFuAfUnhYrCY02+mpxwY4OmnvD3ETbVPs25xJFFz1nRo/25nHj+5eM+zTeRFvWFwD4GXWU2JEtoK1/w==} + '@commitlint/parse@20.4.3': + resolution: {integrity: sha512-hzC3JCo3zs3VkQ833KnGVuWjWIzR72BWZWjQM7tY/7dfKreKAm7fEsy71tIFCRtxf2RtMP2d3RLF1U9yhFSccA==} engines: {node: '>=v18'} - '@commitlint/read@20.4.0': - resolution: {integrity: sha512-QfpFn6/I240ySEGv7YWqho4vxqtPpx40FS7kZZDjUJ+eHxu3azfhy7fFb5XzfTqVNp1hNoI3tEmiEPbDB44+cg==} + '@commitlint/read@20.4.3': + resolution: {integrity: sha512-j42OWv3L31WfnP8WquVjHZRt03w50Y/gEE8FAyih7GQTrIv2+pZ6VZ6pWLD/ml/3PO+RV2SPtRtTp/MvlTb8rQ==} engines: {node: '>=v18'} - '@commitlint/resolve-extends@20.4.0': - resolution: {integrity: sha512-ay1KM8q0t+/OnlpqXJ+7gEFQNlUtSU5Gxr8GEwnVf2TPN3+ywc5DzL3JCxmpucqxfHBTFwfRMXxPRRnR5Ki20g==} + '@commitlint/resolve-extends@20.4.3': + resolution: {integrity: sha512-QucxcOy+00FhS9s4Uy0OyS5HeUV+hbC6OLqkTSIm6fwMdKva+OEavaCDuLtgd9akZZlsUo//XzSmPP3sLKBPog==} engines: {node: '>=v18'} - '@commitlint/rules@20.4.2': - resolution: {integrity: sha512-oz83pnp5Yq6uwwTAabuVQPNlPfeD2Y5ZjMb7Wx8FSUlu4sLYJjbBWt8031Z0osCFPfHzAwSYrjnfDFKtuSMdKg==} + '@commitlint/rules@20.4.3': + resolution: {integrity: sha512-Yuosd7Grn5qiT7FovngXLyRXTMUbj9PYiSkvUgWK1B5a7+ZvrbWDS7epeUapYNYatCy/KTpPFPbgLUdE+MUrBg==} engines: {node: '>=v18'} '@commitlint/to-lines@20.0.0': resolution: {integrity: sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==} engines: {node: '>=v18'} - '@commitlint/top-level@20.4.0': - resolution: {integrity: sha512-NDzq8Q6jmFaIIBC/GG6n1OQEaHdmaAAYdrZRlMgW6glYWGZ+IeuXmiymDvQNXPc82mVxq2KiE3RVpcs+1OeDeA==} + '@commitlint/top-level@20.4.3': + resolution: {integrity: sha512-qD9xfP6dFg5jQ3NMrOhG0/w5y3bBUsVGyJvXxdWEwBm8hyx4WOk3kKXw28T5czBYvyeCVJgJJ6aoJZUWDpaacQ==} engines: {node: '>=v18'} - '@commitlint/types@20.4.0': - resolution: {integrity: sha512-aO5l99BQJ0X34ft8b0h7QFkQlqxC6e7ZPVmBKz13xM9O8obDaM1Cld4sQlJDXXU/VFuUzQ30mVtHjVz74TuStw==} + '@commitlint/types@20.4.3': + resolution: {integrity: sha512-51OWa1Gi6ODOasPmfJPq6js4pZoomima4XLZZCrkldaH2V5Nb3bVhNXPeT6XV0gubbainSpTw4zi68NqAeCNCg==} engines: {node: '>=v18'} '@conventional-changelog/git-client@1.0.1': @@ -687,8 +687,8 @@ packages: '@iconify-json/ic@1.2.4': resolution: {integrity: sha512-pzPMmrZrBQuwT7nmtrYdkttun8KalRGgZPIL1Ny9KpF2zjRGIUPN+npTfuD3lrgO/OnSwAoJWuekQwBpt/Cqrw==} - '@iconify-json/material-icon-theme@1.2.51': - resolution: {integrity: sha512-gZ/EEe2K+sP5f7lfd8TNiRSvHEbFBN4gzBC2fZonZuDjoB008s5Ni8Qvlz++xMBZsEg7gEKG8Ph5r6lFZpQ8AQ==} + '@iconify-json/material-icon-theme@1.2.55': + resolution: {integrity: sha512-V4FUXp2az00xpGYjj4MaOvp6aAIfOMTRRGrt66KH7DmqoIb4WV/YqH4TalgOswCJD/UPGdPuOoy+B6hxLuifTg==} '@iconify-json/solar@1.2.5': resolution: {integrity: sha512-WMAiNwchU8zhfrySww6KQBRIBbsQ6SvgIu2yA+CHGyMima/0KQwT5MXogrZPJGoQF+1Ye3Qj6K+1CiyNn3YkoA==} @@ -1005,6 +1005,10 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@simple-libs/stream-utils@1.2.0': + resolution: {integrity: sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==} + engines: {node: '>=18'} + '@sinclair/typebox@0.31.28': resolution: {integrity: sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==} @@ -1316,8 +1320,8 @@ packages: '@unocss/extractor-arbitrary-variants@66.6.0': resolution: {integrity: sha512-AsCmpbre4hQb+cKOf3gHUeYlF7guR/aCKZvw53VBk12qY5wNF7LdfIx4zWc5LFVCoRxIZlU2C7L4/Tt7AkiFMA==} - '@unocss/extractor-svelte@66.6.3': - resolution: {integrity: sha512-eFr6IVBH3xO7ztwmFrBFkVTUezfqX5PYiMSi+keflSs0PP/YOolaXeJx315b4eqkg3ot+lZUtvCv/6VV3k3zQg==} + '@unocss/extractor-svelte@66.6.6': + resolution: {integrity: sha512-5+Et3jiSFlMqxkoyVLsoT2/Rd8x/Jd65i5KzIyXMtQccDmqN2wSXuyvB2h5sLauHn4bBe/qOWO3PfGjbXBGWOA==} '@unocss/inspector@66.6.0': resolution: {integrity: sha512-BvdY8ah+OTmzFMb+z8RZkaF15+PWRFt9S2bOARkkRBubybX9EE1rxM07l74kO5Dj16++CS4nO15XFq39pPoBvg==} @@ -1615,12 +1619,12 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} - conventional-changelog-angular@8.1.0: - resolution: {integrity: sha512-GGf2Nipn1RUCAktxuVauVr1e3r8QrLP/B0lEUsFktmGqc3ddbQkhoJZHJctVU829U1c6mTSWftrVOCHaL85Q3w==} + conventional-changelog-angular@8.3.0: + resolution: {integrity: sha512-DOuBwYSqWzfwuRByY9O4oOIvDlkUCTDzfbOgcSbkY+imXXj+4tmrEFao3K+FxemClYfYnZzsvudbwrhje9VHDA==} engines: {node: '>=18'} - conventional-changelog-conventionalcommits@9.1.0: - resolution: {integrity: sha512-MnbEysR8wWa8dAEvbj5xcBgJKQlX/m0lhS8DsyAAWDHdfs2faDJxTgzRYlRYpXSe7UiKrIIlB4TrBKU9q9DgkA==} + conventional-changelog-conventionalcommits@9.3.0: + resolution: {integrity: sha512-kYFx6gAyjSIMwNtASkI3ZE99U1fuVDJr0yTYgVy+I2QG46zNZfl2her+0+eoviG82c5WQvW1jMt1eOQTeJLodA==} engines: {node: '>=18'} conventional-changelog-preset-loader@5.0.0: @@ -1636,6 +1640,11 @@ packages: engines: {node: '>=18'} hasBin: true + conventional-commits-parser@6.3.0: + resolution: {integrity: sha512-RfOq/Cqy9xV9bOA8N+ZH6DlrDR+5S3Mi0B5kACEjESpE+AviIpAptx9a9cFpWCCvgRtWT+0BbUw+e1BZfts9jg==} + engines: {node: '>=18'} + hasBin: true + conventional-recommended-bump@10.0.0: resolution: {integrity: sha512-RK/fUnc2btot0oEVtrj3p2doImDSs7iiz/bftFCDzels0Qs1mxLghp+DFHMaOC0qiCI6sWzlTDyBFSYuot6pRA==} engines: {node: '>=18'} @@ -1656,8 +1665,8 @@ packages: cosmiconfig: '>=9' typescript: '>=5' - cosmiconfig@9.0.0: - resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + cosmiconfig@9.0.1: + resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} engines: {node: '>=14'} peerDependencies: typescript: '>=4.9.5' @@ -2609,8 +2618,8 @@ packages: resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} engines: {node: '>=6.0.0'} - prettier-plugin-svelte@3.5.0: - resolution: {integrity: sha512-2lLO/7EupnjO/95t+XZesXs8Bf3nYLIDfCo270h5QWbj/vjLqmrQ1LiRk9LPggxSDsnVYfehamZNf+rgQYApZg==} + prettier-plugin-svelte@3.5.1: + resolution: {integrity: sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==} peerDependencies: prettier: ^3.0.0 svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 @@ -3225,32 +3234,32 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@commitlint/cli@20.4.2(@types/node@25.0.10)(typescript@5.9.3)': + '@commitlint/cli@20.4.3(@types/node@25.0.10)(typescript@5.9.3)': dependencies: - '@commitlint/format': 20.4.0 - '@commitlint/lint': 20.4.2 - '@commitlint/load': 20.4.0(@types/node@25.0.10)(typescript@5.9.3) - '@commitlint/read': 20.4.0 - '@commitlint/types': 20.4.0 + '@commitlint/format': 20.4.3 + '@commitlint/lint': 20.4.3 + '@commitlint/load': 20.4.3(@types/node@25.0.10)(typescript@5.9.3) + '@commitlint/read': 20.4.3 + '@commitlint/types': 20.4.3 tinyexec: 1.0.2 yargs: 17.7.2 transitivePeerDependencies: - '@types/node' - typescript - '@commitlint/config-conventional@20.4.2': + '@commitlint/config-conventional@20.4.3': dependencies: - '@commitlint/types': 20.4.0 - conventional-changelog-conventionalcommits: 9.1.0 + '@commitlint/types': 20.4.3 + conventional-changelog-conventionalcommits: 9.3.0 - '@commitlint/config-validator@20.4.0': + '@commitlint/config-validator@20.4.3': dependencies: - '@commitlint/types': 20.4.0 + '@commitlint/types': 20.4.3 ajv: 8.17.1 - '@commitlint/ensure@20.4.1': + '@commitlint/ensure@20.4.3': dependencies: - '@commitlint/types': 20.4.0 + '@commitlint/types': 20.4.3 lodash.camelcase: 4.3.0 lodash.kebabcase: 4.1.1 lodash.snakecase: 4.1.1 @@ -3259,31 +3268,31 @@ snapshots: '@commitlint/execute-rule@20.0.0': {} - '@commitlint/format@20.4.0': + '@commitlint/format@20.4.3': dependencies: - '@commitlint/types': 20.4.0 + '@commitlint/types': 20.4.3 picocolors: 1.1.1 - '@commitlint/is-ignored@20.4.1': + '@commitlint/is-ignored@20.4.3': dependencies: - '@commitlint/types': 20.4.0 + '@commitlint/types': 20.4.3 semver: 7.7.3 - '@commitlint/lint@20.4.2': + '@commitlint/lint@20.4.3': dependencies: - '@commitlint/is-ignored': 20.4.1 - '@commitlint/parse': 20.4.1 - '@commitlint/rules': 20.4.2 - '@commitlint/types': 20.4.0 + '@commitlint/is-ignored': 20.4.3 + '@commitlint/parse': 20.4.3 + '@commitlint/rules': 20.4.3 + '@commitlint/types': 20.4.3 - '@commitlint/load@20.4.0(@types/node@25.0.10)(typescript@5.9.3)': + '@commitlint/load@20.4.3(@types/node@25.0.10)(typescript@5.9.3)': dependencies: - '@commitlint/config-validator': 20.4.0 + '@commitlint/config-validator': 20.4.3 '@commitlint/execute-rule': 20.0.0 - '@commitlint/resolve-extends': 20.4.0 - '@commitlint/types': 20.4.0 - cosmiconfig: 9.0.0(typescript@5.9.3) - cosmiconfig-typescript-loader: 6.2.0(@types/node@25.0.10)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3) + '@commitlint/resolve-extends': 20.4.3 + '@commitlint/types': 20.4.3 + cosmiconfig: 9.0.1(typescript@5.9.3) + cosmiconfig-typescript-loader: 6.2.0(@types/node@25.0.10)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3) is-plain-obj: 4.1.0 lodash.mergewith: 4.6.2 picocolors: 1.1.1 @@ -3291,47 +3300,47 @@ snapshots: - '@types/node' - typescript - '@commitlint/message@20.4.0': {} + '@commitlint/message@20.4.3': {} - '@commitlint/parse@20.4.1': + '@commitlint/parse@20.4.3': dependencies: - '@commitlint/types': 20.4.0 - conventional-changelog-angular: 8.1.0 - conventional-commits-parser: 6.2.1 + '@commitlint/types': 20.4.3 + conventional-changelog-angular: 8.3.0 + conventional-commits-parser: 6.3.0 - '@commitlint/read@20.4.0': + '@commitlint/read@20.4.3': dependencies: - '@commitlint/top-level': 20.4.0 - '@commitlint/types': 20.4.0 + '@commitlint/top-level': 20.4.3 + '@commitlint/types': 20.4.3 git-raw-commits: 4.0.0 minimist: 1.2.8 tinyexec: 1.0.2 - '@commitlint/resolve-extends@20.4.0': + '@commitlint/resolve-extends@20.4.3': dependencies: - '@commitlint/config-validator': 20.4.0 - '@commitlint/types': 20.4.0 + '@commitlint/config-validator': 20.4.3 + '@commitlint/types': 20.4.3 global-directory: 4.0.1 import-meta-resolve: 4.2.0 lodash.mergewith: 4.6.2 resolve-from: 5.0.0 - '@commitlint/rules@20.4.2': + '@commitlint/rules@20.4.3': dependencies: - '@commitlint/ensure': 20.4.1 - '@commitlint/message': 20.4.0 + '@commitlint/ensure': 20.4.3 + '@commitlint/message': 20.4.3 '@commitlint/to-lines': 20.0.0 - '@commitlint/types': 20.4.0 + '@commitlint/types': 20.4.3 '@commitlint/to-lines@20.0.0': {} - '@commitlint/top-level@20.4.0': + '@commitlint/top-level@20.4.3': dependencies: escalade: 3.2.0 - '@commitlint/types@20.4.0': + '@commitlint/types@20.4.3': dependencies: - conventional-commits-parser: 6.2.1 + conventional-commits-parser: 6.3.0 picocolors: 1.1.1 '@conventional-changelog/git-client@1.0.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.2.1)': @@ -3521,7 +3530,7 @@ snapshots: dependencies: '@iconify/types': 2.0.0 - '@iconify-json/material-icon-theme@1.2.51': + '@iconify-json/material-icon-theme@1.2.55': dependencies: '@iconify/types': 2.0.0 @@ -3842,6 +3851,8 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} + '@simple-libs/stream-utils@1.2.0': {} + '@sinclair/typebox@0.31.28': {} '@sindresorhus/merge-streams@4.0.0': {} @@ -3987,7 +3998,7 @@ snapshots: dependencies: svelte: 5.48.0 - '@trivago/prettier-plugin-sort-imports@6.0.2(prettier-plugin-svelte@3.5.0(prettier@3.8.1)(svelte@5.48.0))(prettier@3.8.1)(svelte@5.48.0)': + '@trivago/prettier-plugin-sort-imports@6.0.2(prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.48.0))(prettier@3.8.1)(svelte@5.48.0)': dependencies: '@babel/generator': 7.28.6 '@babel/parser': 7.28.6 @@ -3999,7 +4010,7 @@ snapshots: parse-imports-exports: 0.2.4 prettier: 3.8.1 optionalDependencies: - prettier-plugin-svelte: 3.5.0(prettier@3.8.1)(svelte@5.48.0) + prettier-plugin-svelte: 3.5.1(prettier@3.8.1)(svelte@5.48.0) svelte: 5.48.0 transitivePeerDependencies: - supports-color @@ -4165,7 +4176,7 @@ snapshots: dependencies: '@unocss/core': 66.6.0 - '@unocss/extractor-svelte@66.6.3': {} + '@unocss/extractor-svelte@66.6.6': {} '@unocss/inspector@66.6.0': dependencies: @@ -4534,11 +4545,11 @@ snapshots: consola@3.4.2: {} - conventional-changelog-angular@8.1.0: + conventional-changelog-angular@8.3.0: dependencies: compare-func: 2.0.0 - conventional-changelog-conventionalcommits@9.1.0: + conventional-changelog-conventionalcommits@9.3.0: dependencies: compare-func: 2.0.0 @@ -4550,6 +4561,11 @@ snapshots: dependencies: meow: 13.2.0 + conventional-commits-parser@6.3.0: + dependencies: + '@simple-libs/stream-utils': 1.2.0 + meow: 13.2.0 + conventional-recommended-bump@10.0.0: dependencies: '@conventional-changelog/git-client': 1.0.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.2.1) @@ -4562,14 +4578,14 @@ snapshots: core-util-is@1.0.3: {} - cosmiconfig-typescript-loader@6.2.0(@types/node@25.0.10)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3): + cosmiconfig-typescript-loader@6.2.0(@types/node@25.0.10)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3): dependencies: '@types/node': 25.0.10 - cosmiconfig: 9.0.0(typescript@5.9.3) + cosmiconfig: 9.0.1(typescript@5.9.3) jiti: 2.6.1 typescript: 5.9.3 - cosmiconfig@9.0.0(typescript@5.9.3): + cosmiconfig@9.0.1(typescript@5.9.3): dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 @@ -5456,7 +5472,7 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier-plugin-svelte@3.5.0(prettier@3.8.1)(svelte@5.48.0): + prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.48.0): dependencies: prettier: 3.8.1 svelte: 5.48.0 @@ -5645,10 +5661,10 @@ snapshots: optionalDependencies: svelte: 5.48.0 - svelte-kit-sessions@0.4.0(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)))(svelte@5.48.0): + svelte-kit-sessions@0.4.0(@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)))(svelte@5.48.0): dependencies: '@isaacs/ttlcache': 1.4.1 - '@sveltejs/kit': 2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) + '@sveltejs/kit': 2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) svelte: 5.48.0 svelte-sonner@1.0.7(svelte@5.48.0):