├── .gitignore ├── .vscode └── settings.json ├── MANIFEST.in ├── README.md ├── doppio ├── __init__.py ├── commands │ ├── __init__.py │ ├── boilerplates.py │ ├── desk_page.py │ ├── spa_generator.py │ └── utils.py ├── config │ ├── __init__.py │ ├── desktop.py │ └── docs.py ├── doppio │ └── __init__.py ├── hooks.py ├── modules.txt ├── patches.txt ├── templates │ ├── __init__.py │ └── pages │ │ └── __init__.py └── tests │ └── test_spa_generation.py ├── libs ├── controllers │ ├── auth.js │ ├── call.js │ └── socket.js └── resourceManager │ ├── ResourceManager.js │ └── index.js ├── license.txt ├── package.json ├── requirements.txt ├── setup.py └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.egg-info 4 | *.swp 5 | tags 6 | doppio/docs/current 7 | node_modules/ 8 | 9 | doppio/docs/current 10 | doppio/public/vision 11 | doppio/www/vision.html 12 | doppio/public/css/email.css 13 | vision/tailwind.theme.json 14 | vision/dist/assets -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[python]": { 3 | "editor.defaultFormatter": "ms-python.black-formatter" 4 | }, 5 | "python.formatting.provider": "none" 6 | } -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include requirements.txt 3 | include *.json 4 | include *.md 5 | include *.py 6 | include *.txt 7 | recursive-include doppio *.css 8 | recursive-include doppio *.csv 9 | recursive-include doppio *.html 10 | recursive-include doppio *.ico 11 | recursive-include doppio *.js 12 | recursive-include doppio *.json 13 | recursive-include doppio *.md 14 | recursive-include doppio *.png 15 | recursive-include doppio *.py 16 | recursive-include doppio *.svg 17 | recursive-include doppio *.txt 18 | recursive-exclude doppio *.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Doppio 2 | 3 | A Frappe App to setup and manage single page applications and custom desk pages (using Vue 3 or React) on your custom Frappe App. 4 | 5 | ## Installation 6 | 7 | In your bench directory: 8 | 9 | ```bash 10 | bench get-app https://github.com/NagariaHussain/doppio 11 | ``` 12 | 13 | This will install the `Doppio` frappe app on your bench and enable some custom bench CLI commands 14 | that will ease the process of attaching a SPA to your Frappe Application. 15 | 16 | ## Setting Up React/Vue SPA 17 | 18 | To set up a new Single Page Application, you can run the following command in your bench directory: 19 | 20 | ```bash 21 | bench add-spa --app [--tailwindcss] [--typescript] 22 | 23 | # or just, and answer the prompts 24 | bench add-spa 25 | ``` 26 | 27 | You will be prompted to enter a name for your single page application, this will be the name of the directory and the URI path at which the application will be served. For instance, if you enter `dashboard` (default), then a folder named `dashboard` will be created inside your app's root directory and the application will be served at `/dashboard`. 28 | 29 | You will then be asked to select the framework you prefer: React or Vue. 30 | 31 | You can also select whether you want to use Typescript or Javascript. 32 | 33 | You can optionally pass the `--tailwindcss` flag which will also setup tailwindCSS (who doesn't like tailwind!) along with the Vue 3/React application. 34 | 35 | The above command will do the following things: 36 | 37 | ### For Vue 3 38 | 39 | 1. Scaffold a new Vue 3 starter application (using [Vite](https://vitejs.dev/)) 40 | 41 | 2. Add and configure Vue router 42 | 43 | 3. Link utility and controller files to make the connection with Frappe backend a breeze! 44 | 45 | 4. Configure Vite's proxy options (which will be helpful in development), check the `proxyOptions.js` file to see to what ports the Vite dev server proxies the requests (you frappe bench server). 46 | 47 | 5. Optionally, installs and set up tailwindCSS. 48 | 49 | 6. Update the `website_route_rules` hook (in `hooks.py` of your app) to handle the routing of this SPA. 50 | 51 | ### For React 52 | 53 | 1. Scaffold a new React starter application (using [Vite](https://vitejs.dev/)) 54 | 55 | 2. Add and configure [frappe-react-sdk](https://github.com/nikkothari22/frappe-react-sdk) to make the connection with Frappe backend a breeze! 56 | 57 | 3. Configure Vite's proxy options (which will be helpful in development), check the `proxyOptions.js` file to see to what ports the Vite dev server proxies the requests (you frappe bench server). 58 | 59 | 4. Optionally, installs and set up tailwindCSS. 60 | 61 | 5. Update the `website_route_rules` hook (in `hooks.py` of your app) to handle the routing of this SPA. 62 | 63 | Once the setup is complete, you can `cd` into the SPA directory of your app (e.g. `dashboard`) and run: 64 | 65 | ```bash 66 | yarn dev 67 | ``` 68 | 69 | This will start a development server at port `8080` by default (any other port if this port's already in use). You can view the running application at: `:8080`. 70 | 71 | ## Adding FrappeUI 72 | 73 | If you want to add a [frappe-ui](https://github.com/frappe/frappe-ui) starter project to your custom app, you can do that using just a single command: 74 | 75 | ```bash 76 | bench add-frappe-ui 77 | ``` 78 | 79 | ## Creating Desk Pages 80 | 81 | If you want to setup Vue 3 or React powered custom desk pages, you can do that with just a single command: 82 | 83 | ```bash 84 | bench --site add-desk-page --app 85 | ``` 86 | 87 | Follow the prompt to select the framework of your choice and **everything will be setup for you auto-magically**! Once the setup is done, the page will be opened up in the browser. 88 | 89 | > Note: Restart your bench to get auto-reload on file changes for your custom app 90 | 91 | ## Building for Production 92 | 93 | The below command builds the application and places it in the `www` directory of your frappe app: 94 | 95 | ```bash 96 | cd && yarn build 97 | ``` 98 | 99 | Check the `package.json` file inside the Vue application directory to learn more about the dev server / build steps. 100 | 101 | If you already have a package.json file with scripts in your app's root directory, you can add the following two scripts to your app's package.json file in order for the `bench build` command to work as expected: 102 | 103 | ```json 104 | "dev": "cd && yarn dev", 105 | "build": "cd && yarn build" 106 | ``` 107 | 108 | ### License 109 | 110 | [MIT](./license.txt) 111 | -------------------------------------------------------------------------------- /doppio/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __version__ = '0.0.1' 3 | 4 | -------------------------------------------------------------------------------- /doppio/commands/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | import frappe 3 | import subprocess 4 | from pathlib import Path 5 | 6 | from .spa_generator import SPAGenerator 7 | from frappe.commands import get_site, pass_context 8 | from .utils import add_commands_to_root_package_json, add_routing_rule_to_hooks 9 | from .desk_page import setup_desk_page 10 | 11 | 12 | @click.command("add-spa") 13 | @click.option("--name", default="dashboard", prompt="Dashboard Name") 14 | @click.option("--app", prompt="App Name") 15 | @click.option( 16 | "--framework", 17 | type=click.Choice(["vue", "react"]), 18 | default="vue", 19 | prompt="Which framework do you want to use?", 20 | help="The framework to use for the SPA", 21 | ) 22 | @click.option( 23 | "--typescript", 24 | default=False, 25 | prompt="Configure TypeScript?", 26 | is_flag=True, 27 | help="Configure with TypeScript", 28 | ) 29 | @click.option( 30 | "--tailwindcss", default=False, is_flag=True, help="Configure tailwindCSS" 31 | ) 32 | def generate_spa(framework, name, app, typescript, tailwindcss): 33 | if not app: 34 | click.echo("Please provide an app with --app") 35 | return 36 | generator = SPAGenerator(framework, name, app, tailwindcss, typescript) 37 | generator.generate_spa() 38 | 39 | 40 | @click.command("add-frappe-ui") 41 | @click.option("--name", default="frontend", prompt="Dashboard Name") 42 | @click.option("--app", prompt="App Name") 43 | def add_frappe_ui(name, app): 44 | if not app: 45 | click.echo("Please provide an app with --app") 46 | return 47 | 48 | click.echo(f"Adding Frappe UI starter to {app}...") 49 | add_frappe_ui_starter(name, app) 50 | 51 | click.echo( 52 | f"🖥️ You can start the dev server by running 'yarn dev' in apps/{app}/{name}" 53 | ) 54 | click.echo("📄 Docs: https://frappeui.com") 55 | 56 | 57 | def add_frappe_ui_starter(name, app): 58 | subprocess.run( 59 | ["npx", "degit", "NagariaHussain/doppio_frappeui_starter", name], 60 | cwd=Path("../apps", app), 61 | ) 62 | subprocess.run(["yarn"], cwd=Path("../apps", app, name)) 63 | 64 | add_commands_to_root_package_json(app, name) 65 | add_routing_rule_to_hooks(app, name) 66 | replace_frontend_name_in_starter(app, name) 67 | 68 | 69 | def replace_frontend_name_in_starter(app, name): 70 | spa_path = Path("../apps", app, name) 71 | files = ("vite.config.js", "src/router.js") 72 | 73 | for file in files: 74 | file_path = spa_path / file 75 | fixed_content = "" 76 | with file_path.open("r") as f: 77 | fixed_content = f.read().replace("/frontend", f"/{name}") 78 | with file_path.open("w") as f: 79 | f.write(fixed_content) 80 | 81 | 82 | @click.command("add-desk-page") 83 | @click.option("--page-name", prompt="Page Name") 84 | @click.option("--app", prompt="App Name") 85 | @click.option( 86 | "--starter", 87 | type=click.Choice(["vue", "react"]), 88 | default="vue", 89 | prompt="Which framework do you want to use?", 90 | help="Setup a desk page with the framework of your choice", 91 | ) 92 | @pass_context 93 | def add_desk_page(context, app, page_name, starter): 94 | site = get_site(context) 95 | frappe.init(site=site) 96 | 97 | try: 98 | frappe.connect() 99 | setup_desk_page(site, app, page_name, starter) 100 | finally: 101 | frappe.destroy() 102 | 103 | 104 | commands = [generate_spa, add_frappe_ui, add_desk_page] 105 | -------------------------------------------------------------------------------- /doppio/commands/boilerplates.py: -------------------------------------------------------------------------------- 1 | APP_VUE_BOILERPLATE = """ 7 | 8 | 9 | 14 | """ 15 | 16 | HOME_VUE_BOILERPLATE = """ 23 | 24 | 38 | """ 39 | 40 | LOGIN_VUE_BOILERPLATE = """ 60 | 87 | """ 88 | 89 | VUE_VITE_CONFIG_BOILERPLATE = """import path from 'path'; 90 | import { defineConfig } from 'vite'; 91 | import vue from '@vitejs/plugin-vue'; 92 | import proxyOptions from './proxyOptions'; 93 | 94 | // https://vitejs.dev/config/ 95 | export default defineConfig({ 96 | plugins: [vue()], 97 | server: { 98 | port: 8080, 99 | host: '0.0.0.0', 100 | proxy: proxyOptions 101 | }, 102 | resolve: { 103 | alias: { 104 | '@': path.resolve(__dirname, 'src') 105 | } 106 | }, 107 | build: { 108 | outDir: '../{{app}}/public/{{name}}', 109 | emptyOutDir: true, 110 | target: 'es2015', 111 | }, 112 | }); 113 | """ 114 | 115 | PROXY_OPTIONS_BOILERPLATE = """const common_site_config = require('../../../sites/common_site_config.json'); 116 | const { webserver_port } = common_site_config; 117 | 118 | export default { 119 | '^/(app|api|assets|files|private)': { 120 | target: `http://127.0.0.1:${webserver_port}`, 121 | ws: true, 122 | router: function(req) { 123 | const site_name = req.headers.host.split(':')[0]; 124 | return `http://${site_name}:${webserver_port}`; 125 | } 126 | } 127 | }; 128 | """ 129 | 130 | MAIN_JS_BOILERPLATE = """import { createApp, reactive } from "vue"; 131 | import App from "./App.vue"; 132 | 133 | import router from './router'; 134 | import resourceManager from "../../../doppio/libs/resourceManager"; 135 | import call from "../../../doppio/libs/controllers/call"; 136 | import socket from "../../../doppio/libs/controllers/socket"; 137 | import Auth from "../../../doppio/libs/controllers/auth"; 138 | 139 | const app = createApp(App); 140 | const auth = reactive(new Auth()); 141 | 142 | // Plugins 143 | app.use(router); 144 | app.use(resourceManager); 145 | 146 | // Global Properties, 147 | // components can inject this 148 | app.provide("$auth", auth); 149 | app.provide("$call", call); 150 | app.provide("$socket", socket); 151 | 152 | 153 | // Configure route gaurds 154 | router.beforeEach(async (to, from, next) => { 155 | if (to.matched.some((record) => !record.meta.isLoginPage)) { 156 | // this route requires auth, check if logged in 157 | // if not, redirect to login page. 158 | if (!auth.isLoggedIn) { 159 | next({ name: 'Login', query: { route: to.path } }); 160 | } else { 161 | next(); 162 | } 163 | } else { 164 | if (auth.isLoggedIn) { 165 | next({ name: 'Home' }); 166 | } else { 167 | next(); 168 | } 169 | } 170 | }); 171 | 172 | app.mount("#app"); 173 | """ 174 | 175 | ROUTER_INDEX_BOILERPLATE = """import { createRouter, createWebHistory } from "vue-router"; 176 | import Home from "../views/Home.vue"; 177 | import authRoutes from './auth'; 178 | 179 | const routes = [ 180 | { 181 | path: "/", 182 | name: "Home", 183 | component: Home, 184 | }, 185 | ...authRoutes, 186 | ]; 187 | 188 | const router = createRouter({ 189 | base: "/{{name}}/", 190 | history: createWebHistory(), 191 | routes, 192 | }); 193 | 194 | export default router; 195 | """ 196 | 197 | 198 | AUTH_ROUTES_BOILERPLATE = """export default [ 199 | { 200 | path: '/login', 201 | name: 'Login', 202 | component: () => 203 | import(/* webpackChunkName: "login" */ '../views/Login.vue'), 204 | meta: { 205 | isLoginPage: true 206 | }, 207 | props: true 208 | } 209 | ] 210 | """ 211 | 212 | REACT_VITE_CONFIG_BOILERPLATE = """import path from 'path'; 213 | import { defineConfig } from 'vite'; 214 | import react from '@vitejs/plugin-react' 215 | import proxyOptions from './proxyOptions'; 216 | 217 | // https://vitejs.dev/config/ 218 | export default defineConfig({ 219 | plugins: [react()], 220 | server: { 221 | port: 8080, 222 | host: '0.0.0.0', 223 | proxy: proxyOptions 224 | }, 225 | resolve: { 226 | alias: { 227 | '@': path.resolve(__dirname, 'src') 228 | } 229 | }, 230 | build: { 231 | outDir: '../{{app}}/public/{{name}}', 232 | emptyOutDir: true, 233 | target: 'es2015', 234 | }, 235 | }); 236 | """ 237 | 238 | APP_REACT_BOILERPLATE = """import { useState } from 'react' 239 | import reactLogo from './assets/react.svg' 240 | import './App.css' 241 | import { FrappeProvider } from 'frappe-react-sdk' 242 | function App() { 243 | const [count, setCount] = useState(0) 244 | 245 | return ( 246 |
247 | 248 |
249 | 257 |

Vite + React + Frappe

258 |
259 | 262 |

263 | Edit src/App.jsx and save to test HMR 264 |

265 |
266 |

267 | Click on the Vite and React logos to learn more 268 |

269 |
270 |
271 |
272 | ) 273 | } 274 | 275 | export default App 276 | """ 277 | 278 | DESK_PAGE_JS_TEMPLATE = """frappe.pages["{{ page_name }}"].on_page_load = function (wrapper) { 279 | frappe.ui.make_app_page({ 280 | parent: wrapper, 281 | title: __("{{ page_title }}"), 282 | single_column: true, 283 | }); 284 | }; 285 | 286 | frappe.pages["{{ page_name }}"].on_page_show = function (wrapper) { 287 | load_desk_page(wrapper); 288 | }; 289 | 290 | function load_desk_page(wrapper) { 291 | let $parent = $(wrapper).find(".layout-main-section"); 292 | $parent.empty(); 293 | 294 | frappe.require("{{ scrubbed_name }}.bundle.{{ bundle_type }}").then(() => { 295 | frappe.{{ scrubbed_name }} = new frappe.ui.{{ pascal_cased_name }}({ 296 | wrapper: $parent, 297 | page: wrapper.page, 298 | }); 299 | }); 300 | } 301 | """ 302 | 303 | DESK_PAGE_JS_BUNDLE_TEMPLATE_VUE = """import { createApp } from "vue"; 304 | import App from "./App.vue"; 305 | 306 | 307 | class {{ pascal_cased_name }} { 308 | constructor({ page, wrapper }) { 309 | this.$wrapper = $(wrapper); 310 | this.page = page; 311 | 312 | this.init(); 313 | } 314 | 315 | init() { 316 | this.setup_page_actions(); 317 | this.setup_app(); 318 | } 319 | 320 | setup_page_actions() { 321 | // setup page actions 322 | this.primary_btn = this.page.set_primary_action(__("Print Message"), () => 323 | frappe.msgprint("Hello My Page!") 324 | ); 325 | } 326 | 327 | setup_app() { 328 | // create a vue instance 329 | let app = createApp(App); 330 | // mount the app 331 | this.${{ scrubbed_name }} = app.mount(this.$wrapper.get(0)); 332 | } 333 | } 334 | 335 | frappe.provide("frappe.ui"); 336 | frappe.ui.{{ pascal_cased_name }} = {{ pascal_cased_name }}; 337 | export default {{ pascal_cased_name }}; 338 | """ 339 | 340 | DESK_PAGE_VUE_APP_COMPONENT_BOILERPLATE = """ 345 | """ 351 | 352 | DESK_PAGE_REACT_APP_COMPONENT_BOILERPLATE = """import * as React from "react"; 353 | 354 | export function App() { 355 | const dynamicMessage = React.useState("Hello from App.jsx"); 356 | return ( 357 |
358 |

{dynamicMessage}

359 |

Start editing at {{ app_component_path }}

360 |
361 | ); 362 | }""" 363 | 364 | DESK_PAGE_JSX_BUNDLE_TEMPLATE_REACT = """import * as React from "react"; 365 | import { App } from "./App"; 366 | import { createRoot } from "react-dom/client"; 367 | 368 | 369 | class {{ pascal_cased_name }} { 370 | constructor({ page, wrapper }) { 371 | this.$wrapper = $(wrapper); 372 | this.page = page; 373 | 374 | this.init(); 375 | } 376 | 377 | init() { 378 | this.setup_page_actions(); 379 | this.setup_app(); 380 | } 381 | 382 | setup_page_actions() { 383 | // setup page actions 384 | this.primary_btn = this.page.set_primary_action(__("Print Message"), () => 385 | frappe.msgprint("Hello My Page!") 386 | ); 387 | } 388 | 389 | setup_app() { 390 | // create and mount the react app 391 | const root = createRoot(this.$wrapper.get(0)); 392 | root.render(); 393 | this.${{ scrubbed_name }} = root; 394 | } 395 | } 396 | 397 | frappe.provide("frappe.ui"); 398 | frappe.ui.{{ pascal_cased_name }} = {{ pascal_cased_name }}; 399 | export default {{ pascal_cased_name }}; 400 | """ 401 | -------------------------------------------------------------------------------- /doppio/commands/desk_page.py: -------------------------------------------------------------------------------- 1 | import os 2 | import click 3 | import frappe 4 | import subprocess 5 | 6 | from frappe import scrub 7 | from pathlib import Path 8 | 9 | from .boilerplates import ( 10 | DESK_PAGE_JS_BUNDLE_TEMPLATE_VUE, 11 | DESK_PAGE_JS_TEMPLATE, 12 | DESK_PAGE_VUE_APP_COMPONENT_BOILERPLATE, 13 | DESK_PAGE_JSX_BUNDLE_TEMPLATE_REACT, 14 | DESK_PAGE_REACT_APP_COMPONENT_BOILERPLATE, 15 | ) 16 | 17 | 18 | def setup_desk_page(site, app_name, page_name, starter): 19 | if not frappe.conf.developer_mode: 20 | click.echo("Please enable developer mode to add custom page") 21 | return 22 | 23 | page = create_page_doc(page_name, app_name, site) 24 | 25 | if starter == "vue": 26 | setup_vue_desk_page_starter(page, app_name) 27 | elif starter == "react": 28 | setup_react_desk_page_starter(page, app_name) 29 | else: 30 | click.echo("Please provide a valid starter") 31 | return 32 | 33 | launch_desk_page_in_browser(page, site) 34 | 35 | 36 | def setup_vue_desk_page_starter(page_doc, app_name): 37 | setup_desk_page_for_framework("vue", page_doc, app_name) 38 | 39 | 40 | def setup_react_desk_page_starter(page_doc, app_name): 41 | # check if package.json exists in app directory 42 | # if not, create package.json using npm init --yes 43 | app_path = Path("../apps") / app_name 44 | package_json_path = app_path / "package.json" 45 | if not package_json_path.exists(): 46 | subprocess.run(["npm", "init", "--yes"], cwd=app_path) 47 | 48 | # install react and react-dom 49 | click.echo("Installing react and react-dom...") 50 | subprocess.run( 51 | ["yarn", "add", "react", "react-dom"], cwd=app_path 52 | ) 53 | 54 | setup_desk_page_for_framework("react", page_doc, app_name) 55 | 56 | 57 | def setup_desk_page_for_framework(framework, page_doc, app_name): 58 | bundle_type = "js" if framework == "vue" else "jsx" 59 | context = { 60 | "pascal_cased_name": page_doc.name.replace("-", " ").title().replace(" ", ""), 61 | "scrubbed_name": page_doc.name.replace("-", "_"), 62 | "page_title": page_doc.title, 63 | "page_name": page_doc.name, 64 | "bundle_type": bundle_type, 65 | } 66 | 67 | desk_page_js_file_content = frappe.render_template( 68 | DESK_PAGE_JS_TEMPLATE, context 69 | ) 70 | desk_page_js_bundle_file_content = frappe.render_template( 71 | DESK_PAGE_JS_BUNDLE_TEMPLATE_VUE 72 | if framework == "vue" 73 | else DESK_PAGE_JSX_BUNDLE_TEMPLATE_REACT, 74 | context, 75 | ) 76 | 77 | # module 78 | module = frappe.get_module_list(app_name)[0] 79 | js_file_path = os.path.join( 80 | frappe.get_module_path(module), 81 | scrub(page_doc.doctype), 82 | scrub(page_doc.name), 83 | scrub(page_doc.name) + ".js", 84 | ) 85 | js_bundle_file_path = os.path.join( 86 | frappe.get_app_path(app_name), 87 | "public", 88 | "js", 89 | scrub(page_doc.name), 90 | scrub(page_doc.name) + f".bundle.{bundle_type}", 91 | ) 92 | 93 | with Path(js_file_path).open("w") as f: 94 | f.write(desk_page_js_file_content) 95 | 96 | # create dir if not exists 97 | Path(js_bundle_file_path).parent.mkdir(parents=True, exist_ok=True) 98 | with Path(js_bundle_file_path).open("w") as f: 99 | f.write(desk_page_js_bundle_file_content) 100 | 101 | app_component_file_name = "App.vue" if framework == "vue" else "App.jsx" 102 | app_component_path = os.path.join( 103 | frappe.get_app_path(app_name), 104 | "public", 105 | "js", 106 | scrub(page_doc.name), 107 | app_component_file_name, 108 | ) 109 | 110 | app_component_path_relative = str( 111 | app_name / Path(app_component_path).relative_to(frappe.get_app_path(app_name)) 112 | ) 113 | 114 | app_component_template = None 115 | if framework == "vue": 116 | app_component_template = DESK_PAGE_VUE_APP_COMPONENT_BOILERPLATE 117 | else: 118 | app_component_template = DESK_PAGE_REACT_APP_COMPONENT_BOILERPLATE 119 | 120 | with Path(app_component_path).open("w") as f: 121 | app_component_template = frappe.render_template(app_component_template, { 122 | "app_component_path": app_component_path_relative, 123 | }) 124 | f.write(app_component_template) 125 | 126 | from frappe.build import bundle 127 | 128 | bundle("development", apps=app_name) 129 | 130 | 131 | def create_page_doc(page_name, app_name, site): 132 | module_name = frappe.get_all( 133 | "Module Def", 134 | filters={"app_name": app_name}, 135 | limit=1, 136 | pluck="name", 137 | order_by="creation", 138 | ) 139 | 140 | if module_name: 141 | module_name = module_name[0] 142 | else: 143 | message = click.style( 144 | f"Make sure {app_name} is installed on the site {site}", fg="yellow" 145 | ) 146 | click.echo(message) 147 | return 148 | 149 | # create page doc 150 | page = frappe.new_doc("Page") 151 | page.module = module_name 152 | page.standard = "Yes" 153 | page.page_name = page_name 154 | page.title = page_name 155 | page.insert() 156 | 157 | frappe.db.commit() 158 | return page 159 | 160 | 161 | def launch_desk_page_in_browser(page, site): 162 | click.echo(f"Opening {page.title} in browser...") 163 | page_url = f"{frappe.utils.get_site_url(site)}/app/{page.name}" 164 | click.launch(page_url) 165 | click.echo( 166 | click.style( 167 | "Restart your bench to enable auto-reload of custom page on changes.", 168 | fg="yellow", 169 | ) 170 | ) 171 | -------------------------------------------------------------------------------- /doppio/commands/spa_generator.py: -------------------------------------------------------------------------------- 1 | import click 2 | import subprocess 3 | 4 | from pathlib import Path 5 | from .boilerplates import * 6 | from .utils import ( 7 | create_file, 8 | add_commands_to_root_package_json, 9 | add_routing_rule_to_hooks, 10 | ) 11 | 12 | 13 | class SPAGenerator: 14 | def __init__(self, framework, spa_name, app, add_tailwindcss, typescript): 15 | """Initialize a new SPAGenerator instance""" 16 | self.framework = framework 17 | self.app = app 18 | self.app_path = Path("../apps") / app 19 | self.spa_name = spa_name 20 | self.spa_path: Path = self.app_path / self.spa_name 21 | self.add_tailwindcss = add_tailwindcss 22 | self.use_typescript = typescript 23 | 24 | self.validate_spa_name() 25 | 26 | def validate_spa_name(self): 27 | if self.spa_name == self.app: 28 | click.echo("Dashboard name must not be same as app name", err=True, color=True) 29 | exit(1) 30 | 31 | def generate_spa(self): 32 | click.echo("Generating spa...") 33 | if self.framework == "vue": 34 | self.initialize_vue_vite_project() 35 | self.link_controller_files() 36 | self.setup_proxy_options() 37 | self.setup_vue_vite_config() 38 | self.setup_vue_router() 39 | self.create_vue_files() 40 | 41 | elif self.framework == "react": 42 | self.initialize_react_vite_project() 43 | self.setup_proxy_options() 44 | self.setup_react_vite_config() 45 | self.create_react_files() 46 | 47 | # Common to all frameworks 48 | add_commands_to_root_package_json(self.app, self.spa_name) 49 | self.create_www_directory() 50 | self.add_csrf_to_html() 51 | 52 | if self.add_tailwindcss: 53 | self.setup_tailwindcss() 54 | 55 | add_routing_rule_to_hooks(self.app, self.spa_name) 56 | 57 | click.echo(f"Run: cd {self.spa_path.absolute().resolve()} && npm run dev") 58 | click.echo("to start the development server and visit: http://:8080") 59 | 60 | def setup_tailwindcss(self): 61 | # TODO: Convert to yarn command 62 | # npm install -D tailwindcss@latest postcss@latest autoprefixer@latest 63 | subprocess.run( 64 | [ 65 | "npm", 66 | "install", 67 | "-D", 68 | "tailwindcss@latest", 69 | "postcss@latest", 70 | "autoprefixer@latest", 71 | ], 72 | cwd=self.spa_path, 73 | ) 74 | 75 | # npx tailwindcss init -p 76 | subprocess.run(["npx", "tailwindcss", "init", "-p"], cwd=self.spa_path) 77 | 78 | # Create an index.css file 79 | index_css_path: Path = self.spa_path / "src/index.css" 80 | 81 | # Add boilerplate code 82 | INDEX_CSS_BOILERPLATE = """@tailwind base; 83 | @tailwind components; 84 | @tailwind utilities; 85 | """ 86 | 87 | create_file(index_css_path, INDEX_CSS_BOILERPLATE) 88 | 89 | # Populate content property in tailwind config file 90 | # the extension of config can be .js or .ts, so we need to check for both 91 | tailwind_config_path: Path = self.spa_path / "tailwind.config.js" 92 | if not tailwind_config_path.exists(): 93 | tailwind_config_path = self.spa_path / "tailwind.config.ts" 94 | 95 | tailwind_config_path: Path = self.spa_path / "tailwind.config.js" 96 | tailwind_config = tailwind_config_path.read_text() 97 | tailwind_config = tailwind_config.replace( 98 | "content: [],", 'content: ["./src/**/*.{html,jsx,tsx,vue,js,ts}"],' 99 | ) 100 | tailwind_config_path.write_text(tailwind_config) 101 | 102 | def create_vue_files(self): 103 | app_vue = self.spa_path / "src/App.vue" 104 | create_file(app_vue, APP_VUE_BOILERPLATE) 105 | 106 | views_dir: Path = self.spa_path / "src/views" 107 | if not views_dir.exists(): 108 | views_dir.mkdir() 109 | 110 | home_vue = views_dir / "Home.vue" 111 | login_vue = views_dir / "Login.vue" 112 | 113 | create_file(home_vue, HOME_VUE_BOILERPLATE) 114 | create_file(login_vue, LOGIN_VUE_BOILERPLATE) 115 | 116 | def setup_vue_router(self): 117 | # Setup vue router 118 | router_dir_path: Path = self.spa_path / "src/router" 119 | 120 | # Create router directory 121 | router_dir_path.mkdir() 122 | 123 | # Create files 124 | router_index_file = router_dir_path / "index.js" 125 | create_file( 126 | router_index_file, ROUTER_INDEX_BOILERPLATE.replace("{{name}}", self.spa_name) 127 | ) 128 | 129 | auth_routes_file = router_dir_path / "auth.js" 130 | create_file(auth_routes_file, AUTH_ROUTES_BOILERPLATE) 131 | 132 | def initialize_vue_vite_project(self): 133 | # Run "yarn create vite {name} --template vue" 134 | print("Scafolding vue project...") 135 | if self.use_typescript: 136 | subprocess.run( 137 | ["yarn", "create", "vite", self.spa_name, "--template", "vue-ts"], cwd=self.app_path 138 | ) 139 | else: 140 | subprocess.run( 141 | ["yarn", "create", "vite", self.spa_name, "--template", "vue"], cwd=self.app_path 142 | ) 143 | 144 | # Install router and other npm packages 145 | # yarn add vue-router@4 socket.io-client@4.5.1 146 | print("Installing dependencies...") 147 | subprocess.run( 148 | ["yarn", "add", "vue-router@^4", "socket.io-client@^4.5.1"], cwd=self.spa_path 149 | ) 150 | 151 | def link_controller_files(self): 152 | # Link controller files in main.js/main.ts 153 | print("Linking controller files...") 154 | main_js: Path = self.app_path / ( 155 | f"{self.spa_name}/src/main.ts" 156 | if self.use_typescript 157 | else f"{self.spa_name}/src/main.js" 158 | ) 159 | 160 | if main_js.exists(): 161 | with main_js.open("w") as f: 162 | boilerplate = MAIN_JS_BOILERPLATE 163 | 164 | # Add css import 165 | if self.add_tailwindcss: 166 | boilerplate = "import './index.css';\n" + boilerplate 167 | 168 | f.write(boilerplate) 169 | else: 170 | click.echo("src/main.js not found!") 171 | return 172 | 173 | def setup_proxy_options(self): 174 | # Setup proxy options file 175 | proxy_options_file: Path = self.spa_path / ( 176 | "proxyOptions.ts" if self.use_typescript else "proxyOptions.js" 177 | ) 178 | create_file(proxy_options_file, PROXY_OPTIONS_BOILERPLATE) 179 | 180 | def setup_vue_vite_config(self): 181 | vite_config_file: Path = self.spa_path / ( 182 | "vite.config.ts" if self.use_typescript else "vite.config.js" 183 | ) 184 | if not vite_config_file.exists(): 185 | vite_config_file.touch() 186 | with vite_config_file.open("w") as f: 187 | boilerplate = VUE_VITE_CONFIG_BOILERPLATE.replace("{{app}}", self.app) 188 | boilerplate = boilerplate.replace("{{name}}", self.spa_name) 189 | f.write(boilerplate) 190 | 191 | def create_www_directory(self): 192 | www_dir_path: Path = self.app_path / f"{self.app}/www" 193 | 194 | if not www_dir_path.exists(): 195 | www_dir_path.mkdir() 196 | 197 | def add_csrf_to_html(self): 198 | index_html_file_path = self.spa_path / "index.html" 199 | with index_html_file_path.open("r") as f: 200 | current_html = f.read() 201 | 202 | # For attaching CSRF Token 203 | updated_html = current_html.replace( 204 | "", "\n\t\t" 205 | ) 206 | 207 | with index_html_file_path.open("w") as f: 208 | f.write(updated_html) 209 | 210 | def initialize_react_vite_project(self): 211 | # Run "yarn create vite {name} --template react" 212 | print("Scaffolding React project...") 213 | if self.use_typescript: 214 | subprocess.run( 215 | ["yarn", "create", "vite", self.spa_name, "--template", "react-ts"], 216 | cwd=self.app_path, 217 | ) 218 | else: 219 | subprocess.run( 220 | ["yarn", "create", "vite", self.spa_name, "--template", "react"], cwd=self.app_path 221 | ) 222 | 223 | # Install router and other npm packages 224 | # yarn add frappe-react-sdk socket.io-client@4.5.1 225 | print("Installing dependencies...") 226 | subprocess.run( 227 | ["yarn", "add", "frappe-react-sdk"], cwd=self.spa_path 228 | ) 229 | 230 | def setup_react_vite_config(self): 231 | vite_config_file: Path = self.spa_path / ( 232 | "vite.config.ts" if self.use_typescript else "vite.config.js" 233 | ) 234 | if not vite_config_file.exists(): 235 | vite_config_file.touch() 236 | with vite_config_file.open("w") as f: 237 | boilerplate = REACT_VITE_CONFIG_BOILERPLATE.replace("{{app}}", self.app) 238 | boilerplate = boilerplate.replace("{{name}}", self.spa_name) 239 | f.write(boilerplate) 240 | 241 | def create_react_files(self): 242 | app_react = self.spa_path / ("src/App.tsx" if self.use_typescript else "src/App.jsx") 243 | create_file(app_react, APP_REACT_BOILERPLATE) 244 | -------------------------------------------------------------------------------- /doppio/commands/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import subprocess 4 | from pathlib import Path 5 | 6 | 7 | def create_file(path: Path, content: str = None): 8 | # Create the file if not exists 9 | if not path.exists(): 10 | path.touch() 11 | 12 | # Write the contents (if any) 13 | if content: 14 | with path.open("w") as f: 15 | f.write(content) 16 | 17 | 18 | def add_commands_to_root_package_json(app, spa_name): 19 | app_path = Path("../apps") / app 20 | spa_path: Path = app_path / spa_name 21 | package_json_path: Path = spa_path / "package.json" 22 | 23 | if not package_json_path.exists(): 24 | print("package.json not found. Please manually update the build command.") 25 | return 26 | 27 | data = {} 28 | with package_json_path.open("r") as f: 29 | data = json.load(f) 30 | 31 | data["scripts"][ 32 | "build" 33 | ] = f"vite build --base=/assets/{app}/{spa_name}/ && yarn copy-html-entry" 34 | 35 | data["scripts"]["copy-html-entry"] = ( 36 | f"cp ../{app}/public/{spa_name}/index.html" f" ../{app}/www/{spa_name}.html" 37 | ) 38 | 39 | with package_json_path.open("w") as f: 40 | json.dump(data, f, indent=2) 41 | 42 | # Update app's package.json 43 | app_package_json_path: Path = app_path / "package.json" 44 | 45 | if not app_package_json_path.exists(): 46 | subprocess.run(["npm", "init", "--yes"], cwd=app_path) 47 | 48 | data = {} 49 | with app_package_json_path.open("r") as f: 50 | data = json.load(f) 51 | 52 | data["scripts"]["postinstall"] = f"cd {spa_name} && yarn install" 53 | data["scripts"]["dev"] = f"cd {spa_name} && yarn dev" 54 | data["scripts"]["build"] = f"cd {spa_name} && yarn build" 55 | 56 | with app_package_json_path.open("w") as f: 57 | json.dump(data, f, indent=2) 58 | 59 | 60 | def add_routing_rule_to_hooks(app, spa_name): 61 | hooks_py = Path(f"../apps/{app}/{app}") / "hooks.py" 62 | hooks = "" 63 | with hooks_py.open("r") as f: 64 | hooks = f.read() 65 | 66 | pattern = re.compile(r"website_route_rules\s?=\s?\[(.+)\]") 67 | 68 | rule = ( 69 | "{" + f"'from_route': '/{spa_name}/', 'to_route': '{spa_name}'" + "}" 70 | ) 71 | 72 | rules = pattern.sub(r"website_route_rules = [{rule}, \1]", hooks) 73 | 74 | # If rule is not already defined 75 | if not pattern.search(hooks): 76 | rules = hooks + "\nwebsite_route_rules = [{rule},]" 77 | 78 | updates_hooks = rules.replace("{rule}", rule) 79 | with hooks_py.open("w") as f: 80 | f.write(updates_hooks) 81 | -------------------------------------------------------------------------------- /doppio/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NagariaHussain/doppio/0eb1a177334be50980e63174c173ad934830f28b/doppio/config/__init__.py -------------------------------------------------------------------------------- /doppio/config/desktop.py: -------------------------------------------------------------------------------- 1 | from frappe import _ 2 | 3 | def get_data(): 4 | return [ 5 | { 6 | "module_name": "Doppio", 7 | "color": "grey", 8 | "icon": "octicon octicon-file-directory", 9 | "type": "module", 10 | "label": _("Doppio") 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /doppio/config/docs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration for docs 3 | """ 4 | 5 | # source_link = "https://github.com/[org_name]/doppio" 6 | # headline = "App that does everything" 7 | # sub_heading = "Yes, you got that right the first time, everything" 8 | 9 | def get_context(context): 10 | context.brand_html = "Doppio" 11 | -------------------------------------------------------------------------------- /doppio/doppio/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NagariaHussain/doppio/0eb1a177334be50980e63174c173ad934830f28b/doppio/doppio/__init__.py -------------------------------------------------------------------------------- /doppio/hooks.py: -------------------------------------------------------------------------------- 1 | from . import __version__ as app_version 2 | 3 | app_name = "doppio" 4 | app_title = "Doppio" 5 | app_publisher = "Hussain Nagaria" 6 | app_description = "A dream." 7 | app_icon = "octicon octicon-file-directory" 8 | app_color = "grey" 9 | app_email = "hussainbhaitech@gmail.com" 10 | app_license = "MIT" 11 | 12 | # Includes in 13 | # ------------------ 14 | 15 | # include js, css files in header of desk.html 16 | # app_include_css = "/assets/doppio/css/doppio.css" 17 | # app_include_js = "/assets/doppio/js/doppio.js" 18 | 19 | # include js, css files in header of web template 20 | # web_include_css = "/assets/doppio/css/doppio.css" 21 | # web_include_js = "/assets/doppio/js/doppio.js" 22 | 23 | # include custom scss in every website theme (without file extension ".scss") 24 | # website_theme_scss = "doppio/public/scss/website" 25 | 26 | # include js, css files in header of web form 27 | # webform_include_js = {"doctype": "public/js/doctype.js"} 28 | # webform_include_css = {"doctype": "public/css/doctype.css"} 29 | 30 | # include js in page 31 | # page_js = {"page" : "public/js/file.js"} 32 | 33 | # include js in doctype views 34 | # doctype_js = {"doctype" : "public/js/doctype.js"} 35 | # doctype_list_js = {"doctype" : "public/js/doctype_list.js"} 36 | # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} 37 | # doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"} 38 | 39 | # Home Pages 40 | # ---------- 41 | 42 | # application home page (will override Website Settings) 43 | # home_page = "login" 44 | 45 | # website user home page (by Role) 46 | # role_home_page = { 47 | # "Role": "home_page" 48 | # } 49 | 50 | # Generators 51 | # ---------- 52 | 53 | # automatically create page for each record of this doctype 54 | # website_generators = ["Web Page"] 55 | 56 | website_route_rules = [ 57 | {"from_route": "/vision/", "to_route": "vision"}, 58 | ] 59 | 60 | # Jinja 61 | # ---------- 62 | 63 | # add methods and filters to jinja environment 64 | # jinja = { 65 | # "methods": "doppio.utils.jinja_methods", 66 | # "filters": "doppio.utils.jinja_filters" 67 | # } 68 | 69 | # Installation 70 | # ------------ 71 | 72 | # before_install = "doppio.install.before_install" 73 | # after_install = "doppio.install.after_install" 74 | 75 | # Desk Notifications 76 | # ------------------ 77 | # See frappe.core.notifications.get_notification_config 78 | 79 | # notification_config = "doppio.notifications.get_notification_config" 80 | 81 | # Permissions 82 | # ----------- 83 | # Permissions evaluated in scripted ways 84 | 85 | # permission_query_conditions = { 86 | # "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions", 87 | # } 88 | # 89 | # has_permission = { 90 | # "Event": "frappe.desk.doctype.event.event.has_permission", 91 | # } 92 | 93 | # DocType Class 94 | # --------------- 95 | # Override standard doctype classes 96 | 97 | # override_doctype_class = { 98 | # "ToDo": "custom_app.overrides.CustomToDo" 99 | # } 100 | 101 | # Document Events 102 | # --------------- 103 | # Hook on document methods and events 104 | 105 | # doc_events = { 106 | # "*": { 107 | # "on_update": "method", 108 | # "on_cancel": "method", 109 | # "on_trash": "method" 110 | # } 111 | # } 112 | 113 | # Scheduled Tasks 114 | # --------------- 115 | 116 | # scheduler_events = { 117 | # "all": [ 118 | # "doppio.tasks.all" 119 | # ], 120 | # "daily": [ 121 | # "doppio.tasks.daily" 122 | # ], 123 | # "hourly": [ 124 | # "doppio.tasks.hourly" 125 | # ], 126 | # "weekly": [ 127 | # "doppio.tasks.weekly" 128 | # ], 129 | # "monthly": [ 130 | # "doppio.tasks.monthly" 131 | # ], 132 | # } 133 | 134 | # Testing 135 | # ------- 136 | 137 | # before_tests = "doppio.install.before_tests" 138 | 139 | # Overriding Methods 140 | # ------------------------------ 141 | # 142 | # override_whitelisted_methods = { 143 | # "frappe.desk.doctype.event.event.get_events": "doppio.event.get_events" 144 | # } 145 | # 146 | # each overriding function accepts a `data` argument; 147 | # generated from the base implementation of the doctype dashboard, 148 | # along with any modifications made in other Frappe apps 149 | # override_doctype_dashboards = { 150 | # "Task": "doppio.task.get_dashboard_data" 151 | # } 152 | 153 | # exempt linked doctypes from being automatically cancelled 154 | # 155 | # auto_cancel_exempted_doctypes = ["Auto Repeat"] 156 | 157 | 158 | # User Data Protection 159 | # -------------------- 160 | 161 | # user_data_fields = [ 162 | # { 163 | # "doctype": "{doctype_1}", 164 | # "filter_by": "{filter_by}", 165 | # "redact_fields": ["{field_1}", "{field_2}"], 166 | # "partial": 1, 167 | # }, 168 | # { 169 | # "doctype": "{doctype_2}", 170 | # "filter_by": "{filter_by}", 171 | # "partial": 1, 172 | # }, 173 | # { 174 | # "doctype": "{doctype_3}", 175 | # "strict": False, 176 | # }, 177 | # { 178 | # "doctype": "{doctype_4}" 179 | # } 180 | # ] 181 | 182 | # Authentication and authorization 183 | # -------------------------------- 184 | 185 | # auth_hooks = [ 186 | # "doppio.auth.validate" 187 | # ] 188 | -------------------------------------------------------------------------------- /doppio/modules.txt: -------------------------------------------------------------------------------- 1 | Doppio -------------------------------------------------------------------------------- /doppio/patches.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NagariaHussain/doppio/0eb1a177334be50980e63174c173ad934830f28b/doppio/patches.txt -------------------------------------------------------------------------------- /doppio/templates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NagariaHussain/doppio/0eb1a177334be50980e63174c173ad934830f28b/doppio/templates/__init__.py -------------------------------------------------------------------------------- /doppio/templates/pages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NagariaHussain/doppio/0eb1a177334be50980e63174c173ad934830f28b/doppio/templates/pages/__init__.py -------------------------------------------------------------------------------- /doppio/tests/test_spa_generation.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from unittest import TestCase 3 | from doppio.commands.spa_generator import SPAGenerator 4 | from doppio.commands import add_frappe_ui_starter 5 | 6 | 7 | class TestSPAGeneration(TestCase): 8 | def setUp(self): 9 | # create ../apps/fake_app directory 10 | self.app_path = Path("../apps/fake_app") 11 | self.app_path.mkdir(parents=True, mode=0o755, exist_ok=True) 12 | 13 | self.app_path.joinpath("fake_app/www").mkdir(parents=True, mode=0o755, exist_ok=True) 14 | self.app_path.joinpath("fake_app/public").mkdir(parents=True, mode=0o755, exist_ok=True) 15 | # create a file hooks.py inside ../apps/fake_app 16 | self.app_path.joinpath("fake_app/hooks.py").touch(exist_ok=True) 17 | 18 | def test_generate_spa_core(self): 19 | spa_generator = SPAGenerator("vue", "dashboard", "fake_app", False, False) 20 | spa_generator.generate_spa() 21 | 22 | # check if the dashboard directory was created 23 | self.assertTrue(self.app_path.joinpath("dashboard").exists()) 24 | 25 | # check if the dashboard directory contains the correct files 26 | self.assertTrue(self.app_path.joinpath("dashboard").joinpath("src").exists()) 27 | self.assertTrue(self.app_path.joinpath("dashboard").joinpath("src").joinpath("main.js").exists()) 28 | self.assertTrue(self.app_path.joinpath("dashboard").joinpath("src").joinpath("App.vue").exists()) 29 | 30 | # check if package.json has correct build command 31 | package_json = self.app_path.joinpath("dashboard").joinpath("package.json") 32 | self.assertTrue(package_json.exists()) 33 | self.assertTrue('"build": "vite build --base=/assets/fake_app/dashboard/ && yarn copy-html-entry"' in package_json.read_text()) 34 | 35 | # check if hooks.py has correct list website_route_rules 36 | hooks_py = self.app_path.joinpath("fake_app").joinpath("hooks.py") 37 | self.assertTrue(hooks_py.exists()) 38 | self.assertTrue("{'from_route': '/dashboard/', 'to_route': 'dashboard'}" in hooks_py.read_text()) 39 | 40 | def test_add_frappe_ui(self): 41 | """Tests if add_frappe_ui_starter function works as expected""" 42 | # run the command 43 | add_frappe_ui_starter("frontend", "fake_app") 44 | 45 | # check if the frontend directory was created 46 | self.assertTrue(self.app_path.joinpath("frontend").exists()) 47 | 48 | # check if the frontend directory contains the correct files 49 | self.assertTrue(self.app_path.joinpath("frontend").joinpath("src").exists()) 50 | self.assertTrue(self.app_path.joinpath("frontend").joinpath("src").joinpath("main.js").exists()) 51 | self.assertTrue(self.app_path.joinpath("frontend").joinpath("src").joinpath("App.vue").exists()) 52 | 53 | # check if package.json has correct build command 54 | package_json = self.app_path.joinpath("frontend").joinpath("package.json") 55 | self.assertTrue(package_json.exists()) 56 | self.assertTrue('"build": "vite build --base=/assets/fake_app/frontend/ && yarn copy-html-entry"' in package_json.read_text()) 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /libs/controllers/auth.js: -------------------------------------------------------------------------------- 1 | import call from './call'; 2 | 3 | export default class Auth { 4 | constructor() { 5 | this.isLoggedIn = false; 6 | this.user = null; 7 | this.user_image = null; 8 | this.cookie = null; 9 | 10 | this.cookie = Object.fromEntries( 11 | document.cookie 12 | .split('; ') 13 | .map((part) => part.split('=')) 14 | .map((d) => [d[0], decodeURIComponent(d[1])]) 15 | ); 16 | 17 | this.isLoggedIn = this.cookie.user_id && this.cookie.user_id !== 'Guest'; 18 | } 19 | 20 | async login(email, password) { 21 | let res = await call('login', { 22 | usr: email, 23 | pwd: password, 24 | }); 25 | if (res) { 26 | this.isLoggedIn = true; 27 | return res; 28 | } 29 | return false; 30 | } 31 | 32 | async logout() { 33 | await call('logout'); 34 | this.isLoggedIn = false; 35 | window.location.reload(); 36 | } 37 | 38 | async resetPassword(email) { 39 | console.log('resetting password'); 40 | // Implement if you want 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /libs/controllers/call.js: -------------------------------------------------------------------------------- 1 | // Author: Gavin D'souza 2 | 3 | import router from '@/router'; 4 | 5 | export default async function call(method, args) { 6 | if (!args) { 7 | args = {}; 8 | } 9 | 10 | let headers = { 11 | Accept: 'application/json', 12 | 'Content-Type': 'application/json; charset=utf-8', 13 | 'X-Frappe-Site-Name': window.location.hostname 14 | }; 15 | 16 | if (window.csrf_token && window.csrf_token !== '{{ csrf_token }}') { 17 | headers['X-Frappe-CSRF-Token'] = window.csrf_token; 18 | } 19 | 20 | updateState(this, 'RequestStarted', null); 21 | 22 | const res = await fetch(`/api/method/${method}`, { 23 | method: 'POST', 24 | headers, 25 | body: JSON.stringify(args) 26 | }); 27 | 28 | if (res.ok) { 29 | updateState(this, null, null); 30 | const data = await res.json(); 31 | if (data.docs || method === 'login') { 32 | return data; 33 | } 34 | return data.message; 35 | } else { 36 | let response = await res.text(); 37 | let error, exception; 38 | try { 39 | error = JSON.parse(response); 40 | // eslint-disable-next-line no-empty 41 | } catch (e) {} 42 | let errorParts = [ 43 | [method, error.exc_type, error._error_message].filter(Boolean).join(' ') 44 | ]; 45 | if (error.exc) { 46 | exception = error.exc; 47 | try { 48 | exception = JSON.parse(exception)[0]; 49 | // eslint-disable-next-line no-empty 50 | } catch (e) {} 51 | errorParts.push(exception); 52 | } 53 | let e = new Error(errorParts.join('\n')); 54 | e.exc_type = error.exc_type; 55 | e.exc = exception; 56 | e.messages = error._server_messages 57 | ? JSON.parse(error._server_messages) 58 | : []; 59 | e.messages = e.messages.concat(error.message); 60 | e.messages = e.messages.map(m => { 61 | try { 62 | return JSON.parse(m).message; 63 | } catch (error) { 64 | return m; 65 | } 66 | }); 67 | e.messages = e.messages.filter(Boolean); 68 | if (!e.messages.length) { 69 | e.messages = error._error_message ? [error._error_message] : ['Internal Server Error']; 70 | } 71 | updateState(this, null, e.messages.join('\n')); 72 | 73 | if ( 74 | [401, 403].includes(res.status) && 75 | router.currentRoute.name !== 'Login' 76 | ) { 77 | router.push('/login'); 78 | } 79 | throw e; 80 | } 81 | 82 | function updateState(vm, state, errorMessage) { 83 | if (vm?.state !== undefined) { 84 | vm.state = state; 85 | } 86 | if (vm?.errorMessage !== undefined) { 87 | vm.errorMessage = errorMessage; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /libs/controllers/socket.js: -------------------------------------------------------------------------------- 1 | // Authors: Faris Ansari 2 | 3 | import io from 'socket.io-client'; 4 | 5 | let host = window.location.hostname; 6 | let port = window.location.port ? ':9000' : ''; 7 | let protocol = port ? 'http' : 'https'; 8 | let url = `${protocol}://${host}${port}`; 9 | let socket = io(url); 10 | 11 | export default socket; -------------------------------------------------------------------------------- /libs/resourceManager/ResourceManager.js: -------------------------------------------------------------------------------- 1 | // Authors: Faris Ansari & Hussain Nagaria 2 | 3 | import call from '../controllers/call'; 4 | import { ref, reactive } from 'vue'; 5 | 6 | export default class ResourceManager { 7 | constructor(vm, resourceDefs) { 8 | this._vm = vm; 9 | this._watchers = []; 10 | let resources = reactive({}); 11 | 12 | for (let key in resourceDefs) { 13 | let resourceDef = resourceDefs[key]; 14 | if (typeof resourceDef === 'function') { 15 | this._watchers.push([ 16 | () => resourceDef.call(vm), 17 | (n, o) => this.updateResource(key, n, o), 18 | { 19 | immediate: true, 20 | deep: true, 21 | flush: 'sync', 22 | }, 23 | ]); 24 | } else { 25 | let resource = new Resource(vm, resourceDef); 26 | resources[key] = ref(resource); 27 | 28 | if (resource.auto) { 29 | resource.reload(); 30 | } 31 | } 32 | } 33 | this.resources = resources; 34 | } 35 | 36 | init() { 37 | this._watchers = this._watchers.map((w) => this._vm.$watch(...w)); 38 | } 39 | 40 | destroy() { 41 | const vm = this._vm; 42 | delete vm._rm; 43 | } 44 | 45 | updateResource(key, newValue, oldValue) { 46 | let resource; 47 | if (key in this.resources) { 48 | resource = this.resources[key]; 49 | } else { 50 | resource = reactive(new Resource(this._vm, newValue)); 51 | this.resources[key] = resource; 52 | } 53 | 54 | let oldData = resource.data; 55 | 56 | // cancel existing fetches 57 | if (oldValue && resource) { 58 | resource.cancel(); 59 | } 60 | 61 | resource.update(newValue); 62 | // keep data if it is needed between refreshes 63 | if (resource.keepData) { 64 | resource.data = oldData; 65 | } 66 | 67 | if (resource.auto) { 68 | resource.reload(); 69 | } 70 | } 71 | } 72 | 73 | class Resource { 74 | constructor(vm, options = {}) { 75 | if (typeof options == 'string') { 76 | options = { method: options, auto: true }; 77 | } 78 | if (!options.method) { 79 | throw new Error( 80 | '[Resource Manager]: method is required to define a resource' 81 | ); 82 | } 83 | this._vm = vm; 84 | this.method = options.method; 85 | this.delay = options.delay || 0; 86 | this.update(options); 87 | } 88 | 89 | update(options) { 90 | if (typeof options == 'string') { 91 | options = { method: options, auto: true }; 92 | } 93 | if (this.method && options.method && options.method !== this.method) { 94 | throw new Error( 95 | '[Resource Manager]: method cannot change for the same resource' 96 | ); 97 | } 98 | this.options = options; 99 | // params 100 | this.params = options.params || null; 101 | this.auto = options.auto || false; 102 | this.keepData = options.keepData || false; 103 | this.condition = options.condition || (() => true); 104 | this.paged = options.paged || false; 105 | this.validate = options.validate || null; 106 | if (this.validate) { 107 | this.validate = this.validate.bind(this._vm); 108 | } 109 | 110 | // events 111 | this.listeners = Object.create(null); 112 | this.onceListeners = Object.create(null); 113 | let listenerKeys = Object.keys(options).filter((key) => 114 | key.startsWith('on') 115 | ); 116 | if (listenerKeys.length > 0) { 117 | for (const key of listenerKeys) { 118 | this.on(key, options[key]); 119 | } 120 | } 121 | 122 | this.reset(); 123 | } 124 | 125 | async fetch(params) { 126 | if (!this.condition()) return; 127 | 128 | this.loading = true; 129 | this.currentParams = params || this.params; 130 | 131 | if (this.validate) { 132 | let message = await this.validate(); 133 | if (message) { 134 | this.setError(message); 135 | this.loading = false; 136 | return; 137 | } 138 | } 139 | 140 | try { 141 | let data = await call(this.method, this.currentParams); 142 | if (this.delay) { 143 | // artificial delay 144 | await new Promise((resolve) => setTimeout(resolve, this.delay * 1000)); 145 | } 146 | if (Array.isArray(data) && this.paged) { 147 | this.lastPageEmpty = data.length === 0; 148 | this.data = [].concat(this.data || [], data); 149 | } else { 150 | this.data = data; 151 | } 152 | this.emit('Success', this.data); 153 | } catch (error) { 154 | let errorMessages = error.messages || ['Internal Server Error']; 155 | this.setError(errorMessages.join('\n')); 156 | } 157 | this.lastLoaded = new Date(); 158 | this.loading = false; 159 | this.currentParams = null; 160 | } 161 | 162 | reload() { 163 | return this.fetch(); 164 | } 165 | 166 | submit(params) { 167 | return this.fetch(params); 168 | } 169 | 170 | reset() { 171 | this.data = this.options.default || null; 172 | this.error = null; 173 | this.loading = false; 174 | this.lastLoaded = null; 175 | this.lastPageEmpty = false; 176 | this.currentParams = null; 177 | } 178 | 179 | cancel() {} 180 | 181 | setError(error) { 182 | this.error = error; 183 | this.emit('Error', this.error); 184 | } 185 | 186 | on(event, handler) { 187 | this.listeners[event] = (this.listeners[event] || []).concat(handler); 188 | return this; 189 | } 190 | 191 | once(event, handler) { 192 | this.onceListeners[event] = (this.onceListeners[event] || []).concat( 193 | handler 194 | ); 195 | return this; 196 | } 197 | 198 | emit(event, ...args) { 199 | let key = 'on' + event; 200 | let vm = this._vm; 201 | 202 | (this.listeners[key] || []).forEach((handler) => { 203 | runHandler(handler); 204 | }); 205 | (this.onceListeners[key] || []).forEach((handler) => { 206 | runHandler(handler); 207 | // remove listener after calling handler 208 | this.onceListeners[key].splice( 209 | this.onceListeners[key].indexOf(handler), 210 | 1 211 | ); 212 | }); 213 | 214 | function runHandler(handler) { 215 | try { 216 | handler.call(vm, ...args); 217 | } catch (error) { 218 | console.error(error); 219 | } 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /libs/resourceManager/index.js: -------------------------------------------------------------------------------- 1 | // Authors: Faris Ansari & Hussain Nagaria 2 | 3 | import ResourceManager from './ResourceManager'; 4 | import { reactive } from 'vue'; 5 | 6 | let plugin = { 7 | beforeCreate() { 8 | const vmOptions = this.$options; 9 | if (!vmOptions.resources || vmOptions._rm) return; 10 | 11 | let resourceManager; 12 | if (typeof vmOptions.resources === 'function') { 13 | vmOptions.resources = vmOptions.resources.call(this); 14 | } 15 | 16 | if (isPlainObject(vmOptions.resources)) { 17 | const { $options, ...resourceDefs } = vmOptions.resources; 18 | resourceManager = new ResourceManager(this, resourceDefs); 19 | } else { 20 | throw new Error( 21 | '[ResourceManager]: resources options should be an object or a function that returns object' 22 | ); 23 | } 24 | 25 | if (!Object.prototype.hasOwnProperty.call(this, '$resources')) { 26 | this.$resources = reactive(resourceManager.resources); 27 | } 28 | 29 | Object.keys(vmOptions.resources).forEach((key) => { 30 | if ( 31 | !( 32 | hasKey(vmOptions.computed, key) || 33 | hasKey(vmOptions.props, key) || 34 | hasKey(vmOptions.methods, key) 35 | ) 36 | ) { 37 | if (!vmOptions.computed) { 38 | vmOptions.computed = {}; 39 | } 40 | vmOptions.computed[key] = vmOptions.resources[key]; 41 | } 42 | }); 43 | 44 | this._rm = resourceManager; 45 | }, 46 | data() { 47 | if (!this._rm) return {}; 48 | return { 49 | $rm: this._rm, 50 | $r: this._rm.resources, 51 | $resources: this._rm.resources, 52 | }; 53 | }, 54 | created() { 55 | if (!this._rm) return; 56 | this._rm.init(); 57 | }, 58 | }; 59 | 60 | export default function install(app) { 61 | app.mixin(plugin); 62 | } 63 | 64 | function isPlainObject(value) { 65 | return ( 66 | typeof value === 'object' && 67 | value && 68 | Object.prototype.toString(value) === '[object Object]' 69 | ); 70 | } 71 | 72 | function hasKey(object, key) { 73 | return key in (object || {}); 74 | } 75 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2022] [Md.Hussain Nagaria] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "doppio", 3 | "version": "0.0.1", 4 | "description": "A dream.", 5 | "main": "index.js", 6 | "scripts": {}, 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/NagariaHussain/doppio.git" 10 | }, 11 | "keywords": [ 12 | "doppio" 13 | ], 14 | "author": "Hussain Nagaria", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/NagariaHussain/doppio/issues" 18 | }, 19 | "homepage": "https://github.com/NagariaHussain/doppio#readme", 20 | "devDependencies": { 21 | "vite": "^5.4.10" 22 | }, 23 | "dependencies": { 24 | "socket.io-client": "^4.8.1", 25 | "vue": "^3.5.12" 26 | } 27 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # frappe -- https://github.com/frappe/frappe is installed via 'bench init' 2 | Click~=8.2.0 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open('requirements.txt') as f: 4 | install_requires = f.read().strip().split('\n') 5 | 6 | # get version from __version__ variable in doppio/__init__.py 7 | from doppio import __version__ as version 8 | 9 | setup( 10 | name='doppio', 11 | version=version, 12 | description='A dream.', 13 | author='Hussain Nagaria', 14 | author_email='hussainbhaitech@gmail.com', 15 | packages=find_packages(), 16 | zip_safe=False, 17 | include_package_data=True, 18 | install_requires=install_requires 19 | ) 20 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@babel/helper-string-parser@^7.25.9": 6 | version "7.25.9" 7 | resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" 8 | integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== 9 | 10 | "@babel/helper-validator-identifier@^7.25.9": 11 | version "7.25.9" 12 | resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" 13 | integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== 14 | 15 | "@babel/parser@^7.25.3": 16 | version "7.26.2" 17 | resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.2.tgz#fd7b6f487cfea09889557ef5d4eeb9ff9a5abd11" 18 | integrity sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ== 19 | dependencies: 20 | "@babel/types" "^7.26.0" 21 | 22 | "@babel/types@^7.26.0": 23 | version "7.26.0" 24 | resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.0.tgz#deabd08d6b753bc8e0f198f8709fb575e31774ff" 25 | integrity sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA== 26 | dependencies: 27 | "@babel/helper-string-parser" "^7.25.9" 28 | "@babel/helper-validator-identifier" "^7.25.9" 29 | 30 | "@esbuild/aix-ppc64@0.21.5": 31 | version "0.21.5" 32 | resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" 33 | integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== 34 | 35 | "@esbuild/android-arm64@0.21.5": 36 | version "0.21.5" 37 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" 38 | integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== 39 | 40 | "@esbuild/android-arm@0.21.5": 41 | version "0.21.5" 42 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" 43 | integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== 44 | 45 | "@esbuild/android-x64@0.21.5": 46 | version "0.21.5" 47 | resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" 48 | integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== 49 | 50 | "@esbuild/darwin-arm64@0.21.5": 51 | version "0.21.5" 52 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" 53 | integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== 54 | 55 | "@esbuild/darwin-x64@0.21.5": 56 | version "0.21.5" 57 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" 58 | integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== 59 | 60 | "@esbuild/freebsd-arm64@0.21.5": 61 | version "0.21.5" 62 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" 63 | integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== 64 | 65 | "@esbuild/freebsd-x64@0.21.5": 66 | version "0.21.5" 67 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" 68 | integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== 69 | 70 | "@esbuild/linux-arm64@0.21.5": 71 | version "0.21.5" 72 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" 73 | integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== 74 | 75 | "@esbuild/linux-arm@0.21.5": 76 | version "0.21.5" 77 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" 78 | integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== 79 | 80 | "@esbuild/linux-ia32@0.21.5": 81 | version "0.21.5" 82 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" 83 | integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== 84 | 85 | "@esbuild/linux-loong64@0.21.5": 86 | version "0.21.5" 87 | resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" 88 | integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== 89 | 90 | "@esbuild/linux-mips64el@0.21.5": 91 | version "0.21.5" 92 | resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" 93 | integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== 94 | 95 | "@esbuild/linux-ppc64@0.21.5": 96 | version "0.21.5" 97 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" 98 | integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== 99 | 100 | "@esbuild/linux-riscv64@0.21.5": 101 | version "0.21.5" 102 | resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" 103 | integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== 104 | 105 | "@esbuild/linux-s390x@0.21.5": 106 | version "0.21.5" 107 | resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" 108 | integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== 109 | 110 | "@esbuild/linux-x64@0.21.5": 111 | version "0.21.5" 112 | resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" 113 | integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== 114 | 115 | "@esbuild/netbsd-x64@0.21.5": 116 | version "0.21.5" 117 | resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" 118 | integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== 119 | 120 | "@esbuild/openbsd-x64@0.21.5": 121 | version "0.21.5" 122 | resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" 123 | integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== 124 | 125 | "@esbuild/sunos-x64@0.21.5": 126 | version "0.21.5" 127 | resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" 128 | integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== 129 | 130 | "@esbuild/win32-arm64@0.21.5": 131 | version "0.21.5" 132 | resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" 133 | integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== 134 | 135 | "@esbuild/win32-ia32@0.21.5": 136 | version "0.21.5" 137 | resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" 138 | integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== 139 | 140 | "@esbuild/win32-x64@0.21.5": 141 | version "0.21.5" 142 | resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" 143 | integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== 144 | 145 | "@jridgewell/sourcemap-codec@^1.5.0": 146 | version "1.5.0" 147 | resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" 148 | integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== 149 | 150 | "@rollup/rollup-android-arm-eabi@4.24.3": 151 | version "4.24.3" 152 | resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.3.tgz#49a2a9808074f2683667992aa94b288e0b54fc82" 153 | integrity sha512-ufb2CH2KfBWPJok95frEZZ82LtDl0A6QKTa8MoM+cWwDZvVGl5/jNb79pIhRvAalUu+7LD91VYR0nwRD799HkQ== 154 | 155 | "@rollup/rollup-android-arm64@4.24.3": 156 | version "4.24.3" 157 | resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.3.tgz#197e3bc01c228d3c23591e0fcedca91f8f398ec1" 158 | integrity sha512-iAHpft/eQk9vkWIV5t22V77d90CRofgR2006UiCjHcHJFVI1E0oBkQIAbz+pLtthFw3hWEmVB4ilxGyBf48i2Q== 159 | 160 | "@rollup/rollup-darwin-arm64@4.24.3": 161 | version "4.24.3" 162 | resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.3.tgz#16772c0309d0dc3cca716580cdac7a1c560ddf46" 163 | integrity sha512-QPW2YmkWLlvqmOa2OwrfqLJqkHm7kJCIMq9kOz40Zo9Ipi40kf9ONG5Sz76zszrmIZZ4hgRIkez69YnTHgEz1w== 164 | 165 | "@rollup/rollup-darwin-x64@4.24.3": 166 | version "4.24.3" 167 | resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.3.tgz#4e98120a1c4cda7d4043ccce72347cee53784140" 168 | integrity sha512-KO0pN5x3+uZm1ZXeIfDqwcvnQ9UEGN8JX5ufhmgH5Lz4ujjZMAnxQygZAVGemFWn+ZZC0FQopruV4lqmGMshow== 169 | 170 | "@rollup/rollup-freebsd-arm64@4.24.3": 171 | version "4.24.3" 172 | resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.24.3.tgz#27145e414986e216e0d9b9a8d488028f33c39566" 173 | integrity sha512-CsC+ZdIiZCZbBI+aRlWpYJMSWvVssPuWqrDy/zi9YfnatKKSLFCe6fjna1grHuo/nVaHG+kiglpRhyBQYRTK4A== 174 | 175 | "@rollup/rollup-freebsd-x64@4.24.3": 176 | version "4.24.3" 177 | resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.24.3.tgz#67e75fd87a903090f038b212273c492e5ca6b32f" 178 | integrity sha512-F0nqiLThcfKvRQhZEzMIXOQG4EeX61im61VYL1jo4eBxv4aZRmpin6crnBJQ/nWnCsjH5F6J3W6Stdm0mBNqBg== 179 | 180 | "@rollup/rollup-linux-arm-gnueabihf@4.24.3": 181 | version "4.24.3" 182 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.3.tgz#bb45ebadbb9496298ab5461373bde357e8f33e88" 183 | integrity sha512-KRSFHyE/RdxQ1CSeOIBVIAxStFC/hnBgVcaiCkQaVC+EYDtTe4X7z5tBkFyRoBgUGtB6Xg6t9t2kulnX6wJc6A== 184 | 185 | "@rollup/rollup-linux-arm-musleabihf@4.24.3": 186 | version "4.24.3" 187 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.3.tgz#384276c23feb0a4d6ffa603a9a760decce8b4118" 188 | integrity sha512-h6Q8MT+e05zP5BxEKz0vi0DhthLdrNEnspdLzkoFqGwnmOzakEHSlXfVyA4HJ322QtFy7biUAVFPvIDEDQa6rw== 189 | 190 | "@rollup/rollup-linux-arm64-gnu@4.24.3": 191 | version "4.24.3" 192 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.3.tgz#89e5a4570ddd9eca908324a6de60bd64f904e3f0" 193 | integrity sha512-fKElSyXhXIJ9pqiYRqisfirIo2Z5pTTve5K438URf08fsypXrEkVmShkSfM8GJ1aUyvjakT+fn2W7Czlpd/0FQ== 194 | 195 | "@rollup/rollup-linux-arm64-musl@4.24.3": 196 | version "4.24.3" 197 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.3.tgz#9ffd7cd6c6c6670d8c039056d6a49ad9f1f66949" 198 | integrity sha512-YlddZSUk8G0px9/+V9PVilVDC6ydMz7WquxozToozSnfFK6wa6ne1ATUjUvjin09jp34p84milxlY5ikueoenw== 199 | 200 | "@rollup/rollup-linux-powerpc64le-gnu@4.24.3": 201 | version "4.24.3" 202 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.3.tgz#4d32ce982e2d25e3b8116336ad5ce6e270b5a024" 203 | integrity sha512-yNaWw+GAO8JjVx3s3cMeG5Esz1cKVzz8PkTJSfYzE5u7A+NvGmbVFEHP+BikTIyYWuz0+DX9kaA3pH9Sqxp69g== 204 | 205 | "@rollup/rollup-linux-riscv64-gnu@4.24.3": 206 | version "4.24.3" 207 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.3.tgz#f43d4e0572397e3d3acd82d77d79ce021dea3310" 208 | integrity sha512-lWKNQfsbpv14ZCtM/HkjCTm4oWTKTfxPmr7iPfp3AHSqyoTz5AgLemYkWLwOBWc+XxBbrU9SCokZP0WlBZM9lA== 209 | 210 | "@rollup/rollup-linux-s390x-gnu@4.24.3": 211 | version "4.24.3" 212 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.3.tgz#264f8a4c206173945bdab2a676d638b7945106a9" 213 | integrity sha512-HoojGXTC2CgCcq0Woc/dn12wQUlkNyfH0I1ABK4Ni9YXyFQa86Fkt2Q0nqgLfbhkyfQ6003i3qQk9pLh/SpAYw== 214 | 215 | "@rollup/rollup-linux-x64-gnu@4.24.3": 216 | version "4.24.3" 217 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.3.tgz#e86172a407b2edd41540ec2ae636e497fadccff6" 218 | integrity sha512-mnEOh4iE4USSccBOtcrjF5nj+5/zm6NcNhbSEfR3Ot0pxBwvEn5QVUXcuOwwPkapDtGZ6pT02xLoPaNv06w7KQ== 219 | 220 | "@rollup/rollup-linux-x64-musl@4.24.3": 221 | version "4.24.3" 222 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.3.tgz#8ae9bf78986d1b16ccbc89ab6f2dfa96807d3178" 223 | integrity sha512-rMTzawBPimBQkG9NKpNHvquIUTQPzrnPxPbCY1Xt+mFkW7pshvyIS5kYgcf74goxXOQk0CP3EoOC1zcEezKXhw== 224 | 225 | "@rollup/rollup-win32-arm64-msvc@4.24.3": 226 | version "4.24.3" 227 | resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.3.tgz#11d6a59f651a3c2a9e5eaab0a99367b77a29c319" 228 | integrity sha512-2lg1CE305xNvnH3SyiKwPVsTVLCg4TmNCF1z7PSHX2uZY2VbUpdkgAllVoISD7JO7zu+YynpWNSKAtOrX3AiuA== 229 | 230 | "@rollup/rollup-win32-ia32-msvc@4.24.3": 231 | version "4.24.3" 232 | resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.3.tgz#7ff146e53dc6e388b60329b7ec3335501d2b0f98" 233 | integrity sha512-9SjYp1sPyxJsPWuhOCX6F4jUMXGbVVd5obVpoVEi8ClZqo52ViZewA6eFz85y8ezuOA+uJMP5A5zo6Oz4S5rVQ== 234 | 235 | "@rollup/rollup-win32-x64-msvc@4.24.3": 236 | version "4.24.3" 237 | resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.3.tgz#7687335781efe6bee14d6ed8eff9746a9f24c9cd" 238 | integrity sha512-HGZgRFFYrMrP3TJlq58nR1xy8zHKId25vhmm5S9jETEfDf6xybPxsavFTJaufe2zgOGYJBskGlj49CwtEuFhWQ== 239 | 240 | "@socket.io/component-emitter@~3.1.0": 241 | version "3.1.2" 242 | resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2" 243 | integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== 244 | 245 | "@types/estree@1.0.6": 246 | version "1.0.6" 247 | resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" 248 | integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== 249 | 250 | "@vue/compiler-core@3.5.12": 251 | version "3.5.12" 252 | resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.12.tgz#bd70b7dabd12b0b6f31bc53418ba3da77994c437" 253 | integrity sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw== 254 | dependencies: 255 | "@babel/parser" "^7.25.3" 256 | "@vue/shared" "3.5.12" 257 | entities "^4.5.0" 258 | estree-walker "^2.0.2" 259 | source-map-js "^1.2.0" 260 | 261 | "@vue/compiler-dom@3.5.12": 262 | version "3.5.12" 263 | resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.12.tgz#456d631d11102535b7ee6fd954cf2c93158d0354" 264 | integrity sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg== 265 | dependencies: 266 | "@vue/compiler-core" "3.5.12" 267 | "@vue/shared" "3.5.12" 268 | 269 | "@vue/compiler-sfc@3.5.12": 270 | version "3.5.12" 271 | resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.12.tgz#6688120d905fcf22f7e44d3cb90f8dabc4dd3cc8" 272 | integrity sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw== 273 | dependencies: 274 | "@babel/parser" "^7.25.3" 275 | "@vue/compiler-core" "3.5.12" 276 | "@vue/compiler-dom" "3.5.12" 277 | "@vue/compiler-ssr" "3.5.12" 278 | "@vue/shared" "3.5.12" 279 | estree-walker "^2.0.2" 280 | magic-string "^0.30.11" 281 | postcss "^8.4.47" 282 | source-map-js "^1.2.0" 283 | 284 | "@vue/compiler-ssr@3.5.12": 285 | version "3.5.12" 286 | resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.12.tgz#5f1a3fbd5c44b79a6dbe88729f7801d9c9218bde" 287 | integrity sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA== 288 | dependencies: 289 | "@vue/compiler-dom" "3.5.12" 290 | "@vue/shared" "3.5.12" 291 | 292 | "@vue/reactivity@3.5.12": 293 | version "3.5.12" 294 | resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.12.tgz#a2815d91842ed7b9e7e7936c851923caf6b6e603" 295 | integrity sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg== 296 | dependencies: 297 | "@vue/shared" "3.5.12" 298 | 299 | "@vue/runtime-core@3.5.12": 300 | version "3.5.12" 301 | resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.5.12.tgz#849207f203d0fd82971f19574d30dbe7134c78c7" 302 | integrity sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw== 303 | dependencies: 304 | "@vue/reactivity" "3.5.12" 305 | "@vue/shared" "3.5.12" 306 | 307 | "@vue/runtime-dom@3.5.12": 308 | version "3.5.12" 309 | resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.5.12.tgz#6d4de3df49a90a460b311b1100baa5e2d0d1c8c9" 310 | integrity sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA== 311 | dependencies: 312 | "@vue/reactivity" "3.5.12" 313 | "@vue/runtime-core" "3.5.12" 314 | "@vue/shared" "3.5.12" 315 | csstype "^3.1.3" 316 | 317 | "@vue/server-renderer@3.5.12": 318 | version "3.5.12" 319 | resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.5.12.tgz#79c6bc3860e4e4ef80d85653c5d03fd94b26574e" 320 | integrity sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg== 321 | dependencies: 322 | "@vue/compiler-ssr" "3.5.12" 323 | "@vue/shared" "3.5.12" 324 | 325 | "@vue/shared@3.5.12": 326 | version "3.5.12" 327 | resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.12.tgz#f9e45b7f63f2c3f40d84237b1194b7f67de192e3" 328 | integrity sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg== 329 | 330 | csstype@^3.1.3: 331 | version "3.1.3" 332 | resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" 333 | integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== 334 | 335 | debug@~4.3.1, debug@~4.3.2: 336 | version "4.3.7" 337 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" 338 | integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== 339 | dependencies: 340 | ms "^2.1.3" 341 | 342 | engine.io-client@~6.6.1: 343 | version "6.6.2" 344 | resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.6.2.tgz#e0a09e1c90effe5d6264da1c56d7281998f1e50b" 345 | integrity sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw== 346 | dependencies: 347 | "@socket.io/component-emitter" "~3.1.0" 348 | debug "~4.3.1" 349 | engine.io-parser "~5.2.1" 350 | ws "~8.17.1" 351 | xmlhttprequest-ssl "~2.1.1" 352 | 353 | engine.io-parser@~5.2.1: 354 | version "5.2.3" 355 | resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f" 356 | integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q== 357 | 358 | entities@^4.5.0: 359 | version "4.5.0" 360 | resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" 361 | integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== 362 | 363 | esbuild@^0.21.3: 364 | version "0.21.5" 365 | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" 366 | integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== 367 | optionalDependencies: 368 | "@esbuild/aix-ppc64" "0.21.5" 369 | "@esbuild/android-arm" "0.21.5" 370 | "@esbuild/android-arm64" "0.21.5" 371 | "@esbuild/android-x64" "0.21.5" 372 | "@esbuild/darwin-arm64" "0.21.5" 373 | "@esbuild/darwin-x64" "0.21.5" 374 | "@esbuild/freebsd-arm64" "0.21.5" 375 | "@esbuild/freebsd-x64" "0.21.5" 376 | "@esbuild/linux-arm" "0.21.5" 377 | "@esbuild/linux-arm64" "0.21.5" 378 | "@esbuild/linux-ia32" "0.21.5" 379 | "@esbuild/linux-loong64" "0.21.5" 380 | "@esbuild/linux-mips64el" "0.21.5" 381 | "@esbuild/linux-ppc64" "0.21.5" 382 | "@esbuild/linux-riscv64" "0.21.5" 383 | "@esbuild/linux-s390x" "0.21.5" 384 | "@esbuild/linux-x64" "0.21.5" 385 | "@esbuild/netbsd-x64" "0.21.5" 386 | "@esbuild/openbsd-x64" "0.21.5" 387 | "@esbuild/sunos-x64" "0.21.5" 388 | "@esbuild/win32-arm64" "0.21.5" 389 | "@esbuild/win32-ia32" "0.21.5" 390 | "@esbuild/win32-x64" "0.21.5" 391 | 392 | estree-walker@^2.0.2: 393 | version "2.0.2" 394 | resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" 395 | integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== 396 | 397 | fsevents@~2.3.2: 398 | version "2.3.2" 399 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" 400 | integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== 401 | 402 | fsevents@~2.3.3: 403 | version "2.3.3" 404 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" 405 | integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== 406 | 407 | magic-string@^0.30.11: 408 | version "0.30.12" 409 | resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.12.tgz#9eb11c9d072b9bcb4940a5b2c2e1a217e4ee1a60" 410 | integrity sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw== 411 | dependencies: 412 | "@jridgewell/sourcemap-codec" "^1.5.0" 413 | 414 | ms@^2.1.3: 415 | version "2.1.3" 416 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" 417 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 418 | 419 | nanoid@^3.3.7: 420 | version "3.3.7" 421 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" 422 | integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== 423 | 424 | picocolors@^1.1.0: 425 | version "1.1.1" 426 | resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" 427 | integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== 428 | 429 | postcss@^8.4.43, postcss@^8.4.47: 430 | version "8.4.47" 431 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365" 432 | integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ== 433 | dependencies: 434 | nanoid "^3.3.7" 435 | picocolors "^1.1.0" 436 | source-map-js "^1.2.1" 437 | 438 | rollup@^4.20.0: 439 | version "4.24.3" 440 | resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.24.3.tgz#8b259063740af60b0030315f88665ba2041789b8" 441 | integrity sha512-HBW896xR5HGmoksbi3JBDtmVzWiPAYqp7wip50hjQ67JbDz61nyoMPdqu1DvVW9asYb2M65Z20ZHsyJCMqMyDg== 442 | dependencies: 443 | "@types/estree" "1.0.6" 444 | optionalDependencies: 445 | "@rollup/rollup-android-arm-eabi" "4.24.3" 446 | "@rollup/rollup-android-arm64" "4.24.3" 447 | "@rollup/rollup-darwin-arm64" "4.24.3" 448 | "@rollup/rollup-darwin-x64" "4.24.3" 449 | "@rollup/rollup-freebsd-arm64" "4.24.3" 450 | "@rollup/rollup-freebsd-x64" "4.24.3" 451 | "@rollup/rollup-linux-arm-gnueabihf" "4.24.3" 452 | "@rollup/rollup-linux-arm-musleabihf" "4.24.3" 453 | "@rollup/rollup-linux-arm64-gnu" "4.24.3" 454 | "@rollup/rollup-linux-arm64-musl" "4.24.3" 455 | "@rollup/rollup-linux-powerpc64le-gnu" "4.24.3" 456 | "@rollup/rollup-linux-riscv64-gnu" "4.24.3" 457 | "@rollup/rollup-linux-s390x-gnu" "4.24.3" 458 | "@rollup/rollup-linux-x64-gnu" "4.24.3" 459 | "@rollup/rollup-linux-x64-musl" "4.24.3" 460 | "@rollup/rollup-win32-arm64-msvc" "4.24.3" 461 | "@rollup/rollup-win32-ia32-msvc" "4.24.3" 462 | "@rollup/rollup-win32-x64-msvc" "4.24.3" 463 | fsevents "~2.3.2" 464 | 465 | socket.io-client@^4.8.1: 466 | version "4.8.1" 467 | resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.8.1.tgz#1941eca135a5490b94281d0323fe2a35f6f291cb" 468 | integrity sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ== 469 | dependencies: 470 | "@socket.io/component-emitter" "~3.1.0" 471 | debug "~4.3.2" 472 | engine.io-client "~6.6.1" 473 | socket.io-parser "~4.2.4" 474 | 475 | socket.io-parser@~4.2.4: 476 | version "4.2.4" 477 | resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" 478 | integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== 479 | dependencies: 480 | "@socket.io/component-emitter" "~3.1.0" 481 | debug "~4.3.1" 482 | 483 | source-map-js@^1.2.0, source-map-js@^1.2.1: 484 | version "1.2.1" 485 | resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" 486 | integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== 487 | 488 | vite@^5.4.10: 489 | version "5.4.10" 490 | resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.10.tgz#d358a7bd8beda6cf0f3b7a450a8c7693a4f80c18" 491 | integrity sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ== 492 | dependencies: 493 | esbuild "^0.21.3" 494 | postcss "^8.4.43" 495 | rollup "^4.20.0" 496 | optionalDependencies: 497 | fsevents "~2.3.3" 498 | 499 | vue@^3.5.12: 500 | version "3.5.12" 501 | resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.12.tgz#e08421c601b3617ea2c9ef0413afcc747130b36c" 502 | integrity sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg== 503 | dependencies: 504 | "@vue/compiler-dom" "3.5.12" 505 | "@vue/compiler-sfc" "3.5.12" 506 | "@vue/runtime-dom" "3.5.12" 507 | "@vue/server-renderer" "3.5.12" 508 | "@vue/shared" "3.5.12" 509 | 510 | ws@~8.17.1: 511 | version "8.17.1" 512 | resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" 513 | integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== 514 | 515 | xmlhttprequest-ssl@~2.1.1: 516 | version "2.1.2" 517 | resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz#e9e8023b3f29ef34b97a859f584c5e6c61418e23" 518 | integrity sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ== 519 | --------------------------------------------------------------------------------