feat: barebones MVP — editor, live LSP, and preview
Backend (typst-leaf-backend) - REST API for multi-project file management under PROJECTS_ROOT (list/create projects, get file tree, read/write files; zod-validated sandbox prevents path traversal). - LSP thin proxy: spawns a single tinymist lsp process, pipes one active WebSocket straight to its stdio via vscode-ws-jsonrpc. New connections trigger a takeover (old session disposed, fresh tinymist spawned). - Live preview pipeline: the frontend sends tinymist.startDefaultPreview over the LSP wire, reads the returned data-plane port, and relays it to the backend. The backend proxies /preview (WS, with retry and an Origin header for typst-preview) and /preview-app (HTTP, injecting a script that rewrites the webview's WS path to /preview and forces light mode). - Graceful SIGTERM/SIGINT shutdown. - Auto-seeds a hello project on first boot. - Removed execa dependency (no longer needed). Frontend (typst-leaf-frontend) - Monaco editor with typst syntax highlighting (Monarch tokenizer) and live LSP diagnostics (markers), completion, hover, definitions, and formatting — all driven by a raw JSON-RPC WebSocket client (no monaco-languageclient or vscode-api dependency). - Project picker and file tree sidebar. - Live preview pane: iframes the typst-preview webview through the backend's /preview-app proxy, so the pipedepeline works remotely. - Monaco bundled locally (no CDN) with a Vite worker setup. - Vite dev proxy so relative /api, /lsp, /preview, and /preVIEW-app URLs reach the backend on port 4000. - Light-mode UI (editor 'vs' theme, white chrome). - Prevent esbuild from tree-shaking monaco.editor services (optimizeDeps.esbuildOptions.treeShaking: false). Infrastructure - Added vscode-ws-jsonrpc to backend dependencies. - Removed monaco-languageclient and vscode-ws-jsonrpc from frontend dependencies (replaced by the raw JSON-RPC client). - Added monaco-editor as a direct frontend dependency. - Updated README (correct frontend port 1420, tinymist install command, environment variables, architecture). - Added projects/ to .gitignore. - Renamed Tauri productName and page title to typst-leaf.
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
projects/
|
||||||
|
|||||||
@@ -4,55 +4,64 @@ A completely self-hostable, high-performance editor for [Typst](https://typst.ap
|
|||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
* **Instant Preview:** Sub-50ms incremental compilation using `tinymist`.
|
* **Instant Preview:** Sub-50ms incremental compilation using `tinymist`.
|
||||||
* **Vim Mode:** First-class support via Monaco.
|
* **Live diagnostics & autocomplete** from the Typst language server.
|
||||||
* **Git Native:** No hidden databases. Your projects are just folders with `.git`.
|
* **Git Native:** No hidden databases. Your projects are just folders.
|
||||||
* **Hybrid:** Run it in Docker on your server, or as a native desktop app via Tauri.
|
* **Hybrid:** Run it in a browser, or as a native desktop app via Tauri.
|
||||||
|
|
||||||
## 📂 Repository Structure
|
## 📂 Repository Structure
|
||||||
|
|
||||||
```text
|
```text
|
||||||
typst-leaf/
|
typst-leaf/
|
||||||
├── typst-leaf-backend/ # Node.js/TS server (manages tinymist & git)
|
├── typst-leaf-backend/ # Node.js/TS server (spawns tinymist, REST + WS)
|
||||||
├── typst-leaf-frontend/ # React + Monaco frontend (includes Tauri)
|
├── typst-leaf-frontend/ # React + Monaco frontend (includes Tauri)
|
||||||
├── VISION.md # Project philosophy and architecture
|
├── VISION.md # Project philosophy and architecture
|
||||||
└── AGENTS.md # Guidelines for AI coding assistants
|
└── AGENTS.md # Guidelines for AI coding assistants
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
* **Node.js 20+**
|
||||||
|
* **pnpm** (10+)
|
||||||
|
* **Rust** (only required for the Tauri desktop wrapper)
|
||||||
|
* **`tinymist`** on your `PATH` — the Typst language server & preview engine:
|
||||||
|
```bash
|
||||||
|
cargo install --git https://github.com/Myriad-Dreamin/tinymist --bin tinymist --locked tinymist-cli
|
||||||
|
```
|
||||||
|
|
||||||
## 🚀 Quick Start (Development)
|
## 🚀 Quick Start (Development)
|
||||||
|
|
||||||
**Prerequisites:**
|
|
||||||
|
|
||||||
* Node.js 20+
|
|
||||||
* pnpm
|
|
||||||
* Rust (for Tauri)
|
|
||||||
* `tinymist` binary installed in your PATH.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Install dependencies
|
# 1. Install workspace dependencies
|
||||||
pnpm install
|
pnpm install
|
||||||
|
|
||||||
# 2. Start the backend (Port 4000)
|
# 2. Start the backend (port 4000)
|
||||||
cd typst-leaf-backend
|
pnpm --filter typst-leaf-backend dev
|
||||||
pnpm dev
|
|
||||||
|
|
||||||
# 3. Start the web frontend (Port 3000)
|
# 3. Start the web frontend (port 1420 — mandated by Tauri)
|
||||||
# (In a new terminal)
|
pnpm --filter typst-leaf-frontend dev
|
||||||
cd typst-leaf-frontend
|
|
||||||
pnpm dev
|
|
||||||
|
|
||||||
# 4. Start Tauri Desktop App (Optional)
|
|
||||||
# (In a new terminal)
|
|
||||||
cd typst-leaf-frontend
|
|
||||||
pnpm tauri dev
|
|
||||||
|
|
||||||
|
# 4. (Optional) Launch the Tauri desktop wrapper
|
||||||
|
pnpm --filter typst-leaf-frontend tauri dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Then open <http://localhost:1420> in your browser. A `hello` project is auto-seeded on first run.
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
| Var | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `PROJECTS_ROOT` | `<repo>/projects` | Directory under which project folders live. The backend refuses to read or write outside this root. |
|
||||||
|
| `TINYMIST_PREVIEW_HOST` | `127.0.0.1` | Host the tinymist preview server binds to. |
|
||||||
|
| `TINYMIST_PREVIEW_PORT` | `23635` | Port the tinymist preview server binds to. |
|
||||||
|
| `PORT` (backend) | `4000` | Backend HTTP/WS port. |
|
||||||
|
| `FRONTEND_ORIGIN` (backend) | `http://localhost:1420` | CORS origin allowed to call the REST API. |
|
||||||
|
|
||||||
## 🏗️ Architecture
|
## 🏗️ Architecture
|
||||||
|
|
||||||
* **Editor:** Monaco Editor
|
* **Editor:** Monaco Editor
|
||||||
* **LSP:** `tinymist` (via WebSocket proxy)
|
* **LSP:** `tinymist` over a WebSocket thin proxy
|
||||||
* **Communication:**
|
* **Preview:** Backend spawns `tinymist preview <file>` on demand; the frontend iframes its webview for live SVG updates.
|
||||||
* `ws://localhost:4000/lsp` -> Tinymist Language Server
|
* **Data plane:**
|
||||||
* `ws://localhost:4000/preview` -> Tinymist Preview Service
|
* `ws://localhost:4000/lsp` → `tinymist lsp` (single active connection, takeover on reconnect)
|
||||||
|
* `GET /api/projects`, `GET/PUT /api/projects/:p/file?path=…`, `POST /api/projects/:p/focus?path=…`
|
||||||
|
|
||||||
|
|||||||
Generated
+6
-615
@@ -11,15 +11,15 @@ importers:
|
|||||||
cors:
|
cors:
|
||||||
specifier: ^2.8.5
|
specifier: ^2.8.5
|
||||||
version: 2.8.6
|
version: 2.8.6
|
||||||
execa:
|
|
||||||
specifier: ^9.6.1
|
|
||||||
version: 9.6.1
|
|
||||||
express:
|
express:
|
||||||
specifier: ^4.22.2
|
specifier: ^4.22.2
|
||||||
version: 4.22.2
|
version: 4.22.2
|
||||||
simple-git:
|
simple-git:
|
||||||
specifier: ^3.27.0
|
specifier: ^3.27.0
|
||||||
version: 3.36.0
|
version: 3.36.0
|
||||||
|
vscode-ws-jsonrpc:
|
||||||
|
specifier: ^3.5.0
|
||||||
|
version: 3.5.0
|
||||||
ws:
|
ws:
|
||||||
specifier: ^8.18.2
|
specifier: ^8.18.2
|
||||||
version: 8.21.0
|
version: 8.21.0
|
||||||
@@ -57,9 +57,9 @@ importers:
|
|||||||
'@tauri-apps/plugin-opener':
|
'@tauri-apps/plugin-opener':
|
||||||
specifier: ^2
|
specifier: ^2
|
||||||
version: 2.5.4
|
version: 2.5.4
|
||||||
monaco-languageclient:
|
monaco-editor:
|
||||||
specifier: ^10.7.0
|
specifier: ^0.55.1
|
||||||
version: 10.7.0
|
version: 0.55.1
|
||||||
monaco-vim:
|
monaco-vim:
|
||||||
specifier: ^0.4.4
|
specifier: ^0.4.4
|
||||||
version: 0.4.4(monaco-editor@0.55.1)
|
version: 0.4.4(monaco-editor@0.55.1)
|
||||||
@@ -69,9 +69,6 @@ importers:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^19.1.0
|
specifier: ^19.1.0
|
||||||
version: 19.2.7(react@19.2.7)
|
version: 19.2.7(react@19.2.7)
|
||||||
vscode-ws-jsonrpc:
|
|
||||||
specifier: ^3.5.0
|
|
||||||
version: 3.5.0
|
|
||||||
zod:
|
zod:
|
||||||
specifier: ^3.24.5
|
specifier: ^3.24.5
|
||||||
version: 3.25.76
|
version: 3.25.76
|
||||||
@@ -186,132 +183,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
|
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@codingame/monaco-vscode-api@25.1.2':
|
|
||||||
resolution: {integrity: sha512-K04QcQA+Zb0KXucBAK/BGCT5dldiwIqdUbBQq7yuLvBLbof3cP1WSUuxasMHGYwM0MWyzIAsDtyAYMS7is8ZuA==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-base-service-override@25.1.2':
|
|
||||||
resolution: {integrity: sha512-OwYs6h1ATUAeMmX+Q1c8esTG7GLMqniBs+fLEr1/9b/ciY485ArKo5UvrUxVPDtRNy/7F06vRW9IUCq9iKP14w==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-bulk-edit-service-override@25.1.2':
|
|
||||||
resolution: {integrity: sha512-+EfSzjiFakCf0IIJKPZrHVGioq5N8GBsp51bXuKBR5J/B58cUaJY0Dc12PNTSpgAusAGOppUIOSBqUk4F/7IaQ==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-configuration-service-override@25.1.2':
|
|
||||||
resolution: {integrity: sha512-oeoZ3WtM42zHA1IWHrx9UGEfE+TixE+G8Bl9M9bjgFj1EROnkB5yOfELwRYPo4WOEtcK1C5nvIvWIj/hL9MaLg==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-editor-api@25.1.2':
|
|
||||||
resolution: {integrity: sha512-dVXoBLRN8vyFHsLY6iYISaNetZ3ispXLut0qL+jvN0e0CEFkUv1F/3EAE7myptrJSS/N1AptrRIxATT3lwFP+Q==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-editor-service-override@25.1.2':
|
|
||||||
resolution: {integrity: sha512-EadvDCyWdgxOPmaIvbcVVDNjTUYuKdjYWwKbPbbcTs9t4z1/DjdE7mV3ZdT6aGh5m6zkEEUOi143l27Y5eRt+Q==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-environment-service-override@25.1.2':
|
|
||||||
resolution: {integrity: sha512-8GoD3lk0CN0dIMZOrZNS/i8RCaF1YSQ6nmrf+rqneOSHG9S382EnsZZD69d4+i7JnoeyttO7Kr9KH8WOhRV6OA==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-extension-api@25.1.2':
|
|
||||||
resolution: {integrity: sha512-SJW/YOhjo+9MXEyzMwQMUWdJVR3Llc6pTq5JQqs6Y30v73gTrpLqtzbd9FNdCuQR8S6bUk5ScH8GL4QrVuL5FA==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-extensions-service-override@25.1.2':
|
|
||||||
resolution: {integrity: sha512-rTTZW2biPxcg+JumhVf2L+38C5ptvNNxiJlwz39VfXFEh6qOHtAsIMy7vIXa0uGg5/y8DNp0SnOQJP/RKhLYZA==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-files-service-override@25.1.2':
|
|
||||||
resolution: {integrity: sha512-TenLLAFIwY7keZFF8e3beUn7OVfnNINR5Noi4PVrjeeTcy6FuNH6Jghdul2JwpRAkvyJLdFMvomE2jlT6F03jQ==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-host-service-override@25.1.2':
|
|
||||||
resolution: {integrity: sha512-lgaalpA9CUQW7i0bBwgBOK0DQNDvOo3QO3p6Rz6yVsHpgA4iMqq2d11dBDUKvuQSwIHPRu8CMHCqhQk/BQN/YA==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-keybindings-service-override@25.1.2':
|
|
||||||
resolution: {integrity: sha512-cp/gGyTvCTAzCYnQm0HJykXJRB0Huz8Lvq60lj5LutgWcb8S3w6dOB2Houm8dHoeUm/jOko8SQNIP8hzWN92Zw==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-cs@25.1.2':
|
|
||||||
resolution: {integrity: sha512-v0cB2uAOCwj135aGIf0arGV+DNW32lbWh04bv8ctTxcWRt1Pr2kTQ1pjfE8ynKgxabPfAk8E25/CerKSYOmZ+A==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-de@25.1.2':
|
|
||||||
resolution: {integrity: sha512-xA3WOt1w5jlAOnyx4PBwx+qV3vx8C8/zie29qjYbgJMxGKDkb0HfpuKUwywDA2uUMI2wJZS+PnNG00zPDoLIrw==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-es@25.1.2':
|
|
||||||
resolution: {integrity: sha512-1/upuO9lRJilZ3sRr0QLTpz55KYRaBWDe8wtPvghOFYOHyWgW8A4VhUQxa6L9SJgY1JkypUAm0U8WcMX2G4LnQ==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-fr@25.1.2':
|
|
||||||
resolution: {integrity: sha512-iq+xx+tv1QIMmFD0eBhFRMF4xMAsVf/HyA1WogqBofteCWeAvRE9HUjZ5JzHz7jXBPe3dLP1LOM0r0GrJZs4fQ==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-it@25.1.2':
|
|
||||||
resolution: {integrity: sha512-FajWCML9OR8ppLnJ0mcg+sFHEhYJl8zhb3/DHnd+pNysw8dLfetXoSWjaPnwPPpwiQgkNN1UsToZHOU9czVifQ==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-ja@25.1.2':
|
|
||||||
resolution: {integrity: sha512-NwKh0BnPgUrJkxsm0X6vY4ftnd9DjxkcnQqK+bohta6UOzm09J1EjZ6QD42fjWngxrp/xiegtrYQ9NA2q6VpoA==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-ko@25.1.2':
|
|
||||||
resolution: {integrity: sha512-fvaisgfcg8YaAwnyPcGmQDLwkwqzamLQUyx9HmnwDpXw0YANzd058Kwn6bz+Vfn9MjwuMNT0nllD0qQMnpdyew==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-pl@25.1.2':
|
|
||||||
resolution: {integrity: sha512-9hDRyzFJkDia5rO9QE262JgxwP/cnalFisLFo7FQcw57ZhqzqXIdQIuwcKaHuAgzeQ6W2+A3KOLfTr3m7VZrXw==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-pt-br@25.1.2':
|
|
||||||
resolution: {integrity: sha512-7fFnqOTAJGb5RuJ4uwh9sh0JmXALuHPGOl7iL9rZkcgIuVP5y6wVDUDXq5qjiRTNSFDs7Bzh463Ir5m5D6mJbA==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-qps-ploc@25.1.2':
|
|
||||||
resolution: {integrity: sha512-IFjoqrSuPtIFWb+KlPT6PFWKszzNX+TCD9drgCV6AigvBO/xfGL3QwHB68l/DLbmDbohOz4Xdkutv20wuENAeA==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-ru@25.1.2':
|
|
||||||
resolution: {integrity: sha512-0uDAeXO+GllKUPhJzP893rlDhlFV1IwCu/515rBdcyegt48iGm/xAgj26V90hNz8hmB6EuM/7d8MFeklbiIpYA==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-tr@25.1.2':
|
|
||||||
resolution: {integrity: sha512-MJhHxDyJEiuVLQ9+jb8MnnN9lsbJOjJjMswVCeJ7v/Q/msAhq25QYUfn0DbOIzESJE1f7crffRb5e38XP8sYWA==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-zh-hans@25.1.2':
|
|
||||||
resolution: {integrity: sha512-c7MMrhnSLb59NxpAa8nVy9aIbxy4gVYrCpDMq8W380LOaXTYb7nueTrw8QJ5QbJBNi2P2KZoGkn2BlONuBtJJg==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-zh-hant@25.1.2':
|
|
||||||
resolution: {integrity: sha512-ARedFTM6JCluoPLJqkBcTJaQFdJNcN86OX6B8/NMApIPrnSIAfanMndpyilt8XjzUG6IH22cypR+DAlEjf48cA==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-languages-service-override@25.1.2':
|
|
||||||
resolution: {integrity: sha512-ipuS1V3NgXDkNrj0vBcgMBFnqo+19HVsZjjFGfPFH3x0uptP9aiWWK42wtDK3Qbu4teSjHL7WnSLrmw94rplWw==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-layout-service-override@25.1.2':
|
|
||||||
resolution: {integrity: sha512-SxBGcMK3RgkGtUn7ZDl7dCoyNW0CWFQ/bfSRYUY06A0IA4JNS5jq1lhof57d0WXewm+5l8w1Spr/vMsfx1c9ig==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-localization-service-override@25.1.2':
|
|
||||||
resolution: {integrity: sha512-QLj62A8XDOIQW3KjsZlNxs+sfsNNHYxWMjQMwZu/y2Vw3IIHGly2Lpn4t4SFbeaBHJQJy4i5s7NpzlbF9MbEzQ==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-log-service-override@25.1.2':
|
|
||||||
resolution: {integrity: sha512-OoileAUtPAJ0j3RW31DFSxtOipy0EcFq+iIXEdGvoRlsQPZJ3o9ayjf1JvCXpxUjJ3QkmvQVhXsWNUFREjEFLg==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-model-service-override@25.1.2':
|
|
||||||
resolution: {integrity: sha512-MGz/eV1CxibLvnl6WzK6idUHJCXJOVepJvKM6Trkv5050vRe+f/o1TjCiG8PaznAypYqZvnwkTG0B7/OTizCpQ==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-monarch-service-override@25.1.2':
|
|
||||||
resolution: {integrity: sha512-akyNHOJQRS7YHyk6kf0Encnkt+shlR+bIB84UJRUHFgSeF8s5gkDkQuFJph0YeUDWJWat+yBLUSZx2nHomdbHQ==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-quickaccess-service-override@25.1.2':
|
|
||||||
resolution: {integrity: sha512-7IIrXnwHiF3w9d9p9kspEUz/LCibMLUztmRpGdZQfFtWBJw043q7rk8V1O42KdXr1hVg9IR5vfffwjy9nbiiUg==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-textmate-service-override@25.1.2':
|
|
||||||
resolution: {integrity: sha512-AL0FtSQBW+1vtoXYQvUqB2hfWojpK73Kq/n6KuNXxjLF/XBJ5FpeeZDfrBfwhWPPoHuBTsaFUCQy4L8xQgbVlA==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-theme-defaults-default-extension@25.1.2':
|
|
||||||
resolution: {integrity: sha512-0vTMFiC89YSDSmjFckuQBUKwRuFNtsILNO3k0PBiSLN/MW+VDItjJpiVLXC42+rUWlGgY2lYxOneGVa5slCV1w==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-theme-service-override@25.1.2':
|
|
||||||
resolution: {integrity: sha512-hsTwl6YYTiheFuQMmCmiEGLIdIdgYaf8Z85XWyxe6YgPtDaYGnp0fGSOXKA9/bf0JtuynzoLKtUUfDupK/A7Tw==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-view-banner-service-override@25.1.2':
|
|
||||||
resolution: {integrity: sha512-zhujHd1PQ6rRXsC2OQGrx/282G2v3lpPFl9heDFGKzpdj5119SgcW+B9p/MwJ1qF3LJpuRRgefNiQtqC/KT1eA==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-view-common-service-override@25.1.2':
|
|
||||||
resolution: {integrity: sha512-4Po/YaHUvVf4VmhVCZmM2lc/flOptiWSM140bIRNpMcfH0VwihYg15CcDeu1Oc+6DaauzsG3u59GtEvlMmJ9Zw==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-view-status-bar-service-override@25.1.2':
|
|
||||||
resolution: {integrity: sha512-Jp9ytLaWZ6evabTPtG3Mu3dFx+7WTIPz69BsGpl9PnU0kiSWUqQhPSob0Jz7E2qmMj0ZcNv2Wqvm6bMBu5OyrA==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-view-title-bar-service-override@25.1.2':
|
|
||||||
resolution: {integrity: sha512-NVYtTAFR35NV/Fx7tSlbASicvpAjK5A14fmxF7/LJJN8ZmzhA/P3Y+UzhqOQl6/VcPV4pAMU0Z7Sicgwbn37dw==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-views-service-override@25.1.2':
|
|
||||||
resolution: {integrity: sha512-LfzlztsvobdP5L5EvJ/rqSEgy5fEVmrkMqRteuhEtNGd4hnmdBoX8W7BNMBPff6d4NfCK74pGHJF57RyT4Iixg==}
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-workbench-service-override@25.1.2':
|
|
||||||
resolution: {integrity: sha512-2LMHr+na03FhOAaXpIGmamq9hf7e4wt2kULn8NqNZRd3i+0v1tx/TSSjGhsA5EkrNrFD7CMSoXayBq8tgpCq/A==}
|
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.28.1':
|
'@esbuild/aix-ppc64@0.28.1':
|
||||||
resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
|
resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -641,19 +512,12 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@sec-ant/readable-stream@0.4.1':
|
|
||||||
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
|
|
||||||
|
|
||||||
'@simple-git/args-pathspec@1.0.3':
|
'@simple-git/args-pathspec@1.0.3':
|
||||||
resolution: {integrity: sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==}
|
resolution: {integrity: sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==}
|
||||||
|
|
||||||
'@simple-git/argv-parser@1.1.1':
|
'@simple-git/argv-parser@1.1.1':
|
||||||
resolution: {integrity: sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==}
|
resolution: {integrity: sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==}
|
||||||
|
|
||||||
'@sindresorhus/merge-streams@4.0.0':
|
|
||||||
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
'@tailwindcss/node@4.3.2':
|
'@tailwindcss/node@4.3.2':
|
||||||
resolution: {integrity: sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==}
|
resolution: {integrity: sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==}
|
||||||
|
|
||||||
@@ -904,9 +768,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||||
|
|
||||||
'@vscode/iconv-lite-umd@0.7.1':
|
|
||||||
resolution: {integrity: sha512-tK6k0DXFHW7q5+GGuGZO+phpAqpxO4WXl+BLc/8/uOk3RsM2ssAL3CQUQDb1TGfwltjsauhN6S4ghYZzs4sPFw==}
|
|
||||||
|
|
||||||
accepts@1.3.8:
|
accepts@1.3.8:
|
||||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -914,9 +775,6 @@ packages:
|
|||||||
array-flatten@1.1.1:
|
array-flatten@1.1.1:
|
||||||
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
|
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
|
||||||
|
|
||||||
balanced-match@1.0.2:
|
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
|
||||||
|
|
||||||
baseline-browser-mapping@2.10.40:
|
baseline-browser-mapping@2.10.40:
|
||||||
resolution: {integrity: sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==}
|
resolution: {integrity: sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
@@ -926,9 +784,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==}
|
resolution: {integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==}
|
||||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||||
|
|
||||||
brace-expansion@2.1.1:
|
|
||||||
resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==}
|
|
||||||
|
|
||||||
browserslist@4.28.4:
|
browserslist@4.28.4:
|
||||||
resolution: {integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==}
|
resolution: {integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==}
|
||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
@@ -971,10 +826,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
|
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
|
||||||
engines: {node: '>= 8'}
|
|
||||||
|
|
||||||
csstype@3.2.3:
|
csstype@3.2.3:
|
||||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||||
|
|
||||||
@@ -1010,9 +861,6 @@ packages:
|
|||||||
dompurify@3.2.7:
|
dompurify@3.2.7:
|
||||||
resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==}
|
resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==}
|
||||||
|
|
||||||
dompurify@3.3.1:
|
|
||||||
resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==}
|
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1059,10 +907,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
execa@9.6.1:
|
|
||||||
resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==}
|
|
||||||
engines: {node: ^18.19.0 || >=20.5.0}
|
|
||||||
|
|
||||||
express@4.22.2:
|
express@4.22.2:
|
||||||
resolution: {integrity: sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==}
|
resolution: {integrity: sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==}
|
||||||
engines: {node: '>= 0.10.0'}
|
engines: {node: '>= 0.10.0'}
|
||||||
@@ -1076,10 +920,6 @@ packages:
|
|||||||
picomatch:
|
picomatch:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
figures@6.1.0:
|
|
||||||
resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
finalhandler@1.3.2:
|
finalhandler@1.3.2:
|
||||||
resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==}
|
resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -1112,10 +952,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
get-stream@9.0.1:
|
|
||||||
resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
gopd@1.2.0:
|
gopd@1.2.0:
|
||||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1135,10 +971,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
|
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
human-signals@8.0.1:
|
|
||||||
resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
|
|
||||||
engines: {node: '>=18.18.0'}
|
|
||||||
|
|
||||||
iconv-lite@0.4.24:
|
iconv-lite@0.4.24:
|
||||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -1150,21 +982,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
is-plain-obj@4.1.0:
|
|
||||||
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
|
|
||||||
is-stream@4.0.1:
|
|
||||||
resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
is-unicode-supported@2.1.0:
|
|
||||||
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
isexe@2.0.0:
|
|
||||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
|
||||||
|
|
||||||
jiti@2.7.0:
|
jiti@2.7.0:
|
||||||
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
|
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -1172,10 +989,6 @@ packages:
|
|||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
jschardet@3.1.4:
|
|
||||||
resolution: {integrity: sha512-/kmVISmrwVwtyYU40iQUOp3SUPk2dhNCMsZBQX0R1/jZ8maaXJ/oZIzUOiyOqcgtLnETFKYChbJ5iDC/eWmFHg==}
|
|
||||||
engines: {node: '>=0.1.90'}
|
|
||||||
|
|
||||||
jsesc@3.1.0:
|
jsesc@3.1.0:
|
||||||
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
|
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -1299,17 +1112,9 @@ packages:
|
|||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
minimatch@5.1.9:
|
|
||||||
resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
|
|
||||||
monaco-editor@0.55.1:
|
monaco-editor@0.55.1:
|
||||||
resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==}
|
resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==}
|
||||||
|
|
||||||
monaco-languageclient@10.7.0:
|
|
||||||
resolution: {integrity: sha512-oA5cOFixkF4bspVL2zMSn48LvlNR/Cu3vJ8MCVam3PdjobSULGgHtOASuZIi3FgWK42X1z8/6hrG0LCjvNu1Hw==}
|
|
||||||
engines: {node: '>=20.10.0', npm: '>=10.2.3'}
|
|
||||||
|
|
||||||
monaco-vim@0.4.4:
|
monaco-vim@0.4.4:
|
||||||
resolution: {integrity: sha512-LNChAb//WEm/W+eyeHG/0+pdVEHotk2hLTN+M3sQZx5E8cAlSWSgqcxpcRuQnxDybSln7pfHF9i63HmbIQvrWw==}
|
resolution: {integrity: sha512-LNChAb//WEm/W+eyeHG/0+pdVEHotk2hLTN+M3sQZx5E8cAlSWSgqcxpcRuQnxDybSln7pfHF9i63HmbIQvrWw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1334,10 +1139,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==}
|
resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
npm-run-path@6.0.0:
|
|
||||||
resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
object-assign@4.1.1:
|
object-assign@4.1.1:
|
||||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -1350,22 +1151,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
parse-ms@4.0.0:
|
|
||||||
resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
parseurl@1.3.3:
|
parseurl@1.3.3:
|
||||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
path-key@3.1.1:
|
|
||||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
|
|
||||||
path-key@4.0.0:
|
|
||||||
resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
|
|
||||||
path-to-regexp@0.1.13:
|
path-to-regexp@0.1.13:
|
||||||
resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==}
|
resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==}
|
||||||
|
|
||||||
@@ -1380,10 +1169,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==}
|
resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
pretty-ms@9.3.0:
|
|
||||||
resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
proxy-addr@2.0.7:
|
proxy-addr@2.0.7:
|
||||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
@@ -1431,11 +1216,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
semver@7.8.5:
|
|
||||||
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
send@0.19.2:
|
send@0.19.2:
|
||||||
resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==}
|
resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -1447,14 +1227,6 @@ packages:
|
|||||||
setprototypeof@1.2.0:
|
setprototypeof@1.2.0:
|
||||||
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||||
|
|
||||||
shebang-command@2.0.0:
|
|
||||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
|
|
||||||
shebang-regex@3.0.0:
|
|
||||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
|
|
||||||
side-channel-list@1.0.1:
|
side-channel-list@1.0.1:
|
||||||
resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==}
|
resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1471,10 +1243,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==}
|
resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
signal-exit@4.1.0:
|
|
||||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
|
||||||
engines: {node: '>=14'}
|
|
||||||
|
|
||||||
simple-git@3.36.0:
|
simple-git@3.36.0:
|
||||||
resolution: {integrity: sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==}
|
resolution: {integrity: sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==}
|
||||||
|
|
||||||
@@ -1489,10 +1257,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
strip-final-newline@4.0.0:
|
|
||||||
resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
tailwindcss@4.3.2:
|
tailwindcss@4.3.2:
|
||||||
resolution: {integrity: sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==}
|
resolution: {integrity: sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==}
|
||||||
|
|
||||||
@@ -1525,10 +1289,6 @@ packages:
|
|||||||
undici-types@6.21.0:
|
undici-types@6.21.0:
|
||||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||||
|
|
||||||
unicorn-magic@0.3.0:
|
|
||||||
resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
unpipe@1.0.0:
|
unpipe@1.0.0:
|
||||||
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -1587,33 +1347,14 @@ packages:
|
|||||||
yaml:
|
yaml:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
vscode-jsonrpc@8.2.0:
|
|
||||||
resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==}
|
|
||||||
engines: {node: '>=14.0.0'}
|
|
||||||
|
|
||||||
vscode-jsonrpc@8.2.1:
|
vscode-jsonrpc@8.2.1:
|
||||||
resolution: {integrity: sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==}
|
resolution: {integrity: sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
vscode-languageclient@9.0.1:
|
|
||||||
resolution: {integrity: sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==}
|
|
||||||
engines: {vscode: ^1.82.0}
|
|
||||||
|
|
||||||
vscode-languageserver-protocol@3.17.5:
|
|
||||||
resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==}
|
|
||||||
|
|
||||||
vscode-languageserver-types@3.17.5:
|
|
||||||
resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==}
|
|
||||||
|
|
||||||
vscode-ws-jsonrpc@3.5.0:
|
vscode-ws-jsonrpc@3.5.0:
|
||||||
resolution: {integrity: sha512-13ZDy7Od4AfEPK2HIfY3DtyRi4FVsvFql1yobVJrpIoHOKGGJpIjVvIJpMxkrHzCZzWlYlg+WEu2hrYkCTvM0Q==}
|
resolution: {integrity: sha512-13ZDy7Od4AfEPK2HIfY3DtyRi4FVsvFql1yobVJrpIoHOKGGJpIjVvIJpMxkrHzCZzWlYlg+WEu2hrYkCTvM0Q==}
|
||||||
engines: {node: '>=20.10.0', npm: '>=10.2.3'}
|
engines: {node: '>=20.10.0', npm: '>=10.2.3'}
|
||||||
|
|
||||||
which@2.0.2:
|
|
||||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
|
||||||
engines: {node: '>= 8'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
ws@8.21.0:
|
ws@8.21.0:
|
||||||
resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
|
resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
@@ -1629,10 +1370,6 @@ packages:
|
|||||||
yallist@3.1.1:
|
yallist@3.1.1:
|
||||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||||
|
|
||||||
yoctocolors@2.1.2:
|
|
||||||
resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
zod@3.25.76:
|
zod@3.25.76:
|
||||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||||
|
|
||||||
@@ -1750,203 +1487,6 @@ snapshots:
|
|||||||
'@babel/helper-string-parser': 7.29.7
|
'@babel/helper-string-parser': 7.29.7
|
||||||
'@babel/helper-validator-identifier': 7.29.7
|
'@babel/helper-validator-identifier': 7.29.7
|
||||||
|
|
||||||
'@codingame/monaco-vscode-api@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-base-service-override': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-environment-service-override': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-extensions-service-override': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-files-service-override': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-host-service-override': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-layout-service-override': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-quickaccess-service-override': 25.1.2
|
|
||||||
'@vscode/iconv-lite-umd': 0.7.1
|
|
||||||
dompurify: 3.3.1
|
|
||||||
jschardet: 3.1.4
|
|
||||||
marked: 14.0.0
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-base-service-override@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-bulk-edit-service-override@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-configuration-service-override@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-files-service-override': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-editor-api@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-editor-service-override@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-environment-service-override@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-extension-api@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-extensions-service-override': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-extensions-service-override@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-files-service-override': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-files-service-override@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-host-service-override@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-keybindings-service-override@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-files-service-override': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-cs@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-de@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-es@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-fr@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-it@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-ja@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-ko@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-pl@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-pt-br@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-qps-ploc@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-ru@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-tr@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-zh-hans@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-language-pack-zh-hant@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-languages-service-override@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-files-service-override': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-layout-service-override@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-localization-service-override@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-log-service-override@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-environment-service-override': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-model-service-override@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-monarch-service-override@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-quickaccess-service-override@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-textmate-service-override@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-files-service-override': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-theme-defaults-default-extension@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-theme-service-override@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-files-service-override': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-view-banner-service-override@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-view-common-service-override@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-bulk-edit-service-override': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-view-status-bar-service-override@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-view-title-bar-service-override@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-views-service-override@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-keybindings-service-override': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-layout-service-override': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-quickaccess-service-override': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-view-common-service-override': 25.1.2
|
|
||||||
|
|
||||||
'@codingame/monaco-vscode-workbench-service-override@25.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-keybindings-service-override': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-quickaccess-service-override': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-view-banner-service-override': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-view-common-service-override': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-view-status-bar-service-override': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-view-title-bar-service-override': 25.1.2
|
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.28.1':
|
'@esbuild/aix-ppc64@0.28.1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -2140,16 +1680,12 @@ snapshots:
|
|||||||
'@rollup/rollup-win32-x64-msvc@4.62.2':
|
'@rollup/rollup-win32-x64-msvc@4.62.2':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@sec-ant/readable-stream@0.4.1': {}
|
|
||||||
|
|
||||||
'@simple-git/args-pathspec@1.0.3': {}
|
'@simple-git/args-pathspec@1.0.3': {}
|
||||||
|
|
||||||
'@simple-git/argv-parser@1.1.1':
|
'@simple-git/argv-parser@1.1.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@simple-git/args-pathspec': 1.0.3
|
'@simple-git/args-pathspec': 1.0.3
|
||||||
|
|
||||||
'@sindresorhus/merge-streams@4.0.0': {}
|
|
||||||
|
|
||||||
'@tailwindcss/node@4.3.2':
|
'@tailwindcss/node@4.3.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/remapping': 2.3.5
|
'@jridgewell/remapping': 2.3.5
|
||||||
@@ -2375,8 +1911,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@vscode/iconv-lite-umd@0.7.1': {}
|
|
||||||
|
|
||||||
accepts@1.3.8:
|
accepts@1.3.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
@@ -2384,8 +1918,6 @@ snapshots:
|
|||||||
|
|
||||||
array-flatten@1.1.1: {}
|
array-flatten@1.1.1: {}
|
||||||
|
|
||||||
balanced-match@1.0.2: {}
|
|
||||||
|
|
||||||
baseline-browser-mapping@2.10.40: {}
|
baseline-browser-mapping@2.10.40: {}
|
||||||
|
|
||||||
body-parser@1.20.5:
|
body-parser@1.20.5:
|
||||||
@@ -2405,10 +1937,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
brace-expansion@2.1.1:
|
|
||||||
dependencies:
|
|
||||||
balanced-match: 1.0.2
|
|
||||||
|
|
||||||
browserslist@4.28.4:
|
browserslist@4.28.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
baseline-browser-mapping: 2.10.40
|
baseline-browser-mapping: 2.10.40
|
||||||
@@ -2448,12 +1976,6 @@ snapshots:
|
|||||||
object-assign: 4.1.1
|
object-assign: 4.1.1
|
||||||
vary: 1.1.2
|
vary: 1.1.2
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
|
||||||
dependencies:
|
|
||||||
path-key: 3.1.1
|
|
||||||
shebang-command: 2.0.0
|
|
||||||
which: 2.0.2
|
|
||||||
|
|
||||||
csstype@3.2.3: {}
|
csstype@3.2.3: {}
|
||||||
|
|
||||||
debug@2.6.9:
|
debug@2.6.9:
|
||||||
@@ -2474,10 +1996,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/trusted-types': 2.0.7
|
'@types/trusted-types': 2.0.7
|
||||||
|
|
||||||
dompurify@3.3.1:
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/trusted-types': 2.0.7
|
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind-apply-helpers: 1.0.2
|
call-bind-apply-helpers: 1.0.2
|
||||||
@@ -2538,21 +2056,6 @@ snapshots:
|
|||||||
|
|
||||||
etag@1.8.1: {}
|
etag@1.8.1: {}
|
||||||
|
|
||||||
execa@9.6.1:
|
|
||||||
dependencies:
|
|
||||||
'@sindresorhus/merge-streams': 4.0.0
|
|
||||||
cross-spawn: 7.0.6
|
|
||||||
figures: 6.1.0
|
|
||||||
get-stream: 9.0.1
|
|
||||||
human-signals: 8.0.1
|
|
||||||
is-plain-obj: 4.1.0
|
|
||||||
is-stream: 4.0.1
|
|
||||||
npm-run-path: 6.0.0
|
|
||||||
pretty-ms: 9.3.0
|
|
||||||
signal-exit: 4.1.0
|
|
||||||
strip-final-newline: 4.0.0
|
|
||||||
yoctocolors: 2.1.2
|
|
||||||
|
|
||||||
express@4.22.2:
|
express@4.22.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
accepts: 1.3.8
|
accepts: 1.3.8
|
||||||
@@ -2593,10 +2096,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
|
|
||||||
figures@6.1.0:
|
|
||||||
dependencies:
|
|
||||||
is-unicode-supported: 2.1.0
|
|
||||||
|
|
||||||
finalhandler@1.3.2:
|
finalhandler@1.3.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 2.6.9
|
debug: 2.6.9
|
||||||
@@ -2638,11 +2137,6 @@ snapshots:
|
|||||||
dunder-proto: 1.0.1
|
dunder-proto: 1.0.1
|
||||||
es-object-atoms: 1.1.2
|
es-object-atoms: 1.1.2
|
||||||
|
|
||||||
get-stream@9.0.1:
|
|
||||||
dependencies:
|
|
||||||
'@sec-ant/readable-stream': 0.4.1
|
|
||||||
is-stream: 4.0.1
|
|
||||||
|
|
||||||
gopd@1.2.0: {}
|
gopd@1.2.0: {}
|
||||||
|
|
||||||
graceful-fs@4.2.11: {}
|
graceful-fs@4.2.11: {}
|
||||||
@@ -2661,8 +2155,6 @@ snapshots:
|
|||||||
statuses: 2.0.2
|
statuses: 2.0.2
|
||||||
toidentifier: 1.0.1
|
toidentifier: 1.0.1
|
||||||
|
|
||||||
human-signals@8.0.1: {}
|
|
||||||
|
|
||||||
iconv-lite@0.4.24:
|
iconv-lite@0.4.24:
|
||||||
dependencies:
|
dependencies:
|
||||||
safer-buffer: 2.1.2
|
safer-buffer: 2.1.2
|
||||||
@@ -2671,20 +2163,10 @@ snapshots:
|
|||||||
|
|
||||||
ipaddr.js@1.9.1: {}
|
ipaddr.js@1.9.1: {}
|
||||||
|
|
||||||
is-plain-obj@4.1.0: {}
|
|
||||||
|
|
||||||
is-stream@4.0.1: {}
|
|
||||||
|
|
||||||
is-unicode-supported@2.1.0: {}
|
|
||||||
|
|
||||||
isexe@2.0.0: {}
|
|
||||||
|
|
||||||
jiti@2.7.0: {}
|
jiti@2.7.0: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
jschardet@3.1.4: {}
|
|
||||||
|
|
||||||
jsesc@3.1.0: {}
|
jsesc@3.1.0: {}
|
||||||
|
|
||||||
json5@2.2.3: {}
|
json5@2.2.3: {}
|
||||||
@@ -2764,52 +2246,11 @@ snapshots:
|
|||||||
|
|
||||||
mime@1.6.0: {}
|
mime@1.6.0: {}
|
||||||
|
|
||||||
minimatch@5.1.9:
|
|
||||||
dependencies:
|
|
||||||
brace-expansion: 2.1.1
|
|
||||||
|
|
||||||
monaco-editor@0.55.1:
|
monaco-editor@0.55.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
dompurify: 3.2.7
|
dompurify: 3.2.7
|
||||||
marked: 14.0.0
|
marked: 14.0.0
|
||||||
|
|
||||||
monaco-languageclient@10.7.0:
|
|
||||||
dependencies:
|
|
||||||
'@codingame/monaco-vscode-api': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-configuration-service-override': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-editor-api': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-editor-service-override': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-extension-api': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-extensions-service-override': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-language-pack-cs': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-language-pack-de': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-language-pack-es': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-language-pack-fr': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-language-pack-it': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-language-pack-ja': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-language-pack-ko': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-language-pack-pl': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-language-pack-pt-br': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-language-pack-qps-ploc': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-language-pack-ru': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-language-pack-tr': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-language-pack-zh-hans': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-language-pack-zh-hant': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-languages-service-override': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-localization-service-override': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-log-service-override': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-model-service-override': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-monarch-service-override': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-textmate-service-override': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-theme-defaults-default-extension': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-theme-service-override': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-views-service-override': 25.1.2
|
|
||||||
'@codingame/monaco-vscode-workbench-service-override': 25.1.2
|
|
||||||
vscode: '@codingame/monaco-vscode-extension-api@25.1.2'
|
|
||||||
vscode-languageclient: 9.0.1
|
|
||||||
vscode-languageserver-protocol: 3.17.5
|
|
||||||
vscode-ws-jsonrpc: 3.5.0
|
|
||||||
|
|
||||||
monaco-vim@0.4.4(monaco-editor@0.55.1):
|
monaco-vim@0.4.4(monaco-editor@0.55.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
monaco-editor: 0.55.1
|
monaco-editor: 0.55.1
|
||||||
@@ -2824,11 +2265,6 @@ snapshots:
|
|||||||
|
|
||||||
node-releases@2.0.50: {}
|
node-releases@2.0.50: {}
|
||||||
|
|
||||||
npm-run-path@6.0.0:
|
|
||||||
dependencies:
|
|
||||||
path-key: 4.0.0
|
|
||||||
unicorn-magic: 0.3.0
|
|
||||||
|
|
||||||
object-assign@4.1.1: {}
|
object-assign@4.1.1: {}
|
||||||
|
|
||||||
object-inspect@1.13.4: {}
|
object-inspect@1.13.4: {}
|
||||||
@@ -2837,14 +2273,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ee-first: 1.1.1
|
ee-first: 1.1.1
|
||||||
|
|
||||||
parse-ms@4.0.0: {}
|
|
||||||
|
|
||||||
parseurl@1.3.3: {}
|
parseurl@1.3.3: {}
|
||||||
|
|
||||||
path-key@3.1.1: {}
|
|
||||||
|
|
||||||
path-key@4.0.0: {}
|
|
||||||
|
|
||||||
path-to-regexp@0.1.13: {}
|
path-to-regexp@0.1.13: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
@@ -2857,10 +2287,6 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
pretty-ms@9.3.0:
|
|
||||||
dependencies:
|
|
||||||
parse-ms: 4.0.0
|
|
||||||
|
|
||||||
proxy-addr@2.0.7:
|
proxy-addr@2.0.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
forwarded: 0.2.0
|
forwarded: 0.2.0
|
||||||
@@ -2928,8 +2354,6 @@ snapshots:
|
|||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
|
|
||||||
semver@7.8.5: {}
|
|
||||||
|
|
||||||
send@0.19.2:
|
send@0.19.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 2.6.9
|
debug: 2.6.9
|
||||||
@@ -2959,12 +2383,6 @@ snapshots:
|
|||||||
|
|
||||||
setprototypeof@1.2.0: {}
|
setprototypeof@1.2.0: {}
|
||||||
|
|
||||||
shebang-command@2.0.0:
|
|
||||||
dependencies:
|
|
||||||
shebang-regex: 3.0.0
|
|
||||||
|
|
||||||
shebang-regex@3.0.0: {}
|
|
||||||
|
|
||||||
side-channel-list@1.0.1:
|
side-channel-list@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
@@ -2993,8 +2411,6 @@ snapshots:
|
|||||||
side-channel-map: 1.0.1
|
side-channel-map: 1.0.1
|
||||||
side-channel-weakmap: 1.0.2
|
side-channel-weakmap: 1.0.2
|
||||||
|
|
||||||
signal-exit@4.1.0: {}
|
|
||||||
|
|
||||||
simple-git@3.36.0:
|
simple-git@3.36.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@kwsites/file-exists': 1.1.1
|
'@kwsites/file-exists': 1.1.1
|
||||||
@@ -3011,8 +2427,6 @@ snapshots:
|
|||||||
|
|
||||||
statuses@2.0.2: {}
|
statuses@2.0.2: {}
|
||||||
|
|
||||||
strip-final-newline@4.0.0: {}
|
|
||||||
|
|
||||||
tailwindcss@4.3.2: {}
|
tailwindcss@4.3.2: {}
|
||||||
|
|
||||||
tapable@2.3.3: {}
|
tapable@2.3.3: {}
|
||||||
@@ -3039,8 +2453,6 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@6.21.0: {}
|
undici-types@6.21.0: {}
|
||||||
|
|
||||||
unicorn-magic@0.3.0: {}
|
|
||||||
|
|
||||||
unpipe@1.0.0: {}
|
unpipe@1.0.0: {}
|
||||||
|
|
||||||
update-browserslist-db@1.2.3(browserslist@4.28.4):
|
update-browserslist-db@1.2.3(browserslist@4.28.4):
|
||||||
@@ -3068,35 +2480,14 @@ snapshots:
|
|||||||
lightningcss: 1.32.0
|
lightningcss: 1.32.0
|
||||||
tsx: 4.22.4
|
tsx: 4.22.4
|
||||||
|
|
||||||
vscode-jsonrpc@8.2.0: {}
|
|
||||||
|
|
||||||
vscode-jsonrpc@8.2.1: {}
|
vscode-jsonrpc@8.2.1: {}
|
||||||
|
|
||||||
vscode-languageclient@9.0.1:
|
|
||||||
dependencies:
|
|
||||||
minimatch: 5.1.9
|
|
||||||
semver: 7.8.5
|
|
||||||
vscode-languageserver-protocol: 3.17.5
|
|
||||||
|
|
||||||
vscode-languageserver-protocol@3.17.5:
|
|
||||||
dependencies:
|
|
||||||
vscode-jsonrpc: 8.2.0
|
|
||||||
vscode-languageserver-types: 3.17.5
|
|
||||||
|
|
||||||
vscode-languageserver-types@3.17.5: {}
|
|
||||||
|
|
||||||
vscode-ws-jsonrpc@3.5.0:
|
vscode-ws-jsonrpc@3.5.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
vscode-jsonrpc: 8.2.1
|
vscode-jsonrpc: 8.2.1
|
||||||
|
|
||||||
which@2.0.2:
|
|
||||||
dependencies:
|
|
||||||
isexe: 2.0.0
|
|
||||||
|
|
||||||
ws@8.21.0: {}
|
ws@8.21.0: {}
|
||||||
|
|
||||||
yallist@3.1.1: {}
|
yallist@3.1.1: {}
|
||||||
|
|
||||||
yoctocolors@2.1.2: {}
|
|
||||||
|
|
||||||
zod@3.25.76: {}
|
zod@3.25.76: {}
|
||||||
|
|||||||
@@ -12,9 +12,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"execa": "^9.6.1",
|
|
||||||
"express": "^4.22.2",
|
"express": "^4.22.2",
|
||||||
"simple-git": "^3.27.0",
|
"simple-git": "^3.27.0",
|
||||||
|
"vscode-ws-jsonrpc": "^3.5.0",
|
||||||
"ws": "^8.18.2",
|
"ws": "^8.18.2",
|
||||||
"zod": "^3.24.5"
|
"zod": "^3.24.5"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const DEFAULT_ROOT = path.resolve(process.cwd(), "..", "projects");
|
||||||
|
|
||||||
|
export const PROJECTS_ROOT = path.resolve(
|
||||||
|
process.env.PROJECTS_ROOT ?? DEFAULT_ROOT,
|
||||||
|
);
|
||||||
@@ -1,27 +1,83 @@
|
|||||||
import { createServer } from "http";
|
import cors from "cors";
|
||||||
|
import express from "express";
|
||||||
|
import { createServer } from "node:http";
|
||||||
import { WebSocketServer } from "ws";
|
import { WebSocketServer } from "ws";
|
||||||
|
import { attachLsp } from "./lsp-proxy.js";
|
||||||
|
import { attachPreview } from "./preview-proxy.js";
|
||||||
|
import { previewWebviewRouter } from "./preview-webview.js";
|
||||||
|
import { seedIfMissing } from "./seed.js";
|
||||||
|
import { filesRouter } from "./routes/files.js";
|
||||||
|
import { previewRouter } from "./routes/preview.js";
|
||||||
|
import { projectsRouter } from "./routes/projects.js";
|
||||||
|
|
||||||
const PORT = Number(process.env.PORT) || 4000;
|
const PORT = Number(process.env.PORT) || 4000;
|
||||||
|
// Allow a single origin or a comma-separated list (self-hosted behind a
|
||||||
|
// reverse proxy / multiple frontends).
|
||||||
|
const FRONTEND_ORIGINS = (process.env.FRONTEND_ORIGIN ?? "http://localhost:1420")
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
const server = createServer((_req, res) => {
|
const app = express();
|
||||||
res.writeHead(200, { "Content-Type": "application/json" });
|
app.use(cors({ origin: FRONTEND_ORIGINS }));
|
||||||
res.end(JSON.stringify({ status: "ok", service: "typst-leaf-backend" }));
|
app.use(express.json({ limit: "4mb" }));
|
||||||
|
|
||||||
|
app.get("/api/health", (_req, res) => {
|
||||||
|
res.json({ status: "ok", service: "typst-leaf-backend" });
|
||||||
});
|
});
|
||||||
|
|
||||||
const wss = new WebSocketServer({ server });
|
app.use("/api/projects", projectsRouter);
|
||||||
|
app.use("/api/projects", filesRouter);
|
||||||
|
app.use("/api/preview", previewRouter);
|
||||||
|
app.use("/preview-app", previewWebviewRouter);
|
||||||
|
|
||||||
wss.on("connection", (ws) => {
|
const server = createServer(app);
|
||||||
console.log("WebSocket client connected");
|
|
||||||
|
|
||||||
ws.on("message", (data) => {
|
const wss = new WebSocketServer({ noServer: true });
|
||||||
console.log("Received:", data.toString());
|
|
||||||
|
server.on("upgrade", (req, socket, head) => {
|
||||||
|
const { url } = req;
|
||||||
|
if (url === "/lsp" || url === "/preview") {
|
||||||
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
||||||
|
wss.emit("connection", ws, req);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
socket.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
wss.on("connection", (ws, req) => {
|
||||||
|
const path = new URL(req.url ?? "/", "http://x").pathname;
|
||||||
|
console.log(`[ws] connected: ${path}`);
|
||||||
|
ws.on("close", () => console.log(`[ws] disconnected: ${path}`));
|
||||||
|
ws.on("error", (err) => console.error(`[ws] error (${path}):`, err.message));
|
||||||
|
if (path === "/lsp") {
|
||||||
|
attachLsp(ws);
|
||||||
|
} else if (path === "/preview") {
|
||||||
|
attachPreview(ws);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
await seedIfMissing();
|
||||||
|
server.listen(PORT, () => {
|
||||||
|
console.log(`Backend running on http://localhost:${PORT}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on("close", () => {
|
// Graceful shutdown: close the HTTP/WS server so tinymist (spawned by the
|
||||||
console.log("WebSocket client disconnected");
|
// LSP proxy) is reaped and ports release. Without this, `tsx watch` restarts
|
||||||
});
|
// can orphan child processes.
|
||||||
});
|
const shutdown = (sig: string) => () => {
|
||||||
|
console.log(`[shutdown] received ${sig}`);
|
||||||
|
server.close(() => process.exit(0));
|
||||||
|
// Force-exit after a short grace if something hangs.
|
||||||
|
setTimeout(() => process.exit(0), 1500).unref();
|
||||||
|
};
|
||||||
|
process.on("SIGTERM", shutdown("SIGTERM"));
|
||||||
|
process.on("SIGINT", shutdown("SIGINT"));
|
||||||
|
}
|
||||||
|
|
||||||
server.listen(PORT, () => {
|
main().catch((err) => {
|
||||||
console.log(`Backend running on http://localhost:${PORT}`);
|
console.error("Fatal startup error:", err);
|
||||||
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import type { WebSocket } from "ws";
|
||||||
|
import {
|
||||||
|
createConnection,
|
||||||
|
forward,
|
||||||
|
type IConnection,
|
||||||
|
} from "vscode-ws-jsonrpc/server";
|
||||||
|
import {
|
||||||
|
WebSocketMessageReader,
|
||||||
|
WebSocketMessageWriter,
|
||||||
|
type IWebSocket,
|
||||||
|
} from "vscode-ws-jsonrpc/socket";
|
||||||
|
import { createServerProcess } from "vscode-ws-jsonrpc/server";
|
||||||
|
|
||||||
|
interface ActiveSession {
|
||||||
|
clientConn: IConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
let active: ActiveSession | null = null;
|
||||||
|
|
||||||
|
function adaptWs(ws: WebSocket): IWebSocket {
|
||||||
|
const messageListeners: Array<(data: unknown) => void> = [];
|
||||||
|
const errorListeners: Array<(reason: unknown) => void> = [];
|
||||||
|
const closeListeners: Array<(code: number, reason: string) => void> = [];
|
||||||
|
ws.on("message", (data) => {
|
||||||
|
const text = typeof data === "string" ? data : data.toString("utf8");
|
||||||
|
for (const cb of messageListeners) cb(text);
|
||||||
|
});
|
||||||
|
ws.on("error", (err) => {
|
||||||
|
for (const cb of errorListeners) cb(err);
|
||||||
|
});
|
||||||
|
ws.on("close", (code, reason) => {
|
||||||
|
for (const cb of closeListeners) cb(code, reason.toString());
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
send(content) {
|
||||||
|
ws.send(content);
|
||||||
|
},
|
||||||
|
onMessage(cb) {
|
||||||
|
messageListeners.push(cb);
|
||||||
|
},
|
||||||
|
onError(cb) {
|
||||||
|
errorListeners.push(cb);
|
||||||
|
},
|
||||||
|
onClose(cb) {
|
||||||
|
closeListeners.push(cb);
|
||||||
|
},
|
||||||
|
dispose() {
|
||||||
|
try {
|
||||||
|
ws.close();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function attachLsp(ws: WebSocket): void {
|
||||||
|
if (active) {
|
||||||
|
console.log("[lsp] takeover: closing previous session");
|
||||||
|
active.clientConn.dispose();
|
||||||
|
active = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverConn = createServerProcess("tinymist", "tinymist", ["lsp"]);
|
||||||
|
if (!serverConn) {
|
||||||
|
console.error("[lsp] failed to spawn tinymist");
|
||||||
|
ws.close(1011, "tinymist spawn failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iws = adaptWs(ws);
|
||||||
|
const wsReader = new WebSocketMessageReader(iws);
|
||||||
|
const wsWriter = new WebSocketMessageWriter(iws);
|
||||||
|
const clientConn = createConnection(wsReader, wsWriter, () => iws.dispose());
|
||||||
|
|
||||||
|
forward(clientConn, serverConn);
|
||||||
|
active = { clientConn };
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
if (active?.clientConn === clientConn) active = null;
|
||||||
|
};
|
||||||
|
clientConn.onClose(cleanup);
|
||||||
|
serverConn.onClose(cleanup);
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { PROJECTS_ROOT } from "./config.js";
|
||||||
|
|
||||||
|
export const projectNameSchema = z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.max(64)
|
||||||
|
.regex(/^[A-Za-z0-9._-]+$/, "invalid project name")
|
||||||
|
.refine((n) => n !== "." && n !== "..", "project name not allowed");
|
||||||
|
|
||||||
|
export const relPathSchema = z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.max(512)
|
||||||
|
.refine((p) => !path.isAbsolute(p), "absolute paths not allowed")
|
||||||
|
.refine((p) => !p.includes("\0"), "NUL bytes not allowed")
|
||||||
|
.refine((p) => {
|
||||||
|
const parts = p.split(/[\\/]+/);
|
||||||
|
return !parts.includes("..");
|
||||||
|
}, "parent traversal not allowed");
|
||||||
|
|
||||||
|
export function projectDir(name: string): string {
|
||||||
|
const dir = path.resolve(PROJECTS_ROOT, name);
|
||||||
|
const rootReal = path.resolve(PROJECTS_ROOT) + path.sep;
|
||||||
|
if (!(dir + path.sep).startsWith(rootReal) && dir !== path.resolve(PROJECTS_ROOT)) {
|
||||||
|
throw new Error("project name escapes projects root");
|
||||||
|
}
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveFile(
|
||||||
|
project: string,
|
||||||
|
relPath: string,
|
||||||
|
): { abs: string; virtual: string } {
|
||||||
|
const projectAbs = projectDir(project);
|
||||||
|
const abs = path.resolve(projectAbs, relPath);
|
||||||
|
const projectReal = projectAbs + path.sep;
|
||||||
|
if (!(abs + path.sep).startsWith(projectReal) && abs !== projectAbs) {
|
||||||
|
throw new Error("path escapes project root");
|
||||||
|
}
|
||||||
|
return { abs, virtual: `${project}/${relPath.split(/[\\/]+/).join("/")}` };
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import type { WebSocket, WebSocket as WSClient } from "ws";
|
||||||
|
import WebSocketImpl from "ws";
|
||||||
|
import { getPreviewHost, getPreviewPort } from "./preview-sink.js";
|
||||||
|
|
||||||
|
const RETRY_DELAY_MS = 250;
|
||||||
|
const RETRY_TIMEOUT_MS = 10000;
|
||||||
|
|
||||||
|
function dialPreview(port: number): Promise<WSClient> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const url = `ws://${getPreviewHost()}:${port}`;
|
||||||
|
// typst-preview warns/rejects connections without an Origin header; the
|
||||||
|
// browser always sends one, so our backend dial must add it too.
|
||||||
|
const upstream = new WebSocketImpl(url, {
|
||||||
|
headers: { Origin: `http://${getPreviewHost()}:${port}` },
|
||||||
|
});
|
||||||
|
upstream.once("open", () => {
|
||||||
|
upstream.off("error", onError);
|
||||||
|
resolve(upstream);
|
||||||
|
});
|
||||||
|
const onError = (err: Error) => {
|
||||||
|
upstream.off("open", onOpen);
|
||||||
|
reject(err);
|
||||||
|
};
|
||||||
|
const onOpen = () => {};
|
||||||
|
upstream.once("open", onOpen);
|
||||||
|
upstream.once("error", onError);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connectWithRetry(port: number): Promise<WSClient> {
|
||||||
|
const start = Date.now();
|
||||||
|
for (;;) {
|
||||||
|
try {
|
||||||
|
return await dialPreview(port);
|
||||||
|
} catch {
|
||||||
|
if (Date.now() - start > RETRY_TIMEOUT_MS) {
|
||||||
|
throw new Error("preview server not reachable");
|
||||||
|
}
|
||||||
|
await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bridge(client: WebSocket, upstream: WSClient): void {
|
||||||
|
client.on("message", (data, isBinary) => {
|
||||||
|
if (upstream.readyState === upstream.OPEN) {
|
||||||
|
upstream.send(data, { binary: isBinary });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
upstream.on("message", (data, isBinary) => {
|
||||||
|
if (client.readyState === client.OPEN) {
|
||||||
|
client.send(data, { binary: isBinary });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const close = () => {
|
||||||
|
try { client.close(); } catch { /* ignore */ }
|
||||||
|
try { upstream.close(); } catch { /* ignore */ }
|
||||||
|
};
|
||||||
|
client.on("close", close);
|
||||||
|
upstream.on("close", close);
|
||||||
|
client.on("error", close);
|
||||||
|
upstream.on("error", close);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function attachPreview(ws: WebSocket): void {
|
||||||
|
const port = getPreviewPort();
|
||||||
|
if (port === null) {
|
||||||
|
console.error("[preview] no preview port registered yet");
|
||||||
|
try { ws.close(1011, "no preview server"); } catch { /* ignore */ }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`[preview] client connected, dialing ws://${getPreviewHost()}:${port}`);
|
||||||
|
connectWithRetry(port)
|
||||||
|
.then((upstream) => {
|
||||||
|
console.log("[preview] upstream connected");
|
||||||
|
bridge(ws, upstream);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(`[preview] upstream unavailable: ${(err as Error).message}`);
|
||||||
|
try { ws.close(1011, "preview server unavailable"); } catch { /* ignore */ }
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// Holds the preview-server port discovered by the client (returned by the
|
||||||
|
// `tinymist.startDefaultPreview` LSP command). The client learns the port
|
||||||
|
// over the LSP wire and relays it here via REST; the backend then proxies
|
||||||
|
// `/preview` (WS) and `/preview-app` (HTTP) to that port. This keeps the LSP
|
||||||
|
// proxy a pure byte-pipe while still letting the browser reach the preview
|
||||||
|
// through our origin (works for remote self-hosting).
|
||||||
|
|
||||||
|
const DEFAULT_HOST = process.env.TINYMIST_PREVIEW_HOST ?? "127.0.0.1";
|
||||||
|
|
||||||
|
let currentPort: number | null = null;
|
||||||
|
|
||||||
|
export function getPreviewHost(): string {
|
||||||
|
return DEFAULT_HOST;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPreviewPort(): number | null {
|
||||||
|
return currentPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setPreviewPort(port: number): void {
|
||||||
|
currentPort = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function previewBaseUrl(): string | null {
|
||||||
|
if (currentPort === null) return null;
|
||||||
|
return `http://${DEFAULT_HOST}:${currentPort}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { Router, type Router as ExpressRouter } from "express";
|
||||||
|
import { previewBaseUrl } from "./preview-sink.js";
|
||||||
|
|
||||||
|
export const previewWebviewRouter: ExpressRouter = Router();
|
||||||
|
|
||||||
|
// Small script injected into the typst-preview webview HTML. The webview opens
|
||||||
|
// its data-plane WebSocket against its own origin (`ws://host/`); when we
|
||||||
|
// serve it from our origin we must repoint that socket at `/preview`, which
|
||||||
|
// our backend proxies to the (random) port tinymist assigned. Patching the
|
||||||
|
// global WebSocket constructor is reliable regardless of how the webview's
|
||||||
|
// minified RxJS builds the URL.
|
||||||
|
//
|
||||||
|
// We also override `matchMedia` so `(prefers-color-scheme: dark)` is always
|
||||||
|
// `false`, forcing the preview to render in light mode. tinymist's CLI
|
||||||
|
// `--invert-colors` flag sometimes doesn't propagate to the webview client;
|
||||||
|
// this is a belt-and-suspenders fix at the HTML level.
|
||||||
|
const REWRITE_SCRIPT = `<script>(function(){
|
||||||
|
// ---- WS path rewrite ----
|
||||||
|
var Orig = window.WebSocket;
|
||||||
|
function Patched(url, protocols){
|
||||||
|
try {
|
||||||
|
var u = new URL(url, location.origin);
|
||||||
|
if (u.pathname === "/" || u.pathname === "") u.pathname = "/preview";
|
||||||
|
url = u.toString();
|
||||||
|
} catch (e) {}
|
||||||
|
return protocols === undefined ? new Orig(url) : new Orig(url, protocols);
|
||||||
|
}
|
||||||
|
Patched.prototype = Orig.prototype;
|
||||||
|
Patched.CONNECTING = Orig.CONNECTING;
|
||||||
|
Patched.OPEN = Orig.OPEN;
|
||||||
|
Patched.CLOSING = Orig.CLOSING;
|
||||||
|
Patched.CLOSED = Orig.CLOSED;
|
||||||
|
window.WebSocket = Patched;
|
||||||
|
|
||||||
|
// ---- Force light mode ----
|
||||||
|
var _mm = window.matchMedia.bind(window);
|
||||||
|
window.matchMedia = function(q) {
|
||||||
|
var m = _mm(q);
|
||||||
|
if (/(prefers-color-scheme|color-scheme)/i.test(q)) {
|
||||||
|
return Object.defineProperties({}, {
|
||||||
|
matches: { get: function(){ return false; } },
|
||||||
|
media: { get: function(){ return q; } },
|
||||||
|
onchange: { get: function(){ return null; }, set: function(){} },
|
||||||
|
addEventListener: { value: function(){} },
|
||||||
|
removeEventListener: { value: function(){} },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
};
|
||||||
|
})();</script>`;
|
||||||
|
|
||||||
|
previewWebviewRouter.get("/", async (_req, res) => {
|
||||||
|
const base = previewBaseUrl();
|
||||||
|
if (!base) {
|
||||||
|
res.status(503).send("preview server not started");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const upstream = await fetch(base + "/");
|
||||||
|
if (!upstream.ok) {
|
||||||
|
res.status(502).send(`preview server returned ${upstream.status}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let html = await upstream.text();
|
||||||
|
if (html.includes("</head>")) {
|
||||||
|
html = html.replace("</head>", `${REWRITE_SCRIPT}</head>`);
|
||||||
|
} else {
|
||||||
|
// no <head>; prepend so the patch is installed before any app script runs
|
||||||
|
html = REWRITE_SCRIPT + html;
|
||||||
|
}
|
||||||
|
res.type("html").send(html);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(502).send(`preview server unreachable: ${(err as Error).message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Router, type Router as ExpressRouter } from "express";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { projectNameSchema, relPathSchema, resolveFile } from "../paths.js";
|
||||||
|
|
||||||
|
export const filesRouter: ExpressRouter = Router();
|
||||||
|
|
||||||
|
const projectParam = z.object({ project: projectNameSchema });
|
||||||
|
const fileQuery = z.object({ path: relPathSchema });
|
||||||
|
const writeBody = z.object({ content: z.string() });
|
||||||
|
|
||||||
|
filesRouter.get("/:project/file", async (req, res) => {
|
||||||
|
const p = projectParam.safeParse(req.params);
|
||||||
|
const q = fileQuery.safeParse(req.query);
|
||||||
|
if (!p.success || !q.success) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: [...(p.error?.issues ?? []), ...(q.error?.issues ?? [])],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { abs } = resolveFile(p.data.project, q.data.path);
|
||||||
|
const content = await fs.readFile(abs, "utf8");
|
||||||
|
const stat = await fs.stat(abs);
|
||||||
|
res.json({ path: q.data.path, content, mtime: stat.mtimeMs });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(404).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
filesRouter.put("/:project/file", async (req, res) => {
|
||||||
|
const p = projectParam.safeParse(req.params);
|
||||||
|
const q = fileQuery.safeParse(req.query);
|
||||||
|
const b = writeBody.safeParse(req.body);
|
||||||
|
if (!p.success || !q.success || !b.success) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: [
|
||||||
|
...(p.error?.issues ?? []),
|
||||||
|
...(q.error?.issues ?? []),
|
||||||
|
...(b.error?.issues ?? []),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { abs } = resolveFile(p.data.project, q.data.path);
|
||||||
|
await fs.mkdir(path.dirname(abs), { recursive: true });
|
||||||
|
await fs.writeFile(abs, b.data.content, "utf8");
|
||||||
|
const stat = await fs.stat(abs);
|
||||||
|
res.json({ path: q.data.path, mtime: stat.mtimeMs });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Router, type Router as ExpressRouter } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { setPreviewPort } from "../preview-sink.js";
|
||||||
|
|
||||||
|
export const previewRouter: ExpressRouter = Router();
|
||||||
|
|
||||||
|
const portSchema = z.object({
|
||||||
|
port: z.number().int().min(1).max(65535),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Called by the frontend after the LSP `tinymist.startDefaultPreview` command
|
||||||
|
// returns its (random) data-plane port. The backend stores it so that the
|
||||||
|
// `/preview` WS and `/preview-app` HTTP proxies know where to dial.
|
||||||
|
previewRouter.post("/sink", (req, res) => {
|
||||||
|
const parsed = portSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
res.status(400).json({ error: parsed.error.issues });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPreviewPort(parsed.data.port);
|
||||||
|
res.json({ ok: true, port: parsed.data.port });
|
||||||
|
});
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { Router, type Router as ExpressRouter } from "express";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { pathToFileURL } from "node:url";
|
||||||
|
import { PROJECTS_ROOT } from "../config.js";
|
||||||
|
import { projectDir, projectNameSchema } from "../paths.js";
|
||||||
|
|
||||||
|
export const projectsRouter: ExpressRouter = Router();
|
||||||
|
|
||||||
|
const MAX_TREE_DEPTH = 6;
|
||||||
|
const MAX_TREE_ENTRIES = 2000;
|
||||||
|
|
||||||
|
projectsRouter.get("/", async (_req, res) => {
|
||||||
|
try {
|
||||||
|
await fs.mkdir(PROJECTS_ROOT, { recursive: true });
|
||||||
|
const entries = await fs.readdir(PROJECTS_ROOT, { withFileTypes: true });
|
||||||
|
const projects = entries
|
||||||
|
.filter((e) => e.isDirectory() && !e.name.startsWith("."))
|
||||||
|
.map((e) => e.name)
|
||||||
|
.sort();
|
||||||
|
res.json({ projects });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
projectsRouter.post("/:project", async (req, res) => {
|
||||||
|
const parsed = projectNameSchema.safeParse(req.params.project);
|
||||||
|
if (!parsed.success) {
|
||||||
|
res.status(400).json({ error: parsed.error.issues });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const name = parsed.data;
|
||||||
|
const dir = path.join(PROJECTS_ROOT, name);
|
||||||
|
try {
|
||||||
|
await fs.mkdir(dir, { recursive: true });
|
||||||
|
const main = path.join(dir, "main.typ");
|
||||||
|
try {
|
||||||
|
await fs.access(main);
|
||||||
|
} catch {
|
||||||
|
await fs.writeFile(
|
||||||
|
main,
|
||||||
|
// The Typst heading `=` must be followed by a space, then the title.
|
||||||
|
`= ${name}\n\nHello from a fresh project.\n`,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
res.status(201).json({ project: name });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
projectsRouter.get("/:project", async (req, res) => {
|
||||||
|
const parsed = projectNameSchema.safeParse(req.params.project);
|
||||||
|
if (!parsed.success) {
|
||||||
|
res.status(400).json({ error: parsed.error.issues });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dir = projectDir(parsed.data);
|
||||||
|
try {
|
||||||
|
await fs.access(dir);
|
||||||
|
} catch {
|
||||||
|
res.status(404).json({ error: "project not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json({ project: parsed.data, uri: pathToFileURL(dir).href });
|
||||||
|
});
|
||||||
|
|
||||||
|
projectsRouter.get("/:project/tree", async (req, res) => {
|
||||||
|
const parsed = projectNameSchema.safeParse(req.params.project);
|
||||||
|
if (!parsed.success) {
|
||||||
|
res.status(400).json({ error: parsed.error.issues });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dir = projectDir(parsed.data);
|
||||||
|
try {
|
||||||
|
await fs.access(dir);
|
||||||
|
} catch {
|
||||||
|
res.status(404).json({ error: "project not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const out: { path: string; mtime: number }[] = [];
|
||||||
|
const budget = { left: MAX_TREE_ENTRIES };
|
||||||
|
|
||||||
|
async function walk(rel: string, depth: number): Promise<void> {
|
||||||
|
if (depth > MAX_TREE_DEPTH || budget.left <= 0) return;
|
||||||
|
const abs = path.join(dir, rel);
|
||||||
|
const entries = await fs.readdir(abs, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (budget.left <= 0) return;
|
||||||
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
||||||
|
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
||||||
|
const childAbs = path.join(abs, entry.name);
|
||||||
|
const stat = await fs.stat(childAbs);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
await walk(childRel, depth + 1);
|
||||||
|
} else if (entry.isFile() && entry.name.endsWith(".typ")) {
|
||||||
|
out.push({ path: childRel, mtime: stat.mtimeMs });
|
||||||
|
budget.left -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await walk("", 0);
|
||||||
|
res.json({ project: parsed.data, files: out });
|
||||||
|
});
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { PROJECTS_ROOT } from "./config.js";
|
||||||
|
|
||||||
|
const SEED_PROJECT = "hello";
|
||||||
|
const SEED_FILE = "main.typ";
|
||||||
|
|
||||||
|
const SEED_CONTENT = `= Hello, typst-leaf!
|
||||||
|
|
||||||
|
Welcome to your first project. Edit this file on the left;
|
||||||
|
the preview on the right updates as you type.
|
||||||
|
|
||||||
|
== Features
|
||||||
|
|
||||||
|
- Instant preview powered by *tinymist*
|
||||||
|
- Live diagnostics and autocomplete
|
||||||
|
- A real editor in your browser
|
||||||
|
|
||||||
|
== Math
|
||||||
|
|
||||||
|
The quadratic formula:
|
||||||
|
$ x = (-b +- sqrt(b^2 - 4 a c)) / (2 a) $
|
||||||
|
|
||||||
|
== Code listing
|
||||||
|
|
||||||
|
\`\`\`typ
|
||||||
|
#let greet(name) = [Hello, *#name*!]
|
||||||
|
|
||||||
|
#greet("world")
|
||||||
|
\`\`\`
|
||||||
|
`;
|
||||||
|
|
||||||
|
export async function seedIfMissing(): Promise<void> {
|
||||||
|
await fs.mkdir(PROJECTS_ROOT, { recursive: true });
|
||||||
|
const projectDir = path.join(PROJECTS_ROOT, SEED_PROJECT);
|
||||||
|
const file = path.join(projectDir, SEED_FILE);
|
||||||
|
try {
|
||||||
|
await fs.access(file);
|
||||||
|
} catch {
|
||||||
|
await fs.mkdir(projectDir, { recursive: true });
|
||||||
|
await fs.writeFile(file, SEED_CONTENT, "utf8");
|
||||||
|
console.log(`[seed] created ${SEED_PROJECT}/${SEED_FILE}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Tauri + React + Typescript</title>
|
<title>typst-leaf</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -14,11 +14,10 @@
|
|||||||
"@monaco-editor/react": "^4.7.0",
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"monaco-languageclient": "^10.7.0",
|
"monaco-editor": "^0.55.1",
|
||||||
"monaco-vim": "^0.4.4",
|
"monaco-vim": "^0.4.4",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"vscode-ws-jsonrpc": "^3.5.0",
|
|
||||||
"zod": "^3.24.5"
|
"zod": "^3.24.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "typst-leaf-frontend",
|
"productName": "typst-leaf",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"identifier": "com.nav.typst-leaf-frontend",
|
"identifier": "com.nav.typst-leaf-frontend",
|
||||||
"build": {
|
"build": {
|
||||||
|
|||||||
@@ -1,5 +1,48 @@
|
|||||||
function App() {
|
import { AppProvider, useApp } from "./app-state";
|
||||||
return <div className="h-screen w-screen bg-zinc-900 text-zinc-100" />;
|
import { EditorView } from "./components/Editor";
|
||||||
|
import { FileTree } from "./components/FileTree";
|
||||||
|
import { Preview } from "./components/Preview";
|
||||||
|
import { ProjectPicker } from "./components/ProjectPicker";
|
||||||
|
|
||||||
|
function Shell() {
|
||||||
|
const { state, lsp } = useApp();
|
||||||
|
return (
|
||||||
|
<div className="h-screen w-screen bg-white text-zinc-800 flex">
|
||||||
|
<aside className="w-60 border-r border-zinc-200 flex flex-col bg-zinc-50">
|
||||||
|
<div className="px-3 py-2 border-b border-zinc-200 text-sm font-semibold flex items-center gap-2">
|
||||||
|
<span>typst-leaf</span>
|
||||||
|
<span className="ml-auto text-xs font-normal text-zinc-400">
|
||||||
|
{lsp.status === "ready"
|
||||||
|
? "●"
|
||||||
|
: lsp.status === "connecting"
|
||||||
|
? "◌"
|
||||||
|
: lsp.status === "error"
|
||||||
|
? "✕"
|
||||||
|
: "○"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ProjectPicker />
|
||||||
|
<div className="flex-1 overflow-auto">
|
||||||
|
<FileTree />
|
||||||
|
</div>
|
||||||
|
{state.error && (
|
||||||
|
<div className="p-2 text-xs text-red-500 border-t border-zinc-200">
|
||||||
|
{state.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</aside>
|
||||||
|
<main className="flex-1 flex min-w-0">
|
||||||
|
<EditorView />
|
||||||
|
<Preview />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<AppProvider>
|
||||||
|
<Shell />
|
||||||
|
</AppProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import type {
|
||||||
|
FileResponse,
|
||||||
|
Project,
|
||||||
|
TreeResponse,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
const API = "/api";
|
||||||
|
|
||||||
|
async function jsonOrThrow<T>(res: Response): Promise<T> {
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
throw new Error(`${res.status} ${res.statusText}: ${text}`);
|
||||||
|
}
|
||||||
|
return res.json() as Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const api = {
|
||||||
|
listProjects: () =>
|
||||||
|
fetch(`${API}/projects`).then((r) =>
|
||||||
|
jsonOrThrow<{ projects: string[] }>(r).then((d) => d.projects),
|
||||||
|
),
|
||||||
|
createProject: (name: string) =>
|
||||||
|
fetch(`${API}/projects/${encodeURIComponent(name)}`, { method: "POST" }).then(
|
||||||
|
(r) => jsonOrThrow<Project>(r),
|
||||||
|
),
|
||||||
|
getProject: (name: string) =>
|
||||||
|
fetch(`${API}/projects/${encodeURIComponent(name)}`).then((r) =>
|
||||||
|
jsonOrThrow<Project>(r),
|
||||||
|
),
|
||||||
|
getTree: (name: string) =>
|
||||||
|
fetch(`${API}/projects/${encodeURIComponent(name)}/tree`).then((r) =>
|
||||||
|
jsonOrThrow<TreeResponse>(r),
|
||||||
|
),
|
||||||
|
getFile: (project: string, path: string) =>
|
||||||
|
fetch(
|
||||||
|
`${API}/projects/${encodeURIComponent(project)}/file?path=${encodeURIComponent(path)}`,
|
||||||
|
).then((r) => jsonOrThrow<FileResponse>(r)),
|
||||||
|
putFile: (project: string, path: string, content: string) =>
|
||||||
|
fetch(
|
||||||
|
`${API}/projects/${encodeURIComponent(project)}/file?path=${encodeURIComponent(path)}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ content }),
|
||||||
|
},
|
||||||
|
).then((r) => jsonOrThrow<FileResponse>(r)),
|
||||||
|
sinkPreviewPort: (port: number) =>
|
||||||
|
fetch(`${API}/preview/sink`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ port }),
|
||||||
|
}).then((r) => jsonOrThrow<{ ok: boolean; port: number }>(r)),
|
||||||
|
};
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useReducer,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
type ReactNode,
|
||||||
|
} from "react";
|
||||||
|
import { api } from "./api";
|
||||||
|
import { loadMonaco } from "./monaco-setup";
|
||||||
|
import { startLsp, startPreview, type RawLspClient } from "./lsp";
|
||||||
|
import type { Monaco } from "@monaco-editor/react";
|
||||||
|
import type { FileEntry } from "./types";
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
projects: string[];
|
||||||
|
currentProject: string | null;
|
||||||
|
currentUri: string | null;
|
||||||
|
tree: FileEntry[];
|
||||||
|
openFile: string | null;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Action =
|
||||||
|
| { type: "setProjects"; projects: string[] }
|
||||||
|
| { type: "selectProject"; project: string; uri: string }
|
||||||
|
| { type: "setTree"; tree: FileEntry[] }
|
||||||
|
| { type: "openFile"; path: string | null }
|
||||||
|
| { type: "loading"; loading: boolean }
|
||||||
|
| { type: "error"; error: string | null };
|
||||||
|
|
||||||
|
const initial: State = {
|
||||||
|
projects: [],
|
||||||
|
currentProject: null,
|
||||||
|
currentUri: null,
|
||||||
|
tree: [],
|
||||||
|
openFile: null,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
function reducer(state: State, action: Action): State {
|
||||||
|
switch (action.type) {
|
||||||
|
case "setProjects":
|
||||||
|
return { ...state, projects: action.projects };
|
||||||
|
case "selectProject":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
currentProject: action.project,
|
||||||
|
currentUri: action.uri,
|
||||||
|
tree: [],
|
||||||
|
openFile: null,
|
||||||
|
};
|
||||||
|
case "setTree":
|
||||||
|
return { ...state, tree: action.tree };
|
||||||
|
case "openFile":
|
||||||
|
return { ...state, openFile: action.path };
|
||||||
|
case "loading":
|
||||||
|
return { ...state, loading: action.loading };
|
||||||
|
case "error":
|
||||||
|
return { ...state, error: action.error };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LspState {
|
||||||
|
monaco: Monaco | null;
|
||||||
|
client: RawLspClient | null;
|
||||||
|
status: "idle" | "connecting" | "ready" | "error";
|
||||||
|
previewReady: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppContext = createContext<{
|
||||||
|
state: State;
|
||||||
|
dispatch: React.Dispatch<Action>;
|
||||||
|
lsp: LspState;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
export function useApp() {
|
||||||
|
const ctx = useContext(AppContext);
|
||||||
|
if (!ctx) throw new Error("useApp outside provider");
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [state, dispatch] = useReducer(reducer, initial);
|
||||||
|
const [lsp, setLsp] = useState<LspState>({
|
||||||
|
monaco: null,
|
||||||
|
client: null,
|
||||||
|
status: "idle",
|
||||||
|
previewReady: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
const sessionRef = useRef<{ dispose: () => void } | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadMonaco()
|
||||||
|
.then((monaco) => setLsp((s) => ({ ...s, monaco })))
|
||||||
|
.catch((e) =>
|
||||||
|
setLsp((s) => ({ ...s, status: "error", error: String(e) })),
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api
|
||||||
|
.listProjects()
|
||||||
|
.then((projects) => dispatch({ type: "setProjects", projects }))
|
||||||
|
.catch((e) => dispatch({ type: "error", error: String(e) }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!state.currentProject || !lsp.monaco) return;
|
||||||
|
const sessionRefLocal = sessionRef;
|
||||||
|
let cancelled = false;
|
||||||
|
const project = state.currentProject;
|
||||||
|
const uri = state.currentUri!;
|
||||||
|
setLsp((s) => ({ ...s, status: "connecting", previewReady: false }));
|
||||||
|
startLsp(lsp.monaco, project, uri, async (client) => {
|
||||||
|
// First file opened: ask tinymist for a live preview and relay the
|
||||||
|
// (random) data-plane port to the backend so /preview-app can proxy.
|
||||||
|
try {
|
||||||
|
const port = await startPreview(client);
|
||||||
|
await api.sinkPreviewPort(port);
|
||||||
|
setLsp((s) => ({ ...s, previewReady: true }));
|
||||||
|
} catch (e) {
|
||||||
|
console.error("preview start failed:", String(e));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((session) => {
|
||||||
|
if (cancelled) {
|
||||||
|
session.dispose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sessionRefLocal.current = session;
|
||||||
|
setLsp({
|
||||||
|
monaco: lsp.monaco,
|
||||||
|
client: session.client,
|
||||||
|
status: "ready",
|
||||||
|
previewReady: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("[lsp] startLsp failed:", e);
|
||||||
|
if (!cancelled) {
|
||||||
|
setLsp({
|
||||||
|
monaco: lsp.monaco,
|
||||||
|
client: null,
|
||||||
|
status: "error",
|
||||||
|
previewReady: false,
|
||||||
|
error: e instanceof Error ? `${e.name}: ${e.message}` : String(e),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
sessionRefLocal.current?.dispose();
|
||||||
|
sessionRefLocal.current = null;
|
||||||
|
};
|
||||||
|
}, [state.currentProject, state.currentUri, lsp.monaco]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!state.currentProject) return;
|
||||||
|
api
|
||||||
|
.getTree(state.currentProject)
|
||||||
|
.then((t) => dispatch({ type: "setTree", tree: t.files }))
|
||||||
|
.catch((e) => dispatch({ type: "error", error: String(e) }));
|
||||||
|
}, [state.currentProject]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppContext.Provider value={{ state, dispatch, lsp }}>
|
||||||
|
{children}
|
||||||
|
</AppContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import Editor, { type Monaco, type OnMount } from "@monaco-editor/react";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { api } from "../api";
|
||||||
|
import { useApp } from "../app-state";
|
||||||
|
|
||||||
|
type ITextModel = Monaco["editor"]["ITextModel"];
|
||||||
|
type IStandaloneCodeEditor = Monaco["editor"]["IStandaloneCodeEditor"];
|
||||||
|
|
||||||
|
export function EditorView() {
|
||||||
|
const { state, lsp } = useApp();
|
||||||
|
const monaco = lsp.monaco;
|
||||||
|
const [model, setModel] = useState<ITextModel | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const editorRef = useRef<IStandaloneCodeEditor | null>(null);
|
||||||
|
const project = state.currentProject;
|
||||||
|
const uri = state.currentUri;
|
||||||
|
const file = state.openFile;
|
||||||
|
const savingRef = useRef(false);
|
||||||
|
|
||||||
|
// Create/replace the Monaco model for the open file. Monaco comes from the
|
||||||
|
// app-level loader (no per-component polling); the effect re-runs when the
|
||||||
|
// file, project, or monaco instance changes.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!monaco || !project || !file || !uri) {
|
||||||
|
setModel((m: ITextModel | null) => {
|
||||||
|
m?.dispose();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cancelled = false;
|
||||||
|
setError(null);
|
||||||
|
api
|
||||||
|
.getFile(project, file)
|
||||||
|
.then((f) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
const base = uri.endsWith("/") ? uri : uri + "/";
|
||||||
|
const docUri = base + file;
|
||||||
|
// Create the model OUTSIDE the setState updater: React StrictMode
|
||||||
|
// double-invokes updaters with the same `prev`, and createModel is a
|
||||||
|
// side effect (the second call would crash on "model already exists").
|
||||||
|
// Disposing any stale model with this URI also keeps remounts idempotent.
|
||||||
|
const uriObj = monaco.Uri.parse(docUri);
|
||||||
|
monaco.editor.getModel(uriObj)?.dispose();
|
||||||
|
const next = monaco.editor.createModel(f.content, "typst", uriObj);
|
||||||
|
setModel((prev: ITextModel | null) => {
|
||||||
|
prev?.dispose();
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
// Creating the model triggers the LSP `didOpen` (via bindModels in
|
||||||
|
// app-state), which in turn starts the live preview. No disk write
|
||||||
|
// needed — the preview is driven by the LSP document state.
|
||||||
|
})
|
||||||
|
.catch((e) => !cancelled && setError(String(e)));
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [monaco, project, file, uri]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editorRef.current && model) {
|
||||||
|
editorRef.current.setModel(model);
|
||||||
|
}
|
||||||
|
}, [model]);
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
if (!project || !file || !model || savingRef.current) return;
|
||||||
|
savingRef.current = true;
|
||||||
|
try {
|
||||||
|
await api.putFile(project, file, model.getValue());
|
||||||
|
} catch (e) {
|
||||||
|
setError(String(e));
|
||||||
|
} finally {
|
||||||
|
savingRef.current = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMount: OnMount = (editor, m) => {
|
||||||
|
editorRef.current = editor;
|
||||||
|
if (model) editor.setModel(model);
|
||||||
|
editor.addCommand(m.KeyMod.CtrlCmd | m.KeyCode.KeyS, () => {
|
||||||
|
void save();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex items-center justify-center text-zinc-400 text-sm">
|
||||||
|
Select a project to begin.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!file) {
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex items-center justify-center text-zinc-400 text-sm">
|
||||||
|
Open a file from the sidebar.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex items-center justify-center text-red-400 text-sm p-4">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex flex-col min-h-0">
|
||||||
|
<div className="flex items-center gap-2 px-3 py-1.5 border-b border-zinc-200 text-xs text-zinc-500 font-mono">
|
||||||
|
<span>{file}</span>
|
||||||
|
<span className="ml-auto">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void save()}
|
||||||
|
className="bg-white hover:bg-zinc-100 text-zinc-700 border border-zinc-300 px-2 py-0.5 rounded"
|
||||||
|
>
|
||||||
|
Save (Ctrl+S)
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-h-0">
|
||||||
|
<Editor
|
||||||
|
defaultLanguage="typst"
|
||||||
|
theme="vs"
|
||||||
|
onMount={handleMount}
|
||||||
|
options={{
|
||||||
|
fontSize: 14,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
wordWrap: "on",
|
||||||
|
automaticLayout: true,
|
||||||
|
tabSize: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { useApp } from "../app-state";
|
||||||
|
|
||||||
|
export function FileTree() {
|
||||||
|
const { state, dispatch } = useApp();
|
||||||
|
|
||||||
|
if (!state.currentProject) {
|
||||||
|
return (
|
||||||
|
<div className="p-2 text-xs text-zinc-400">select a project</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (state.tree.length === 0) {
|
||||||
|
return <div className="p-2 text-xs text-zinc-400">no .typ files</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-0.5 p-2">
|
||||||
|
<div className="text-xs uppercase tracking-wide text-zinc-400 mb-1">
|
||||||
|
Files
|
||||||
|
</div>
|
||||||
|
{state.tree.map((f) => (
|
||||||
|
<button
|
||||||
|
key={f.path}
|
||||||
|
type="button"
|
||||||
|
onClick={() => dispatch({ type: "openFile", path: f.path })}
|
||||||
|
className={`text-left text-sm px-2 py-1 rounded font-mono ${
|
||||||
|
f.path === state.openFile
|
||||||
|
? "bg-emerald-100 text-emerald-800 font-medium"
|
||||||
|
: "hover:bg-zinc-100 text-zinc-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{f.path}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { useApp } from "../app-state";
|
||||||
|
|
||||||
|
export function Preview() {
|
||||||
|
const { state, lsp } = useApp();
|
||||||
|
|
||||||
|
let status = "idle";
|
||||||
|
if (!state.currentProject) status = "no project";
|
||||||
|
else if (lsp.status !== "ready") status = `LSP ${lsp.status}`;
|
||||||
|
else if (!lsp.previewReady) status = "starting preview…";
|
||||||
|
else status = "live";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-1/2 border-l border-zinc-200 flex flex-col bg-white">
|
||||||
|
<div className="px-3 py-1.5 border-b border-zinc-200 text-xs text-zinc-500 font-mono flex items-center gap-2">
|
||||||
|
<span>Preview</span>
|
||||||
|
<span className="ml-auto text-zinc-400">{status}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 bg-zinc-50 relative">
|
||||||
|
{state.currentProject && lsp.status === "ready" && lsp.previewReady ? (
|
||||||
|
<iframe
|
||||||
|
key={state.currentProject}
|
||||||
|
src="/preview-app"
|
||||||
|
title="Typst preview"
|
||||||
|
className="absolute inset-0 w-full h-full bg-white"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="absolute inset-0 flex flex-col items-center justify-center text-zinc-400 text-sm p-6 gap-2">
|
||||||
|
<span>{status}</span>
|
||||||
|
{lsp.status === "error" && lsp.error && (
|
||||||
|
<pre className="mt-2 max-w-full overflow-auto text-xs text-red-500 bg-red-50 p-3 rounded whitespace-pre-wrap break-all">
|
||||||
|
{lsp.error}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { api } from "../api";
|
||||||
|
import { useApp } from "../app-state";
|
||||||
|
|
||||||
|
export function ProjectPicker() {
|
||||||
|
const { state, dispatch } = useApp();
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
|
||||||
|
async function select(p: string) {
|
||||||
|
if (p === state.currentProject) return;
|
||||||
|
const info = await api.getProject(p);
|
||||||
|
dispatch({ type: "selectProject", project: info.project, uri: info.uri });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function create() {
|
||||||
|
const n = name.trim();
|
||||||
|
if (!n) return;
|
||||||
|
await api.createProject(n);
|
||||||
|
setName("");
|
||||||
|
const projects = await api.listProjects();
|
||||||
|
dispatch({ type: "setProjects", projects });
|
||||||
|
const info = await api.getProject(n);
|
||||||
|
dispatch({ type: "selectProject", project: info.project, uri: info.uri });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2 p-2 border-b border-zinc-200">
|
||||||
|
<div className="text-xs uppercase tracking-wide text-zinc-400">
|
||||||
|
Projects
|
||||||
|
</div>
|
||||||
|
<ul className="flex flex-col gap-0.5">
|
||||||
|
{state.projects.map((p) => (
|
||||||
|
<li key={p}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => select(p)}
|
||||||
|
className={`w-full text-left text-sm px-2 py-1 rounded ${
|
||||||
|
p === state.currentProject
|
||||||
|
? "bg-emerald-100 text-emerald-800 font-medium"
|
||||||
|
: "hover:bg-zinc-100 text-zinc-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{p}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
{state.projects.length === 0 && (
|
||||||
|
<li className="text-xs text-zinc-400 px-2">no projects yet</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<input
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") create();
|
||||||
|
}}
|
||||||
|
placeholder="new-project"
|
||||||
|
className="flex-1 bg-white text-zinc-800 text-sm px-2 py-1 rounded border border-zinc-300 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 placeholder:text-zinc-400"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={create}
|
||||||
|
className="text-sm bg-emerald-600 hover:bg-emerald-500 text-white px-2 py-1 rounded"
|
||||||
|
>
|
||||||
|
New
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,555 @@
|
|||||||
|
import type { Monaco } from "@monaco-editor/react";
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Raw LSP client over WebSocket.
|
||||||
|
//
|
||||||
|
// We deliberately avoid monaco-languageclient / vscode-languageclient here.
|
||||||
|
// monaco-languageclient v10's low-level client requires bootstrapping the
|
||||||
|
// entire @codingame/monaco-vscode-api service stack (the wrapper does this),
|
||||||
|
// which is heavy and fragile. Driving LSP directly is ~300 lines, has no
|
||||||
|
// magic, and the model<->LSP binding + Monaco provider registration are done
|
||||||
|
// explicitly. The wire protocol is just JSON-RPC messages, one per WebSocket
|
||||||
|
// text frame (raw JSON, no Content-Length framing — that's only for stdio).
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface LspSession {
|
||||||
|
client: RawLspClient;
|
||||||
|
dispose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotifHandler = (params: unknown) => void;
|
||||||
|
type ReqHandler = (params: unknown) => Promise<unknown> | unknown;
|
||||||
|
|
||||||
|
export class RawLspClient {
|
||||||
|
private ws: WebSocket;
|
||||||
|
private nextId = 1;
|
||||||
|
private pending = new Map<
|
||||||
|
number,
|
||||||
|
{ resolve: (v: unknown) => void; reject: (e: Error) => void }
|
||||||
|
>();
|
||||||
|
private notifHandlers = new Map<string, NotifHandler[]>();
|
||||||
|
private reqHandlers = new Map<string, ReqHandler>();
|
||||||
|
|
||||||
|
constructor(ws: WebSocket) {
|
||||||
|
this.ws = ws;
|
||||||
|
ws.addEventListener("message", (e) => this.onMessage(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
private onMessage(e: MessageEvent): void {
|
||||||
|
let msg: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
msg = JSON.parse(
|
||||||
|
typeof e.data === "string" ? e.data : e.data.toString(),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hasMethod = typeof msg.method === "string";
|
||||||
|
const id = msg.id;
|
||||||
|
const hasId = id !== undefined && id !== null;
|
||||||
|
|
||||||
|
if (hasMethod && hasId) {
|
||||||
|
// request from server -> handle and reply
|
||||||
|
const handler = this.reqHandlers.get(msg.method as string);
|
||||||
|
Promise.resolve(handler ? handler(msg.params) : undefined).then(
|
||||||
|
(result) =>
|
||||||
|
this.send({ jsonrpc: "2.0", id, result: result ?? null }),
|
||||||
|
(err) =>
|
||||||
|
this.send({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id,
|
||||||
|
error: {
|
||||||
|
code: -32603,
|
||||||
|
message: err instanceof Error ? err.message : String(err),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else if (hasMethod) {
|
||||||
|
// notification from server
|
||||||
|
const handlers = this.notifHandlers.get(msg.method as string);
|
||||||
|
if (handlers) for (const h of handlers) h(msg.params);
|
||||||
|
} else if (hasId) {
|
||||||
|
// response to a request we sent
|
||||||
|
const p = this.pending.get(id as number);
|
||||||
|
if (p) {
|
||||||
|
this.pending.delete(id as number);
|
||||||
|
if (msg.error) {
|
||||||
|
p.reject(
|
||||||
|
new Error(
|
||||||
|
(msg.error as { message: string }).message ?? "LSP error",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
p.resolve(msg.result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private send(msg: unknown): void {
|
||||||
|
if (this.ws.readyState === this.ws.OPEN) {
|
||||||
|
this.ws.send(JSON.stringify(msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notify(method: string, params?: unknown): void {
|
||||||
|
this.send({ jsonrpc: "2.0", method, params });
|
||||||
|
}
|
||||||
|
|
||||||
|
sendRequest(method: string, params?: unknown): Promise<unknown> {
|
||||||
|
const id = this.nextId++;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.pending.set(id, { resolve, reject });
|
||||||
|
this.send({ jsonrpc: "2.0", id, method, params });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onNotification(method: string, handler: NotifHandler): void {
|
||||||
|
const arr = this.notifHandlers.get(method) ?? [];
|
||||||
|
arr.push(handler);
|
||||||
|
this.notifHandlers.set(method, arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
onRequest(method: string, handler: ReqHandler): void {
|
||||||
|
this.reqHandlers.set(method, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
try {
|
||||||
|
this.ws.close();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
for (const [, p] of this.pending) p.reject(new Error("disposed"));
|
||||||
|
this.pending.clear();
|
||||||
|
this.notifHandlers.clear();
|
||||||
|
this.reqHandlers.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- LSP <-> Monaco type helpers ----------
|
||||||
|
|
||||||
|
interface LspPosition {
|
||||||
|
line: number;
|
||||||
|
character: number;
|
||||||
|
}
|
||||||
|
interface LspRange {
|
||||||
|
start: LspPosition;
|
||||||
|
end: LspPosition;
|
||||||
|
}
|
||||||
|
interface LspDiagnostic {
|
||||||
|
message: string;
|
||||||
|
severity?: number;
|
||||||
|
source?: string;
|
||||||
|
range: LspRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toMonacoRange(
|
||||||
|
monaco: Monaco,
|
||||||
|
r: LspRange | undefined,
|
||||||
|
): Monaco["ranges"]["IRange"] {
|
||||||
|
if (!r) return new monaco.Range(1, 1, 1, 1);
|
||||||
|
return new monaco.Range(
|
||||||
|
r.start.line + 1,
|
||||||
|
r.start.character + 1,
|
||||||
|
r.end.line + 1,
|
||||||
|
r.end.character + 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toMarker(d: LspDiagnostic): Monaco["editor"]["IMarkerData"] {
|
||||||
|
// LSP severity: 1=Error 2=Warning 3=Info 4=Hint
|
||||||
|
// MarkerSeverity: Error=8 Warning=4 Info=2 Hint=1
|
||||||
|
const sev =
|
||||||
|
d.severity === 1 ? 8 : d.severity === 2 ? 4 : d.severity === 3 ? 2 : 1;
|
||||||
|
return {
|
||||||
|
message: d.message,
|
||||||
|
severity: sev as Monaco["MarkerSeverity"],
|
||||||
|
source: d.source ?? "typst",
|
||||||
|
startLineNumber: d.range.start.line + 1,
|
||||||
|
startColumn: d.range.start.character + 1,
|
||||||
|
endLineNumber: d.range.end.line + 1,
|
||||||
|
endColumn: d.range.end.character + 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Model <-> LSP binding ----------
|
||||||
|
|
||||||
|
interface ModelBinding {
|
||||||
|
dispose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindModels(
|
||||||
|
client: RawLspClient,
|
||||||
|
monaco: Monaco,
|
||||||
|
onFirstOpen?: () => void,
|
||||||
|
): ModelBinding {
|
||||||
|
const versions = new Map<string, number>();
|
||||||
|
const opened = new Set<string>();
|
||||||
|
const disposers: Array<() => void> = [];
|
||||||
|
const perModel = new Map<string, () => void>();
|
||||||
|
let firstOpenFired = false;
|
||||||
|
|
||||||
|
const openModel = (model: Monaco["editor"]["ITextModel"]) => {
|
||||||
|
const u = model.uri.toString();
|
||||||
|
// The @monaco-editor/react <Editor> creates default models with
|
||||||
|
// inmemory:// URIs. We only sync models that have file:// URIs
|
||||||
|
// (the ones we explicitly create), otherwise tinymist panics trying
|
||||||
|
// to resolve the URI.
|
||||||
|
if (model.uri.scheme !== "file") return;
|
||||||
|
if (opened.has(u) || model.getLanguageId() !== "typst") return;
|
||||||
|
opened.add(u);
|
||||||
|
versions.set(u, 1);
|
||||||
|
client.notify("textDocument/didOpen", {
|
||||||
|
textDocument: {
|
||||||
|
uri: u,
|
||||||
|
languageId: "typst",
|
||||||
|
version: 1,
|
||||||
|
text: model.getValue(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const onChange = model.onDidChangeContent(() => {
|
||||||
|
const v = (versions.get(u) ?? 1) + 1;
|
||||||
|
versions.set(u, v);
|
||||||
|
client.notify("textDocument/didChange", {
|
||||||
|
textDocument: { uri: u, version: v },
|
||||||
|
contentChanges: [{ text: model.getValue() }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
perModel.set(u, () => onChange.dispose());
|
||||||
|
if (!firstOpenFired) {
|
||||||
|
firstOpenFired = true;
|
||||||
|
onFirstOpen?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModel = (uri: string) => {
|
||||||
|
if (!opened.has(uri)) return;
|
||||||
|
opened.delete(uri);
|
||||||
|
versions.delete(uri);
|
||||||
|
perModel.get(uri)?.();
|
||||||
|
perModel.delete(uri);
|
||||||
|
client.notify("textDocument/didClose", { textDocument: { uri } });
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const m of monaco.editor.getModels()) openModel(m);
|
||||||
|
disposers.push(
|
||||||
|
monaco.editor.onDidCreateModel((m: Monaco["editor"]["ITextModel"]) =>
|
||||||
|
openModel(m),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
disposers.push(
|
||||||
|
monaco.editor.onWillDisposeModel((m: Monaco["editor"]["ITextModel"]) => {
|
||||||
|
if (m.uri.scheme === "file") closeModel(m.uri.toString());
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dispose() {
|
||||||
|
for (const d of disposers) d();
|
||||||
|
for (const [, d] of perModel) d();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Monaco provider registration (LSP-backed) ----------
|
||||||
|
|
||||||
|
function registerProviders(
|
||||||
|
client: RawLspClient,
|
||||||
|
monaco: Monaco,
|
||||||
|
capabilities: Record<string, { [k: string]: unknown } | undefined>,
|
||||||
|
): Monaco["IDisposable"][] {
|
||||||
|
const disposables: Monaco["IDisposable"][] = [];
|
||||||
|
const doc = (model: Monaco["editor"]["ITextModel"]) => ({
|
||||||
|
textDocument: { uri: model.uri.toString() },
|
||||||
|
});
|
||||||
|
const pos = (p: Monaco["Position"]) => ({
|
||||||
|
line: p.lineNumber - 1,
|
||||||
|
character: p.column - 1,
|
||||||
|
});
|
||||||
|
type ITextModel = Monaco["editor"]["ITextModel"];
|
||||||
|
type IPosition = Monaco["Position"];
|
||||||
|
|
||||||
|
if (capabilities.completionProvider) {
|
||||||
|
const triggers =
|
||||||
|
(capabilities.completionProvider.triggerCharacters as string[] | undefined) ??
|
||||||
|
[];
|
||||||
|
disposables.push(
|
||||||
|
monaco.languages.registerCompletionItemProvider("typst", {
|
||||||
|
triggerCharacters: triggers,
|
||||||
|
provideCompletionItems: async (model: ITextModel, position: IPosition) => {
|
||||||
|
const r = await client.sendRequest("textDocument/completion", {
|
||||||
|
...doc(model),
|
||||||
|
position: pos(position),
|
||||||
|
});
|
||||||
|
const items = Array.isArray(r)
|
||||||
|
? r
|
||||||
|
: ((r as { items?: unknown[] })?.items ?? []);
|
||||||
|
const word = model.getWordUntilPosition(position);
|
||||||
|
const range = new monaco.Range(
|
||||||
|
position.lineNumber,
|
||||||
|
word.startColumn,
|
||||||
|
position.lineNumber,
|
||||||
|
word.endColumn,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
suggestions: (items as Record<string, unknown>[]).map((it) => ({
|
||||||
|
label: it.label as string,
|
||||||
|
kind: (it.kind as number) ?? 1,
|
||||||
|
detail: it.detail as string,
|
||||||
|
documentation: (
|
||||||
|
typeof it.documentation === "string"
|
||||||
|
? it.documentation
|
||||||
|
: (it.documentation as { value?: string })?.value
|
||||||
|
) as string,
|
||||||
|
insertText: (it.insertText as string) ?? (it.label as string),
|
||||||
|
insertTextRules:
|
||||||
|
it.insertTextFormat === 2
|
||||||
|
? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
|
||||||
|
: undefined,
|
||||||
|
range,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (capabilities.hoverProvider) {
|
||||||
|
disposables.push(
|
||||||
|
monaco.languages.registerHoverProvider("typst", {
|
||||||
|
provideHover: async (model: ITextModel, position: IPosition) => {
|
||||||
|
const r = (await client.sendRequest("textDocument/hover", {
|
||||||
|
...doc(model),
|
||||||
|
position: pos(position),
|
||||||
|
})) as { contents?: unknown[]; range?: LspRange } | null;
|
||||||
|
if (!r) return undefined;
|
||||||
|
const contents = (r.contents ?? []).map((c) =>
|
||||||
|
typeof c === "string"
|
||||||
|
? c
|
||||||
|
: (c as { value?: string })?.value ?? "",
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
range: toMonacoRange(monaco, r.range),
|
||||||
|
contents: contents.filter(Boolean),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const defProv = async (
|
||||||
|
model: Monaco["editor"]["ITextModel"],
|
||||||
|
position: Monaco["Position"],
|
||||||
|
) => {
|
||||||
|
const r = await client.sendRequest("textDocument/definition", {
|
||||||
|
...doc(model),
|
||||||
|
position: pos(position),
|
||||||
|
});
|
||||||
|
const locs = (Array.isArray(r) ? r : r ? [r] : []) as {
|
||||||
|
uri: string;
|
||||||
|
range: LspRange;
|
||||||
|
}[];
|
||||||
|
return locs.map((l) => ({
|
||||||
|
uri: monaco.Uri.parse(l.uri),
|
||||||
|
range: toMonacoRange(monaco, l.range),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (capabilities.definitionProvider) {
|
||||||
|
disposables.push(
|
||||||
|
monaco.languages.registerDefinitionProvider("typst", {
|
||||||
|
provideDefinition: defProv,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (capabilities.referencesProvider) {
|
||||||
|
disposables.push(
|
||||||
|
monaco.languages.registerReferenceProvider("typst", {
|
||||||
|
provideReferences: defProv,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (capabilities.documentSymbolProvider) {
|
||||||
|
disposables.push(
|
||||||
|
monaco.languages.registerDocumentSymbolProvider("typst", {
|
||||||
|
provideDocumentSymbols: async (model: ITextModel) => {
|
||||||
|
const r = await client.sendRequest(
|
||||||
|
"textDocument/documentSymbol",
|
||||||
|
doc(model),
|
||||||
|
);
|
||||||
|
return ((r as Record<string, unknown>[]) ?? []).map((s) => ({
|
||||||
|
name: s.name as string,
|
||||||
|
detail: (s.detail as string) ?? "",
|
||||||
|
kind: (s.kind as number) ?? 1,
|
||||||
|
range: toMonacoRange(monaco, s.range as LspRange),
|
||||||
|
selectionRange: toMonacoRange(monaco, s.selectionRange as LspRange),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (capabilities.documentFormattingProvider) {
|
||||||
|
disposables.push(
|
||||||
|
monaco.languages.registerDocumentFormattingEditProvider("typst", {
|
||||||
|
provideDocumentFormattingEdits: async (model: ITextModel) => {
|
||||||
|
const r = await client.sendRequest("textDocument/formatting", {
|
||||||
|
...doc(model),
|
||||||
|
options: { tabSize: 2, insertSpaces: true },
|
||||||
|
});
|
||||||
|
return ((r as { range: LspRange; newText: string }[]) ?? []).map(
|
||||||
|
(e) => ({
|
||||||
|
range: toMonacoRange(monaco, e.range),
|
||||||
|
text: e.newText,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return disposables;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Session bootstrap ----------
|
||||||
|
|
||||||
|
export async function startLsp(
|
||||||
|
monaco: Monaco,
|
||||||
|
projectName: string,
|
||||||
|
projectUri: string,
|
||||||
|
onFirstOpen?: (client: RawLspClient) => void,
|
||||||
|
): Promise<LspSession> {
|
||||||
|
const wsUrl = `${location.protocol === "https:" ? "wss:" : "ws:"}//${location.host}/lsp`;
|
||||||
|
const ws = new WebSocket(wsUrl);
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
const onOpen = () => {
|
||||||
|
ws.removeEventListener("open", onOpen);
|
||||||
|
ws.removeEventListener("error", onError);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
const onError = (e: Event) => {
|
||||||
|
ws.removeEventListener("open", onOpen);
|
||||||
|
ws.removeEventListener("error", onError);
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`WebSocket to ${wsUrl} failed: ${(e as ErrorEvent).message ?? "unknown"}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
ws.addEventListener("open", onOpen);
|
||||||
|
ws.addEventListener("error", onError);
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = new RawLspClient(ws);
|
||||||
|
|
||||||
|
// Diagnostics → Monaco markers (using the low‑level `setModelMarkers`
|
||||||
|
// because esbuild's tree‑shaking strips `createDiagnosticCollection`
|
||||||
|
// from standaloneLanguages when bundling monaco‑editor for the browser).
|
||||||
|
const diagOwner = "typst-leaf";
|
||||||
|
client.onNotification("textDocument/publishDiagnostics", (params) => {
|
||||||
|
const p = params as { uri: string; diagnostics: LspDiagnostic[] };
|
||||||
|
const model = monaco.editor.getModel(monaco.Uri.parse(p.uri));
|
||||||
|
if (model) {
|
||||||
|
monaco.editor.setModelMarkers(
|
||||||
|
model,
|
||||||
|
diagOwner,
|
||||||
|
p.diagnostics.map(toMarker),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Server -> client requests tinymist issues during init
|
||||||
|
client.onRequest("workspace/configuration", (params) => {
|
||||||
|
const items = (params as { items?: unknown[] }).items ?? [];
|
||||||
|
return items.map(() => ({}));
|
||||||
|
});
|
||||||
|
client.onRequest("workspace/workspaceFolders", () => [
|
||||||
|
{ uri: projectUri, name: projectName },
|
||||||
|
]);
|
||||||
|
client.onRequest("client/registerCapability", () => undefined);
|
||||||
|
client.onRequest("window/workDoneProgress/create", () => undefined);
|
||||||
|
|
||||||
|
// LSP handshake
|
||||||
|
// Force typst-preview into light mode by passing `dark` theme as
|
||||||
|
// `--invert-colors=never` during initialization. The preview
|
||||||
|
// `x-preview` input carries a `theme` (string) and a `version` (int);
|
||||||
|
// tinymist passes them through to the Typst document as `sys.inputs`
|
||||||
|
// so templates can read them. We set a light theme here so the
|
||||||
|
// rendering stays light from the very first compile.
|
||||||
|
const initResult = (await client.sendRequest("initialize", {
|
||||||
|
processId: null,
|
||||||
|
rootUri: projectUri,
|
||||||
|
capabilities: {
|
||||||
|
workspace: { configuration: true, workspaceFolders: true },
|
||||||
|
textDocument: {
|
||||||
|
synchronization: { openClose: true, change: 1 },
|
||||||
|
completion: {
|
||||||
|
completionItem: {
|
||||||
|
snippet: true,
|
||||||
|
documentationFormat: ["markdown", "plaintext"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hover: { contentFormat: ["markdown", "plaintext"] },
|
||||||
|
signatureHelp: {},
|
||||||
|
definition: {},
|
||||||
|
references: {},
|
||||||
|
documentSymbol: {},
|
||||||
|
formatting: {},
|
||||||
|
publishDiagnostics: { relatedInformation: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
trace: "off",
|
||||||
|
workspaceFolders: [{ uri: projectUri, name: projectName }],
|
||||||
|
initializationOptions: {
|
||||||
|
"tinymist.preview.browsing.args": [
|
||||||
|
"--data-plane-host=127.0.0.1:0",
|
||||||
|
"--invert-colors=never",
|
||||||
|
],
|
||||||
|
"tinymist.preview.invert_colors": "never",
|
||||||
|
},
|
||||||
|
})) as { capabilities?: Record<string, { [k: string]: unknown } | undefined> };
|
||||||
|
client.notify("initialized", {});
|
||||||
|
|
||||||
|
const capabilities = initResult.capabilities ?? {};
|
||||||
|
const providers = registerProviders(client, monaco, capabilities);
|
||||||
|
const binding = bindModels(client, monaco, () => onFirstOpen?.(client));
|
||||||
|
|
||||||
|
return {
|
||||||
|
client,
|
||||||
|
dispose: () => {
|
||||||
|
try {
|
||||||
|
binding.dispose();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
for (const p of providers) {
|
||||||
|
try {
|
||||||
|
p.dispose();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
client.dispose();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask tinymist to start a live (LSP-driven) preview and return the random
|
||||||
|
// data-plane port it assigned. The caller relays this port to the backend
|
||||||
|
// (POST /api/preview/sink) so the backend can proxy the preview webview.
|
||||||
|
export async function startPreview(
|
||||||
|
client: RawLspClient,
|
||||||
|
): Promise<number> {
|
||||||
|
const result = (await client.sendRequest("workspace/executeCommand", {
|
||||||
|
command: "tinymist.startDefaultPreview",
|
||||||
|
arguments: [{}],
|
||||||
|
})) as { dataPlanePort?: number } | null;
|
||||||
|
const port = result?.dataPlanePort;
|
||||||
|
if (typeof port !== "number") {
|
||||||
|
throw new Error("tinymist did not return a preview port");
|
||||||
|
}
|
||||||
|
return port;
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { loader, type Monaco } from "@monaco-editor/react";
|
||||||
|
import * as monaco from "monaco-editor";
|
||||||
|
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
|
||||||
|
|
||||||
|
let initialized = false;
|
||||||
|
let monacoPromise: Promise<Monaco> | null = null;
|
||||||
|
|
||||||
|
// A Monarch tokenizer for Typst covering the common surface (headings,
|
||||||
|
// script keywords, math, comments, emphasis, strings, raw blocks). This is
|
||||||
|
// syntax-only; the LSP additionally provides diagnostics and (optionally)
|
||||||
|
// semantic tokens.
|
||||||
|
const typstLanguageDefinition: monaco.languages.IMonarchLanguage = {
|
||||||
|
defaultToken: "",
|
||||||
|
tokenPostfix: ".typst",
|
||||||
|
brackets: [
|
||||||
|
{ open: "(", close: ")", token: "delimiter.parenthesis" },
|
||||||
|
{ open: "[", close: "]", token: "delimiter.square" },
|
||||||
|
{ open: "{", close: "}", token: "delimiter.curly" },
|
||||||
|
],
|
||||||
|
tokenizer: {
|
||||||
|
root: [
|
||||||
|
[/^=+\s.*$/, "keyword.heading"],
|
||||||
|
[/\*[^*]+\*/, "string.emphasis"],
|
||||||
|
[/_[^_]+_/, "string.emphasis"],
|
||||||
|
[/\/\/.*$/, "comment"],
|
||||||
|
[/\/\*/, "comment", "@comment"],
|
||||||
|
[/#\w+/, "keyword"], // #set, #let, #show, #if, ...
|
||||||
|
[/\$/, { token: "string", next: "@math" }],
|
||||||
|
[/"/, "string", "@string"],
|
||||||
|
[/[{}()\[\]]/, "@brackets"],
|
||||||
|
],
|
||||||
|
comment: [
|
||||||
|
[/\*\//, "comment", "@pop"],
|
||||||
|
[/[^*]+/, "comment"],
|
||||||
|
[/\*/, "comment"],
|
||||||
|
],
|
||||||
|
math: [
|
||||||
|
[/\$/, { token: "string", next: "@pop" }],
|
||||||
|
[/\\[a-zA-Z]+/, "keyword"],
|
||||||
|
[/[a-zA-Z]+/, "variable"],
|
||||||
|
[/[^\$]/, "string"],
|
||||||
|
],
|
||||||
|
string: [
|
||||||
|
[/[^"]/, "string"],
|
||||||
|
[/"/, "string", "@pop"],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function initEnvironment(): void {
|
||||||
|
if (initialized) return;
|
||||||
|
// Vite bundles the editor worker via `?worker`. Monaco only needs the base
|
||||||
|
// editor worker for plaintext/typst (no language-specific workers required).
|
||||||
|
self.MonacoEnvironment = {
|
||||||
|
getWorker: () => new editorWorker(),
|
||||||
|
};
|
||||||
|
loader.config({ monaco });
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadMonaco(): Promise<Monaco> {
|
||||||
|
if (!monacoPromise) {
|
||||||
|
initEnvironment();
|
||||||
|
// loader.init() resolves to the same `monaco` instance we configured above.
|
||||||
|
monacoPromise = loader.init().then((m) => {
|
||||||
|
m.languages.register({ id: "typst" });
|
||||||
|
m.languages.setMonarchTokensProvider("typst", typstLanguageDefinition);
|
||||||
|
return m;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return monacoPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMonaco(): Monaco {
|
||||||
|
return monaco as unknown as Monaco;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
export interface Project {
|
||||||
|
project: string;
|
||||||
|
uri: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectSummary {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileEntry {
|
||||||
|
path: string;
|
||||||
|
mtime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TreeResponse {
|
||||||
|
project: string;
|
||||||
|
files: FileEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileResponse {
|
||||||
|
path: string;
|
||||||
|
content: string;
|
||||||
|
mtime: number;
|
||||||
|
}
|
||||||
@@ -8,6 +8,15 @@ const host = process.env.TAURI_DEV_HOST;
|
|||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig(async () => ({
|
export default defineConfig(async () => ({
|
||||||
plugins: [tailwindcss(), react()],
|
plugins: [tailwindcss(), react()],
|
||||||
|
// Force esbuild to tree-shake as little as possible from monaco-editor.
|
||||||
|
// By default, Vite's pre-bundling applies tree-shaking which discards
|
||||||
|
// `monaco.languages.createDiagnosticCollection` and other standalone-editor
|
||||||
|
// services that we need at runtime.
|
||||||
|
optimizeDeps: {
|
||||||
|
esbuildOptions: {
|
||||||
|
treeShaking: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||||
//
|
//
|
||||||
@@ -29,5 +38,19 @@ export default defineConfig(async () => ({
|
|||||||
// 3. tell Vite to ignore watching `src-tauri`
|
// 3. tell Vite to ignore watching `src-tauri`
|
||||||
ignored: ["**/src-tauri/**"],
|
ignored: ["**/src-tauri/**"],
|
||||||
},
|
},
|
||||||
|
// Dev proxy: the SPA talks to the backend via relative URLs so that the
|
||||||
|
// same code works in dev (Vite :1420 → backend :4000) and in self-hosted
|
||||||
|
// prod (behind a reverse proxy). The preview webview and its WS are also
|
||||||
|
// proxied so the iframe is same-origin with the app.
|
||||||
|
proxy: {
|
||||||
|
"/api": "http://localhost:4000",
|
||||||
|
"/preview-app": {
|
||||||
|
target: "http://localhost:4000",
|
||||||
|
// the webview is HTML; rewrite so the path is preserved for upstream
|
||||||
|
ws: false,
|
||||||
|
},
|
||||||
|
"/lsp": { target: "ws://localhost:4000", ws: true },
|
||||||
|
"/preview": { target: "ws://localhost:4000", ws: true },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user