diff --git a/README.md b/README.md index abc4b16e..c22cca8c 100644 --- a/README.md +++ b/README.md @@ -1,118 +1,283 @@ -`TODO: Add a nice screenshot of the app!` +# StudyBridge -# Cohort XX final project +## Overview -This is the final project for the HackYourFuture curriculum we did as a cohort using the [MERN stack](https://www.mongodb.com/resources/languages/mern-stack) by following the agile methodology with our team and a group of mentors. A quick guide to what we built: +StudyBridge is a full-stack educational platform that connects students and teachers in one ecosystem. +The application is designed to simplify the process of finding a tutor, booking lessons, managing communication, and organizing the learning workflow in real time. -> TODO: Add short description of the app +The platform supports two main user journeys: -`[Click here for the Demo version](TODO: add link)` +- **Student flow** - search for teachers, view profiles, create lesson appointments, communicate with tutors, receive notifications, and join online lessons +- **Teacher flow** - create and manage a professional profile, receive and process appointment requests, communicate with students, manage regular learners, and conduct online lessons -## 1. Setup +In addition, the platform includes moderation flows that help keep content structured and the teacher approval process controlled. -First, to setup all the directories run the following in the main directory: +StudyBridge is built as a modern full-stack application with strong attention to architecture, scalability, and real user interaction. The project combines a React-based client application with a Node.js backend, +real-time features via WebSockets, structured backend layering with CQRS, dependency injection, and TypeScript across the stack. -`npm install` +--- -`npm run setup` +## Tech Stack -The first command will install `cypress` and some small libraries needed for running the rest of the commands. The second will go into the `client` and `server` directories and set those up to be ran. +### Core stack +- **MERN** +- **TypeScript** +- **CQRS** +- **Dependency Injection** -In the `client` and `server` directory there are two `.env.example` files. Create a copy and rename that to `.env`. Then follow the instructions in those files to fill in the right values. +### Frontend +- **React** +- **React Router** +- **TanStack Query (React Query)** +- **Zustand** +- **Socket.IO Client** +- **Tailwind CSS** +- **Framer Motion** +- **Axios** -To run the app in dev mode you can run the following command in the main directory: +### Backend +- **Node.js** +- **Express** +- **Socket.IO** +- **MongoDB** +- **Mongoose** +- **InversifyJS** -`npm run dev` +### Tooling +- **ESLint** +- **Prettier** +- **Husky** +- **npm** -## 2. Code structure +--- -``` -client -├── public -└── src -| └── __tests__ -| └── __testUtils__ -| └── components -| └── hooks -| └── pages -| └── __tests__ -| └── components -| └── util -| index.jsx -cypress -| └── fixtures -| └── integration -| └── plugins -| └── support -server -└── src - └── __tests__ - └── __testUtils__ - └── controllers - └── db - └── models - └── routes - └── util - index.ts -``` +## Full Stack Description + +### MERN +StudyBridge follows the **MERN** stack architecture: + +- **MongoDB** for storing users, appointments, conversations, messages, notifications, and related entities +- **Express** for building the backend API and request handling +- **React** for the client-side user interface +- **Node.js** as the server runtime + +### TypeScript +TypeScript is used across both frontend and backend to ensure strong typing, safer refactoring, and better maintainability. This helps reduce runtime errors and improves developer experience. + +### CQRS +The backend uses **CQRS (Command Query Responsibility Segregation)**: + +- **Command repositories** are responsible for create, update, and delete operations +- **Query repositories** are responsible for read operations + +This separation makes the backend more scalable and keeps business logic organized. + +### Dependency Injection +The backend uses **InversifyJS** for dependency injection. +This allows services, repositories, and controllers to stay decoupled and easier to test. + +### Real-time communication +**Socket.IO** powers the real-time features of the platform: + +- live chat messaging +- typing indicators +- unread message counters +- online presence tracking +- incoming call events +- live notifications + +### State management +The frontend clearly separates responsibilities between: + +- **React Query** for server state +- **Zustand** for client-side and UI state + +This keeps the application predictable and easier to manage as it grows. + +--- + +## Features + +### Authentication and authorization +- Student and teacher registration +- Secure login flow +- Access token and refresh token handling +- Protected routes +- Role-based access control +- Session restore and logout flow + +### Teacher discovery +- Browse teacher profiles +- View lesson details, pricing, and profile information +- Search and filtering +- Teacher profile editing + +### Appointment management +- Create lesson appointments +- Approve or reject lesson requests +- Track appointment statuses +- Manage regular students +- Support recurring weekly schedules + +### Real-time chat +- One chat per teacher-student pair +- Real-time messaging +- Typing indicators +- Online presence +- Unread message counters +- Read/unread synchronization + +### Notifications +- Real-time notifications for new messages +- Notifications for appointment approval or rejection +- Persistent notifications stored in database +- Mark one as read +- Clear read notifications + +### Video calls +- Online lesson calls +- Incoming call popup +- Join links for specific calls +- Teacher-student live communication flow + +### Moderation +- Teacher moderation workflow +- Status management +- Restricted moderator routes -### 2.1 Client structure +--- -- `public` || public facing client code -- `__tests__` || any `jest` tests for specific components will be in a `__tests__` folder on the same level -- `__testUtils__` || any code that is only being used in the tests is put in the `__testUtils__` folder to separate that away from the rest of the code -- `components` || all of our shared components that are used over multiple pages -- `hooks` || all of our custom hooks -- `pages` || the page components of our app, any routing will go between these components -- `pages/components` || components used specifically on those pages -- `util` || any utility functions that can be used anywhere on the client side -- `main.tsx` || the start point of the client -- `vite.config.ts` || to configure vite +## Screenshots -### 2.2 Cypress structure +### Home page +![Home page](docs/homePage.png) -- `fixtures` || any data/files that `cypress` needs can be placed here -- `integration` || all of our tests are in here, separated in folders based on the pages in our app -- `plugins` || any plugins for our `cypress` configuration can be placed here -- `support` || custom commands and other support files for `cypress` can be placed here +### Teachers list +![Teacher profile](docs/teacherList.png) -### 2.3 Server structure +### Teacher profile +![Teacher profile](docs/teacherPage.png) -- `__tests__` || any `jest` tests for the api endpoints as that is our testing strategy for the backend -- `__testUtils__` || any code that is only being used in the tests is put in the `__testUtils__` folder to separate that away from the rest of the code -- `controllers` || all of our controller functions that interact with the database -- `db` || all of our configuration for the database -- `models` || all of our `mongoose` models will be placed here -- `routes` || code to match up the API with our controllers -- `util` || any utility functions that can be used anywhere on the server side -- `index.ts` || the start point of the server +### Appointments +![Chat page](docs/appointments.png) -## 3. Stack / external libraries +--- -The base stack of the app is a MERN stack (Mongoose, Express, React, Node). Next to that we make use of the following extras: +## Architecture -### 3.1 Configuration libraries +StudyBridge is structured as a layered full-stack application. -- `dotenv` || To load the .env variables into the process environment. See [docs](https://www.npmjs.com/package/dotenv) -- `vite` || To bundle our React app and create a static app to host. See [docs](https://vite.dev/) -- `husky` || To run our tests and linter before committing. See [docs](https://typicode.github.io/husky/#/) -- `eslint` || To check our code. We have different configurations for frontend and backend. You can check out the configuration in the `.eslintrc.(c)js` files in the respective `client` and `server` folders. See [docs](https://eslint.org/) -- `prettier` || To automatically format our code. See [docs](https://prettier.io/) -- `concurrently` || To run commands in parallel. See [docs](https://github.com/open-cli-tools/concurrently#readme) +### Frontend architecture +The frontend is built as a React SPA with a modular structure. -For more information on how these work together including the automatic deployment to heroku, have a look at our detailed [DEV](./DEV.md) file. +Main responsibilities are split into: +- **Pages** for route-level UI +- **Components** for reusable UI blocks +- **Features** for domain-specific logic +- **Hooks** for reusable behavior +- **Stores** for client-side state +- **API layer** for backend communication -### 3.2 Client-side libraries +### Backend architecture +The backend is divided into clear layers: -- `@testing-library/*` || We use React Testing Library to write all of our tests. See [docs](https://testing-library.com/docs/react-testing-library/intro/) -- `jest` || To run our tests and coverage. See [docs](https://jestjs.io/) -- `jest-fetch-mock` || To mock out the backend for our testing purposes. See [docs](https://github.com/jefflau/jest-fetch-mock#readme) +- **Controllers** — handle HTTP requests and responses +- **Services** — contain business logic +- **Command repositories** — handle write operations +- **Query repositories** — handle read operations +- **Schemas/models** — define persistence layer +- **Socket handlers** — manage real-time events + +This structure helps keep the codebase maintainable and scalable. + +--- + +## Why this project is technically interesting + +StudyBridge is more than a simple CRUD application. +It combines several real-world product requirements in one system: + +- authentication with refresh flow +- role-based access control +- real-time messaging +- persistent notifications +- appointment approval workflow +- online presence tracking +- video call integration +- CQRS-based backend architecture +- dependency injection + +The project demonstrates how to build a production-style platform with real-time communication and maintainable architecture. + +--- + +## Project Goal + +The goal of StudyBridge is to provide a convenient and structured environment where: + +- students can quickly find suitable teachers +- teachers can present their expertise and manage their work +- both sides can communicate smoothly in one application + +The platform is designed to reduce friction in online learning by combining search, booking, messaging, and lesson management in a single place. + +--- + +## Folder Structure + +### Client +```bash +client/ + src/ + api/ + components/ + features/ + hooks/ + layouts/ + pages/ + router/ + store/ + types/ + util/ + +server/ + src/ + composition/ + controllers/ + db/ + repositories/ + commandRepositories/ + queryRepositories/ + routes/ + services/ + socket/ + types/ + utils/ + validation/ +``` +## How to run locally + +## Clone repository +```bash +git clone +cd StudyBridge +``` + +## Install dependencies +```bash +npm install +cd client && npm install +cd ../server && npm install +``` +## Start the development servers + +```bash +cd client +npm run dev +``` +## Open the application +```bash +http://localhost:5173 +``` -### 3.3 Server-side libraries -- `nodemon` || To automatically restart the server when in development mode. See [docs](https://nodemon.io/) -- `jest` || To run our tests and coverage. See [docs](https://jestjs.io/) -- `supertest` || To more easily test our endpoints. See [docs](https://github.com/visionmedia/supertest#readme) -- `mongodb-memory-server` || To mock out our database in our backend tests. See [docs](https://github.com/nodkz/mongodb-memory-server) -- `cors` || To open up our API. See [docs](https://github.com/expressjs/cors#readme) -- `mongoose` || To add schemas to our database. See [docs](https://mongoosejs.com/) diff --git a/client/src/components/DropdownNotificationsMenu/DropdownNotificationsMenu.tsx b/client/src/components/DropdownNotificationsMenu/DropdownNotificationsMenu.tsx index c2fbdcee..28b30ca4 100644 --- a/client/src/components/DropdownNotificationsMenu/DropdownNotificationsMenu.tsx +++ b/client/src/components/DropdownNotificationsMenu/DropdownNotificationsMenu.tsx @@ -182,7 +182,7 @@ export const DropdownNotificationsMenu = ({
{option.actor.name} - +
{option.status === "approved" diff --git a/client/src/components/notificationBar/NotificationBar.tsx b/client/src/components/notificationBar/NotificationBar.tsx index cbf94636..94ff2bb8 100644 --- a/client/src/components/notificationBar/NotificationBar.tsx +++ b/client/src/components/notificationBar/NotificationBar.tsx @@ -31,12 +31,14 @@ export const NotificationBar = ({ aria-expanded={openMenu} aria-haspopup="menu" > -
- {unreadNotifications} -
+ > + {unreadNotifications} +
+ )} { queryClient.invalidateQueries({ queryKey: queryKeys.teacherAppointments(appointment.teacherId), }); + queryClient.invalidateQueries({ queryKey: chatKeys.conversations }); queryClient.invalidateQueries({ queryKey: queryKeys.studentAppointments(appointment.studentId), }); diff --git a/client/src/features/chat/chat.query.ts b/client/src/features/chat/chat.query.ts index ea6d4cd8..9a69c62b 100644 --- a/client/src/features/chat/chat.query.ts +++ b/client/src/features/chat/chat.query.ts @@ -9,10 +9,14 @@ export function useChatConversationsQuery() { }); } -export function useChatMessagesQuery(conversationId: string | undefined) { +export function useChatMessagesQuery( + conversationId: string | undefined, + enabled = true, +) { return useQuery({ queryKey: chatKeys.messages(conversationId ?? ""), queryFn: () => getMessages(conversationId as string), - enabled: Boolean(conversationId), + enabled: Boolean(conversationId) && enabled, + retry: false, }); } diff --git a/client/src/hooks/useChatRealtime.ts b/client/src/hooks/useChatRealtime.ts index 0a15f911..7674c526 100644 --- a/client/src/hooks/useChatRealtime.ts +++ b/client/src/hooks/useChatRealtime.ts @@ -62,7 +62,7 @@ export function useChatRealtime(args: { ); if (myUserId && msg.senderId !== myUserId) { - void markConversationAsRead(conversationId); + void markConversationAsRead(conversationId).catch(() => {}); } }; diff --git a/client/src/pages/chat/chatDialogPage/ChatDialogPage.tsx b/client/src/pages/chat/chatDialogPage/ChatDialogPage.tsx index be174f2c..468503a9 100644 --- a/client/src/pages/chat/chatDialogPage/ChatDialogPage.tsx +++ b/client/src/pages/chat/chatDialogPage/ChatDialogPage.tsx @@ -21,32 +21,44 @@ export const ChatDialogPage = () => { const { id: conversationId } = useParams(); const socket = useSocketStore((s) => s.socket); const myUserId = useAuthSessionStore((s) => s.user?.id); - const { data: messages = [] } = useChatMessagesQuery(conversationId); + const { data: conversations = [] } = useChatConversationsQuery(); + const conversation = useMemo( () => conversations.find((c) => c.id === conversationId), [conversations, conversationId], ); + const { data: messages = [] } = useChatMessagesQuery( + conversationId, + Boolean(conversation), + ); + const peer = conversation?.peer; const peerId = peer?.id; - useChatRealtime({ socket, conversationId, myUserId }); + useChatRealtime({ + socket, + conversationId: conversation ? conversationId : undefined, + myUserId, + }); const { typingUserId } = useTypingIndicator({ socket, - conversationId, + conversationId: conversation ? conversationId : undefined, myUserId, }); + const { emitTyping, stopTypingNow } = useTypingEmitter({ socket, - conversationId, + conversationId: conversation ? conversationId : undefined, }); const [text, setText] = useState(""); + const send = useSendChatMessage({ socket, - conversationId, + conversationId: conversation ? conversationId : undefined, stopTypingNow, onSuccess: () => setText(""), }); @@ -56,11 +68,12 @@ export const ChatDialogPage = () => { ); useEffect(() => { - if (!conversationId || !socket) { + if (!conversationId || !socket || !conversation) { return; } - void markConversationAsRead(conversationId); - }, [conversationId, socket]); + + void markConversationAsRead(conversationId).catch(() => {}); + }, [conversationId, socket, conversation]); if (!conversation) { return
Conversation not found
; diff --git a/docs/appointments.png b/docs/appointments.png new file mode 100644 index 00000000..71890b27 Binary files /dev/null and b/docs/appointments.png differ diff --git a/docs/homePage.png b/docs/homePage.png new file mode 100644 index 00000000..3d859ffc Binary files /dev/null and b/docs/homePage.png differ diff --git a/docs/teacherList.png b/docs/teacherList.png new file mode 100644 index 00000000..2d55e80c Binary files /dev/null and b/docs/teacherList.png differ diff --git a/docs/teacherPage.png b/docs/teacherPage.png new file mode 100644 index 00000000..a06d8a32 Binary files /dev/null and b/docs/teacherPage.png differ diff --git a/server/package-lock.json b/server/package-lock.json index 361e3727..c451176f 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -20,7 +20,6 @@ "google-auth-library": "^10.6.1", "inversify": "^7.11.0", "jsonwebtoken": "^9.0.3", - "mongodb-memory-server": "^11.0.1", "mongoose": "^9.1.5", "multer": "^2.0.2", "nodemailer": "^8.0.1", @@ -49,6 +48,7 @@ "eslint-plugin-import": "2.32.0", "eslint-plugin-prettier": "^5.1.3", "jest": "^30.2.0", + "mongodb-memory-server": "^11.0.1", "nodemon": "^3.1.0", "prettier": "^3.2.5", "prettier-eslint": "^16.3.0", @@ -5025,6 +5025,7 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.4.0" @@ -5057,6 +5058,7 @@ "version": "1.7.3", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "dev": true, "license": "Apache-2.0", "peerDependencies": { "react-native-b4a": "*" @@ -5218,6 +5220,7 @@ "version": "2.8.2", "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "dev": true, "license": "Apache-2.0", "peerDependencies": { "bare-abort-controller": "*" @@ -5417,6 +5420,7 @@ "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, "license": "MIT", "engines": { "node": "*" @@ -5721,6 +5725,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, "license": "MIT" }, "node_modules/component-emitter": { @@ -6903,6 +6908,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "bare-events": "^2.7.0" @@ -7040,6 +7046,7 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -7193,6 +7200,7 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, "license": "MIT", "dependencies": { "commondir": "^1.0.1", @@ -7210,6 +7218,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, "license": "MIT", "dependencies": { "semver": "^6.0.0" @@ -7225,6 +7234,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -7260,6 +7270,7 @@ "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, "funding": [ { "type": "individual", @@ -10059,6 +10070,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -10518,6 +10530,7 @@ "version": "11.0.1", "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-11.0.1.tgz", "integrity": "sha512-nUlKovSJZBh7q5hPsewFRam9H66D08Ne18nyknkNalfXMPtK1Og3kOcuqQhcX88x/pghSZPIJHrLbxNFW3OWiw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -10532,6 +10545,7 @@ "version": "11.0.1", "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-11.0.1.tgz", "integrity": "sha512-IcIb2S9Xf7Lmz43Z1ZujMqNg7PU5Q7yn+4wOnu7l6pfeGPkEmlqzV1hIbroVx8s4vXhPB1oMGC1u8clW7aj3Xw==", + "dev": true, "license": "MIT", "dependencies": { "async-mutex": "^0.5.0", @@ -10555,6 +10569,7 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -10567,6 +10582,7 @@ "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -10724,6 +10740,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/new-find-package-json/-/new-find-package-json-2.0.0.tgz", "integrity": "sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.3.4" @@ -11095,6 +11112,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -11107,6 +11125,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -11122,6 +11141,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -11178,6 +11198,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -11255,6 +11276,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, "license": "MIT" }, "node_modules/picocolors": { @@ -11291,6 +11313,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, "license": "MIT", "dependencies": { "find-up": "^4.0.0" @@ -11945,6 +11968,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -12381,6 +12405,7 @@ "version": "2.23.0", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "dev": true, "license": "MIT", "dependencies": { "events-universal": "^1.0.0", @@ -12662,6 +12687,7 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, "license": "MIT", "dependencies": { "b4a": "^1.6.4", @@ -12688,6 +12714,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, "license": "Apache-2.0", "dependencies": { "b4a": "^1.6.4" @@ -12932,6 +12959,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, "license": "0BSD" }, "node_modules/tsx": { @@ -13679,6 +13707,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.0.tgz", "integrity": "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==", + "dev": true, "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3",