typst-mcp 1.0.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -87
- package/dist/client/compiler.js +234 -6
- package/dist/client/typst-client.js +306 -52
- package/dist/index.js +97 -5
- package/dist/protocol/edit-diff.js +330 -0
- package/dist/protocol/search-replace.js +97 -0
- package/dist/protocol/yjs-doc.js +31 -38
- package/dist/tools/definitions.js +123 -6
- package/dist/utils/file-mutation-queue.js +30 -0
- package/dist/utils/mime.js +23 -0
- package/dist/utils/truncate.js +114 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -4,91 +4,39 @@
|
|
|
4
4
|
|
|
5
5
|
**Model Context Protocol (MCP) server for [`https://typst.app`](https://typst.app) — cloud projects, Yjs CRDT editing, and Typst compiler diagnostics**
|
|
6
6
|
|
|
7
|
-
Install from npm as [`typst-mcp`](https://www.npmjs.com/package/typst-mcp). Source repo: [`typst-web-mcp`](https://github.com/caphefalumi/typst-web-mcp).
|
|
8
|
-
|
|
9
7
|
[](https://www.npmjs.com/package/typst-mcp)
|
|
10
8
|
[](https://www.npmjs.com/package/typst-mcp)
|
|
11
9
|
[](./LICENSE)
|
|
12
|
-
[](https://github.com/caphefalumi/typst-web-mcp/actions/workflows/ci.yml)
|
|
13
10
|
[](https://github.com/caphefalumi/typst-web-mcp/actions/workflows/npm-publish.yml)
|
|
14
|
-
[](https://bun.sh)
|
|
15
|
-
[](https://www.typescriptlang.org/)
|
|
16
|
-
[](https://modelcontextprotocol.io)
|
|
17
|
-
[](./docs/PROTOCOL.md)
|
|
18
|
-
[-2ea44f?style=flat-square)](./test/typst-mcp.test.ts)
|
|
19
11
|
|
|
20
12
|
</div>
|
|
21
13
|
|
|
22
14
|
---
|
|
23
15
|
|
|
24
|
-
## Overview
|
|
25
|
-
|
|
26
|
-
`typst-web-mcp` connects AI coding and writing agents directly to collaborative [**typst.app**](https://typst.app) cloud projects. Instead of relying on brittle browser DOM scraping, it implements the native `typst.app` client protocol:
|
|
27
|
-
|
|
28
|
-
- **Native Yjs CRDT Text Sync:** Decodes and mutates multiplayer `Y.Doc` (`doc.getText("text")`) state vectors so surgical edits preserve collaborator cursors and version history.
|
|
29
|
-
- **Binary `/editing/v3` WebSocket Wire Protocol:** Encodes and decodes `lib0` variable-length binary frames (`Subscribe`, `TextUpdate`, `Pong`, `Refresh`, `Presence`, `Cursor`, `Leave`) with REST `/v1/files/:projectId/:fileId/yjs` persistence.
|
|
30
|
-
- **GraphQL & REST Project Management:** Full project CRUD, file tree inspection, rename/move, delete, and binary figure/asset uploads (`PNG`, `JPG`, `SVG`, `PDF`, fonts).
|
|
31
|
-
- **Direct Authentication + Fallback:** Authenticates via `GET /v1/to-signin` + `POST /v1/auth/signin` (`__Http-typst-session`), with persistent session caching and automatic cookie/Playwright fallback on challenge.
|
|
32
|
-
- **Compiler Diagnostics, Preview Screenshots & PDF Export:** Synchronizes cloud Yjs text and binary assets into an isolated workspace and runs the Typst compiler to return structured line/column diagnostics, high-DPI PNG preview screenshots (`typst_capture_screenshot`), and compiled PDFs (`typst_export_pdf`).
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
## Architecture
|
|
37
|
-
|
|
38
|
-
```mermaid
|
|
39
|
-
flowchart LR
|
|
40
|
-
Agent["AI Agent\n(Pi / Claude / Cursor)"] <-->|"JSON-RPC (stdio)"| MCP["typst-mcp\n(Node / TypeScript)"]
|
|
41
|
-
MCP <-->|"GraphQL (/v1/graphql)\n+ X-Typst-Preflight"| API["https://api.typst.app"]
|
|
42
|
-
MCP <-->|"REST Yjs & Blobs\n(/v1/files/:pid/:fid/yjs)"| API
|
|
43
|
-
MCP <-->|"Binary lib0 WebSocket\n(wss://api.typst.app/editing/v3)"| WS["typst.app Real-Time Sync"]
|
|
44
|
-
MCP -->|"Workspace Sync + Compile"| TypstCLI["Typst Compiler\n(Diagnostics & PDF)"]
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
See [`docs/PROTOCOL.md`](./docs/PROTOCOL.md) for the complete reverse-engineered protocol specification.
|
|
48
|
-
|
|
49
|
-
---
|
|
50
|
-
|
|
51
|
-
## MCP Tools (16 Tools)
|
|
52
|
-
|
|
53
|
-
| Category | Tool Name | Description |
|
|
54
|
-
|:---|:---|:---|
|
|
55
|
-
| **Auth** | `typst_auth_login` | Authenticate with `https://typst.app` via email/password (`POST /v1/auth/signin`) or `__Http-typst-session` cookie, with browser fallback. |
|
|
56
|
-
| **Auth** | `typst_auth_status` | Check active authentication state, login method, and stored session cookies. |
|
|
57
|
-
| **Projects** | `typst_list_projects` | List all owned and shared/external projects in the user's `typst.app` dashboard (`DashboardContent`). |
|
|
58
|
-
| **Projects** | `typst_get_project` | Retrieve full project metadata, access permissions, pinned compiler version, and file tree (`ProjectFiles`). |
|
|
59
|
-
| **Projects** | `typst_create_project` | Create a new `typst.app` cloud project with initial `.typ`/`.bib` files and pinned compiler version. |
|
|
60
|
-
| **Projects** | `typst_delete_project` | Permanently delete a `typst.app` project by ID (`DeleteProjectMutation`). |
|
|
61
|
-
| **Files & CRDT** | `typst_list_files` | List all multiplayer Yjs text files and binary asset blobs in a project. |
|
|
62
|
-
| **Files & CRDT** | `typst_read_file` | Read any project file — decodes binary Yjs CRDT state (`doc.getText("text")`) into UTF-8 text or base64 for binary assets. |
|
|
63
|
-
| **Files & CRDT** | `typst_write_file` | Create or overwrite a `.typ`/`.bib`/`.toml` file, syncing Yjs state diffs over `/editing/v3` WebSocket and REST. |
|
|
64
|
-
| **Files & CRDT** | `typst_edit_file` | Apply non-overlapping surgical `oldText -> newText` replacements directly to the `Y.Text("text")` CRDT. |
|
|
65
|
-
| **Assets & Tree** | `typst_upload_asset` | Upload or replace binary assets (`PNG`, `JPG`, `SVG`, `PDF`, fonts) from a local path or base64 payload. |
|
|
66
|
-
| **Assets & Tree** | `typst_rename_file` | Rename or move a file inside a `typst.app` project (`UpdatesFileMutation`). |
|
|
67
|
-
| **Assets & Tree** | `typst_delete_file` | Delete a file or binary asset from a project (`DeleteFilesMutation`). |
|
|
68
|
-
| **Compiler** | `typst_get_diagnostics` | Sync all cloud files and run `typst compile` to return structured errors, warnings, line/column spans, and hints. |
|
|
69
|
-
| **Compiler** | `typst_export_pdf` | Sync all cloud text & binary assets, compile the project to PDF, and save to `outputPath`. |
|
|
70
|
-
| **Preview** | `typst_capture_screenshot` | Capture high-DPI PNG preview screenshots (`mode="pages"` renders compiled PNG pages; `mode="browser"` screenshots the live `typst.app` web UI). |
|
|
71
|
-
|
|
72
|
-
---
|
|
73
|
-
|
|
74
16
|
## Quick Start
|
|
75
17
|
|
|
76
18
|
### Prerequisites
|
|
77
19
|
|
|
78
20
|
- [**Node.js**](https://nodejs.org/) `>= 18` (for `npx` install) or [**Bun**](https://bun.sh) `>= 1.2` (for local development)
|
|
79
|
-
- [**Typst CLI**](https://github.com/typst/typst) (`typst` on `PATH` for `typst_get_diagnostics`
|
|
21
|
+
- [**Typst CLI**](https://github.com/typst/typst) (optional: `typst` on `PATH` for local CLI compilation speed; if not installed, `typst_get_diagnostics` automatically uses Typst Web cloud compiler diagnostics)
|
|
80
22
|
|
|
81
23
|
### Installation
|
|
82
24
|
|
|
83
|
-
**
|
|
25
|
+
**Option 1: Quick start (no clone, no global install):**
|
|
84
26
|
|
|
85
27
|
```bash
|
|
86
28
|
npx -y typst-mcp
|
|
87
29
|
```
|
|
88
30
|
|
|
89
|
-
|
|
31
|
+
**Option 2: Always run latest release (bypasses local `npx` cache):**
|
|
90
32
|
|
|
91
|
-
|
|
33
|
+
```bash
|
|
34
|
+
npx -y typst-mcp@latest
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Runs the published [`typst-mcp`](https://www.npmjs.com/package/typst-mcp) package (Node `>= 18`). `-y` skips the npx confirmation prompt.
|
|
38
|
+
|
|
39
|
+
**Option 3: From source (development):**
|
|
92
40
|
|
|
93
41
|
```bash
|
|
94
42
|
git clone https://github.com/caphefalumi/typst-web-mcp.git
|
|
@@ -116,10 +64,49 @@ Add `typst-mcp` to your MCP client configuration (`.pi/agent/mcp-adapter.json`,
|
|
|
116
64
|
}
|
|
117
65
|
```
|
|
118
66
|
|
|
67
|
+
> **Tip:** You can also set `"args": ["-y", "typst-mcp@latest"]` to guarantee your MCP client automatically pulls and runs the newest version on launch.
|
|
68
|
+
|
|
119
69
|
> **Session Persistence:** Once authenticated via `typst_auth_login` (or environment variables), the `__Http-typst-session` cookie is cached locally at `~/.pi/agent/typst-mcp-session.json` so subsequent tool calls authenticate automatically without re-signing in.
|
|
120
70
|
|
|
121
71
|
---
|
|
122
72
|
|
|
73
|
+
## Overview
|
|
74
|
+
|
|
75
|
+
`typst-web-mcp` connects AI coding and writing agents directly to collaborative [**typst.app**](https://typst.app) cloud projects. Instead of relying on brittle browser DOM scraping, it implements the native `typst.app` client protocol:
|
|
76
|
+
|
|
77
|
+
- **Native Yjs CRDT Text Sync:** Decodes and mutates multiplayer `Y.Doc` (`doc.getText("text")`) state vectors so surgical edits preserve collaborator cursors and version history.
|
|
78
|
+
- **Binary `/editing/v3` WebSocket Wire Protocol:** Encodes and decodes `lib0` variable-length binary frames (`Subscribe`, `TextUpdate`, `Pong`, `Refresh`, `Presence`, `Cursor`, `Leave`) with REST `/v1/files/:projectId/:fileId/yjs` persistence.
|
|
79
|
+
- **GraphQL & REST Project Management:** Full project CRUD, file tree inspection, rename/move, delete, and binary figure/asset uploads (`PNG`, `JPG`, `SVG`, `PDF`, fonts).
|
|
80
|
+
- **Direct Authentication + Fallback:** Authenticates via `GET /v1/to-signin` + `POST /v1/auth/signin` (`__Http-typst-session`), with persistent session caching and automatic cookie/Playwright fallback on challenge.
|
|
81
|
+
- **Compiler Diagnostics, Preview Screenshots & PDF Export:** Synchronizes cloud Yjs text and binary assets into an isolated workspace and runs the Typst compiler to return structured line/column diagnostics, high-DPI PNG preview screenshots (`typst_capture_screenshot`), and compiled PDFs (`typst_export_pdf`).
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## MCP Tools (18 Tools)
|
|
86
|
+
|
|
87
|
+
| Category | Tool Name | Description |
|
|
88
|
+
|:---|:---|:---|
|
|
89
|
+
| **Auth** | `typst_auth_login` | Authenticate with `https://typst.app` via email/password (`POST /v1/auth/signin`) or `__Http-typst-session` cookie, with browser fallback. |
|
|
90
|
+
| **Auth** | `typst_auth_status` | Check active authentication state, login method, and stored session cookies. |
|
|
91
|
+
| **Projects** | `typst_list_projects` | List all owned and shared/external projects in the user's `typst.app` dashboard (`DashboardContent`). |
|
|
92
|
+
| **Projects** | `typst_get_project` | Retrieve full project metadata, access permissions, pinned compiler version, and file tree (`ProjectFiles`). |
|
|
93
|
+
| **Projects** | `typst_create_project` | Create a new `typst.app` cloud project with initial `.typ`/`.bib` files and pinned compiler version. |
|
|
94
|
+
| **Projects** | `typst_delete_project` | Permanently delete a `typst.app` project by ID (`DeleteProjectMutation`). |
|
|
95
|
+
| **Files & CRDT** | `typst_list_files` | List all multiplayer Yjs text files and binary asset blobs in a project. |
|
|
96
|
+
| **Files & CRDT** | `typst_read_file` | Read any project file with offset/limit pagination, optional line numbering, truncation limits (2,000 lines/50KB), and multimodal MCP image content blocks (`PNG`, `JPG`, `SVG`, `WebP`). |
|
|
97
|
+
| **Files & CRDT** | `typst_write_file` | Create or overwrite a `.typ`/`.bib`/`.toml` file, protected by per-file mutation queue serialization, syncing Yjs state diffs over `/editing/v3` WebSocket and REST. |
|
|
98
|
+
| **Files & CRDT** | `typst_edit_file` | Apply surgical `oldText -> newText` replacements directly to the `Y.Text("text")` CRDT with CRLF/BOM resilience, fuzzy quote/dash matching, unified diff & patch output, and per-file mutation serialization. |
|
|
99
|
+
| **Search & Replace** | `typst_grep_project` | Search for text or regular expressions across project files, returning file paths, line numbers, and optional surrounding context lines. |
|
|
100
|
+
| **Search & Replace** | `typst_find_and_replace` | Find and replace text or regex across a single file or project-wide with regex capture groups (`$1`), dry-run preview diffs, and per-file mutation serialization. |
|
|
101
|
+
| **Assets & Tree** | `typst_upload_asset` | Upload or replace binary assets (`PNG`, `JPG`, `SVG`, `PDF`, fonts) from a local path or base64 payload. |
|
|
102
|
+
| **Assets & Tree** | `typst_rename_file` | Rename or move a file inside a `typst.app` project (`UpdatesFileMutation`). |
|
|
103
|
+
| **Assets & Tree** | `typst_delete_file` | Delete a file or binary asset from a project (`DeleteFilesMutation`). |
|
|
104
|
+
| **Compiler** | `typst_get_diagnostics` | Retrieve compiler diagnostics (errors, warnings, line/column numbers, and hints). Supports both local Typst CLI (`mode="cli"`) and Typst Web cloud compiler (`mode="web"`), with automatic fallback (`mode="auto"`). |
|
|
105
|
+
| **Compiler** | `typst_export_pdf` | Sync all cloud text & binary assets, compile the project to PDF, and save to `outputPath`. |
|
|
106
|
+
| **Preview** | `typst_capture_screenshot` | Capture high-DPI PNG preview screenshots (`mode="pages"` renders compiled PNG pages; `mode="browser"` screenshots the live `typst.app` web UI). |
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
123
110
|
## Development & Verification
|
|
124
111
|
|
|
125
112
|
```bash
|
|
@@ -149,31 +136,6 @@ Same flow as [Canvas-CLI](https://github.com/caphefalumi/Canvas-CLI):
|
|
|
149
136
|
|
|
150
137
|
**Trusted publisher (maintainers):** package [`typst-mcp`](https://www.npmjs.com/package/typst-mcp) → Settings → Trusted publishing → GitHub Actions — user/org `caphefalumi`, repo `typst-web-mcp`, workflow file `npm-publish.yml`, environment `npm-publish`, allow **npm publish**.
|
|
151
138
|
|
|
152
|
-
### Project Structure
|
|
153
|
-
|
|
154
|
-
```text
|
|
155
|
-
typst-web-mcp/
|
|
156
|
-
├── docs/
|
|
157
|
-
│ └── PROTOCOL.md # Reverse-engineered typst.app REST, GraphQL, Yjs & WS v3 spec
|
|
158
|
-
├── src/
|
|
159
|
-
│ ├── protocol/
|
|
160
|
-
│ │ ├── ws-codec.ts # Binary lib0 /editing/v3 WebSocket encoder & decoder
|
|
161
|
-
│ │ ├── yjs-doc.ts # Yjs CRDT text decoding, replacement & surgical patching
|
|
162
|
-
│ │ └── graphql.ts # GraphQL queries, mutations, fragments & types
|
|
163
|
-
│ ├── client/
|
|
164
|
-
│ │ ├── auth.ts # Direct /v1/auth/signin login, cookie jar & Playwright fallback
|
|
165
|
-
│ │ ├── compiler.ts # Typst binary resolver, diagnostic parser, PDF & PNG preview compiler
|
|
166
|
-
│ │ ├── errors.ts # Structured TypstLoadError & error formatting helpers
|
|
167
|
-
│ │ └── typst-client.ts # TypstCloudClient (REST, GraphQL, WebSocket v3, Compiler facade)
|
|
168
|
-
│ ├── tools/
|
|
169
|
-
│ │ └── definitions.ts # MCP JSON Schema definitions for all 16 tools
|
|
170
|
-
│ └── index.ts # MCP stdio server entrypoint (dev); npm bin runs dist/index.js
|
|
171
|
-
├── test/
|
|
172
|
-
│ └── typst-mcp.test.ts # 7 test suites (57 assertions) covering codecs, CRDTs & E2E
|
|
173
|
-
├── LICENSE # MIT License
|
|
174
|
-
└── package.json
|
|
175
|
-
```
|
|
176
|
-
|
|
177
139
|
---
|
|
178
140
|
|
|
179
141
|
## License
|
package/dist/client/compiler.js
CHANGED
|
@@ -29,6 +29,52 @@ export function resolveTypstBinary(explicitBin) {
|
|
|
29
29
|
}
|
|
30
30
|
return "typst";
|
|
31
31
|
}
|
|
32
|
+
export function hasTypstBinary(explicitBin) {
|
|
33
|
+
try {
|
|
34
|
+
const bin = resolveTypstBinary(explicitBin);
|
|
35
|
+
if (bin !== "typst" && existsSync(bin)) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
const proc = spawnSync(bin, ["--version"], {
|
|
39
|
+
stdio: "ignore",
|
|
40
|
+
encoding: "utf8",
|
|
41
|
+
});
|
|
42
|
+
return proc.status === 0 && !proc.error;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function resolvePythonBinary() {
|
|
49
|
+
if (process.env.PYTHON_BIN) {
|
|
50
|
+
return process.env.PYTHON_BIN;
|
|
51
|
+
}
|
|
52
|
+
for (const cmd of ["python", "python3", "py"]) {
|
|
53
|
+
try {
|
|
54
|
+
const res = spawnSync(cmd, ["--version"], { stdio: "ignore" });
|
|
55
|
+
if (res.status === 0) {
|
|
56
|
+
return cmd;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch { }
|
|
60
|
+
}
|
|
61
|
+
return "python";
|
|
62
|
+
}
|
|
63
|
+
export function offsetToPosition(text, offset) {
|
|
64
|
+
let line = 1;
|
|
65
|
+
let column = 1;
|
|
66
|
+
const target = Math.min(Math.max(0, offset), text.length);
|
|
67
|
+
for (let i = 0; i < target; i++) {
|
|
68
|
+
if (text[i] === "\n") {
|
|
69
|
+
line++;
|
|
70
|
+
column = 1;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
column++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { line, column };
|
|
77
|
+
}
|
|
32
78
|
function hasValidPngSignature(bytes) {
|
|
33
79
|
return (bytes.byteLength >= 8 &&
|
|
34
80
|
bytes[0] === 0x89 &&
|
|
@@ -95,6 +141,9 @@ export function compileWorkspaceDiagnostics(params) {
|
|
|
95
141
|
cwd: params.workspaceDir,
|
|
96
142
|
encoding: "utf8",
|
|
97
143
|
});
|
|
144
|
+
if (proc.error) {
|
|
145
|
+
throw new TypstLoadError(["TYPST_EXECUTION_FAILED"], `Failed to execute Typst compiler binary '${params.typstBin}': ${proc.error.message}`);
|
|
146
|
+
}
|
|
98
147
|
const combinedOutput = `${proc.stderr ?? ""}\n${proc.stdout ?? ""}`;
|
|
99
148
|
const diagnostics = parseShortDiagnostics(combinedOutput).map((d) => ({
|
|
100
149
|
...d,
|
|
@@ -130,6 +179,9 @@ export function compileWorkspacePdf(params) {
|
|
|
130
179
|
cwd: params.workspaceDir,
|
|
131
180
|
encoding: "utf8",
|
|
132
181
|
});
|
|
182
|
+
if (proc.error) {
|
|
183
|
+
throw new TypstLoadError(["TYPST_EXECUTION_FAILED"], `Failed to execute Typst compiler binary '${params.typstBin}': ${proc.error.message}`);
|
|
184
|
+
}
|
|
133
185
|
const combinedOutput = `${proc.stderr ?? ""}\n${proc.stdout ?? ""}`;
|
|
134
186
|
const diagnostics = parseShortDiagnostics(combinedOutput);
|
|
135
187
|
if (proc.status !== 0) {
|
|
@@ -148,12 +200,169 @@ export function compileWorkspacePdf(params) {
|
|
|
148
200
|
diagnostics,
|
|
149
201
|
};
|
|
150
202
|
}
|
|
203
|
+
export function captureWebDiagnostics(params) {
|
|
204
|
+
const pythonBin = resolvePythonBinary();
|
|
205
|
+
const timeoutSec = ((params.timeoutMs ?? 25000) / 1000).toFixed(1);
|
|
206
|
+
const pyScript = `
|
|
207
|
+
import os, sys, json, time
|
|
208
|
+
from playwright.sync_api import sync_playwright
|
|
209
|
+
|
|
210
|
+
app_url = os.environ.get("TYPST_APP_URL", "https://typst.app")
|
|
211
|
+
proj_id = os.environ["TYPST_PROJ_ID"]
|
|
212
|
+
raw_cookie = os.environ.get("TYPST_COOKIE", "")
|
|
213
|
+
timeout_sec = float(os.environ.get("TYPST_TIMEOUT_SEC", "25.0"))
|
|
214
|
+
|
|
215
|
+
cookies = []
|
|
216
|
+
for part in raw_cookie.split(";"):
|
|
217
|
+
part = part.strip()
|
|
218
|
+
if "=" in part:
|
|
219
|
+
k, v = part.split("=", 1)
|
|
220
|
+
name = k.strip()
|
|
221
|
+
val = v.strip()
|
|
222
|
+
is_session = "__Http" in name or "session" in name
|
|
223
|
+
cookies.append({
|
|
224
|
+
"name": name,
|
|
225
|
+
"value": val,
|
|
226
|
+
"domain": ".typst.app",
|
|
227
|
+
"path": "/",
|
|
228
|
+
"httpOnly": is_session,
|
|
229
|
+
"secure": True,
|
|
230
|
+
"sameSite": "Strict" if is_session else "Lax",
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
with sync_playwright() as p:
|
|
234
|
+
browser = None
|
|
235
|
+
for channel in ["msedge", "chrome", None]:
|
|
236
|
+
try:
|
|
237
|
+
if channel:
|
|
238
|
+
browser = p.chromium.launch(channel=channel, headless=True)
|
|
239
|
+
else:
|
|
240
|
+
browser = p.chromium.launch(headless=True)
|
|
241
|
+
break
|
|
242
|
+
except Exception:
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
if browser is None:
|
|
246
|
+
sys.stderr.write("NO_BROWSER_FOUND")
|
|
247
|
+
sys.exit(2)
|
|
248
|
+
|
|
249
|
+
ctx = browser.new_context(viewport={"width": 1920, "height": 1080})
|
|
250
|
+
if cookies:
|
|
251
|
+
ctx.add_cookies(cookies)
|
|
252
|
+
|
|
253
|
+
ctx.add_init_script("""
|
|
254
|
+
window.__typst_diagnostics = null;
|
|
255
|
+
window.__typst_status = 'starting';
|
|
256
|
+
const OrigWorker = window.Worker;
|
|
257
|
+
window.Worker = function(url, opts) {
|
|
258
|
+
const w = new OrigWorker(url, opts);
|
|
259
|
+
w.addEventListener('message', (ev) => {
|
|
260
|
+
try {
|
|
261
|
+
if (ev.data && ev.data.tag === 'notification') {
|
|
262
|
+
if (ev.data.name === 'diagnostics') {
|
|
263
|
+
window.__typst_diagnostics = ev.data.notification?.diagnostics || [];
|
|
264
|
+
} else if (ev.data.name === 'status') {
|
|
265
|
+
window.__typst_status = ev.data.notification?.status || 'idle';
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
} catch(e) {}
|
|
269
|
+
});
|
|
270
|
+
return w;
|
|
271
|
+
};
|
|
272
|
+
""")
|
|
273
|
+
|
|
274
|
+
page = ctx.new_page()
|
|
275
|
+
page.goto(f"{app_url}/project/{proj_id}", wait_until="commit", timeout=45000)
|
|
276
|
+
|
|
277
|
+
start_time = time.time()
|
|
278
|
+
diags = None
|
|
279
|
+
while time.time() - start_time < timeout_sec:
|
|
280
|
+
data = page.evaluate("({ diag: window.__typst_diagnostics, status: window.__typst_status, url: window.location.href })")
|
|
281
|
+
if "signin" in data.get("url", ""):
|
|
282
|
+
browser.close()
|
|
283
|
+
sys.stderr.write(f"AUTH_REQUIRED: Redirected to {data.get('url')}")
|
|
284
|
+
sys.exit(3)
|
|
285
|
+
if data.get("diag") is not None:
|
|
286
|
+
diags = data.get("diag")
|
|
287
|
+
if data.get("status") == "idle":
|
|
288
|
+
break
|
|
289
|
+
page.wait_for_timeout(300)
|
|
290
|
+
|
|
291
|
+
browser.close()
|
|
292
|
+
print(json.dumps(diags or []))
|
|
293
|
+
`;
|
|
294
|
+
const proc = spawnSync(pythonBin, ["-c", pyScript], {
|
|
295
|
+
env: {
|
|
296
|
+
...process.env,
|
|
297
|
+
TYPST_APP_URL: params.appUrl,
|
|
298
|
+
TYPST_PROJ_ID: params.projectId,
|
|
299
|
+
TYPST_COOKIE: params.cookieHeader,
|
|
300
|
+
TYPST_TIMEOUT_SEC: timeoutSec,
|
|
301
|
+
},
|
|
302
|
+
encoding: "utf8",
|
|
303
|
+
timeout: (params.timeoutMs ?? 25000) + 15000,
|
|
304
|
+
});
|
|
305
|
+
if (proc.error || proc.status !== 0) {
|
|
306
|
+
const stderr = (proc.stderr || proc.error?.message || "").trim();
|
|
307
|
+
if (stderr.includes("NO_BROWSER_FOUND")) {
|
|
308
|
+
throw new TypstLoadError(["NO_BROWSER_FOUND"], "No compatible browser (Edge, Chrome, or Chromium) found for Playwright web diagnostics.");
|
|
309
|
+
}
|
|
310
|
+
if (stderr.includes("AUTH_REQUIRED")) {
|
|
311
|
+
throw new TypstLoadError(["UNAUTHENTICATED"], `Typst Web cloud session expired or invalid: ${stderr}`);
|
|
312
|
+
}
|
|
313
|
+
throw new TypstLoadError(["WEB_DIAGNOSTICS_FAILED"], `Failed to capture Typst Web cloud diagnostics: ${stderr || "Ensure python with playwright is installed ('pip install playwright && playwright install')"}`);
|
|
314
|
+
}
|
|
315
|
+
const rawJson = (proc.stdout || "").trim();
|
|
316
|
+
let rawList = [];
|
|
317
|
+
try {
|
|
318
|
+
rawList = JSON.parse(rawJson);
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
throw new TypstLoadError(["WEB_DIAGNOSTICS_PARSE_FAILED"], `Failed to parse Typst Web diagnostics output: ${err.message}`);
|
|
322
|
+
}
|
|
323
|
+
const mainFileName = params.mainFile ?? "main.typ";
|
|
324
|
+
const diagnostics = rawList.map((raw) => {
|
|
325
|
+
let line = 1;
|
|
326
|
+
let column = 1;
|
|
327
|
+
const filePath = raw.path || mainFileName;
|
|
328
|
+
const fileText = params.fileContents?.[filePath];
|
|
329
|
+
if (fileText && raw.range?.start !== undefined) {
|
|
330
|
+
const pos = offsetToPosition(fileText, raw.range.start);
|
|
331
|
+
line = pos.line;
|
|
332
|
+
column = pos.column;
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
path: filePath,
|
|
336
|
+
line,
|
|
337
|
+
column,
|
|
338
|
+
severity: raw.severity === "error" ? "error" : "warning",
|
|
339
|
+
message: raw.message,
|
|
340
|
+
hints: Array.isArray(raw.hints) ? raw.hints : [],
|
|
341
|
+
package: raw.package ?? null,
|
|
342
|
+
range: raw.range
|
|
343
|
+
? { start: raw.range.start, end: raw.range.end }
|
|
344
|
+
: undefined,
|
|
345
|
+
};
|
|
346
|
+
});
|
|
347
|
+
const errorCount = diagnostics.filter((d) => d.severity === "error").length;
|
|
348
|
+
const warningCount = diagnostics.filter((d) => d.severity === "warning").length;
|
|
349
|
+
return {
|
|
350
|
+
projectId: params.projectId,
|
|
351
|
+
mainFile: mainFileName,
|
|
352
|
+
mode: "web",
|
|
353
|
+
success: errorCount === 0,
|
|
354
|
+
errorCount,
|
|
355
|
+
warningCount,
|
|
356
|
+
diagnostics,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
151
359
|
export function captureBrowserPreviewScreenshot(params) {
|
|
152
360
|
const outputDir = resolveOutputDirectory(params.outputDir, `typst-preview-ui-${params.projectId}-`);
|
|
153
361
|
mkdirSync(outputDir, { recursive: true });
|
|
154
362
|
const screenshotPath = join(outputDir, `typst-app-${params.projectId}.png`);
|
|
363
|
+
const pythonBin = resolvePythonBinary();
|
|
155
364
|
const pyScript = `
|
|
156
|
-
import os
|
|
365
|
+
import os, sys
|
|
157
366
|
from playwright.sync_api import sync_playwright
|
|
158
367
|
|
|
159
368
|
app_url = os.environ["TYPST_PREVIEW_APP_URL"]
|
|
@@ -166,16 +375,35 @@ for part in raw_cookie.split(";"):
|
|
|
166
375
|
part = part.strip()
|
|
167
376
|
if "=" in part:
|
|
168
377
|
k, v = part.split("=", 1)
|
|
378
|
+
name = k.strip()
|
|
379
|
+
val = v.strip()
|
|
380
|
+
is_session = "__Http" in name or "session" in name
|
|
169
381
|
cookies.append({
|
|
170
|
-
"name":
|
|
171
|
-
"value":
|
|
172
|
-
"domain": "typst.app",
|
|
382
|
+
"name": name,
|
|
383
|
+
"value": val,
|
|
384
|
+
"domain": ".typst.app",
|
|
173
385
|
"path": "/",
|
|
386
|
+
"httpOnly": is_session,
|
|
174
387
|
"secure": True,
|
|
388
|
+
"sameSite": "Strict" if is_session else "Lax",
|
|
175
389
|
})
|
|
176
390
|
|
|
177
391
|
with sync_playwright() as p:
|
|
178
|
-
browser =
|
|
392
|
+
browser = None
|
|
393
|
+
for channel in ["msedge", "chrome", None]:
|
|
394
|
+
try:
|
|
395
|
+
if channel:
|
|
396
|
+
browser = p.chromium.launch(channel=channel, headless=True)
|
|
397
|
+
else:
|
|
398
|
+
browser = p.chromium.launch(headless=True)
|
|
399
|
+
break
|
|
400
|
+
except Exception:
|
|
401
|
+
continue
|
|
402
|
+
|
|
403
|
+
if browser is None:
|
|
404
|
+
sys.stderr.write("NO_BROWSER_FOUND")
|
|
405
|
+
sys.exit(2)
|
|
406
|
+
|
|
179
407
|
ctx = browser.new_context(viewport={"width": 1920, "height": 1080}, device_scale_factor=2)
|
|
180
408
|
if cookies:
|
|
181
409
|
ctx.add_cookies(cookies)
|
|
@@ -185,7 +413,7 @@ with sync_playwright() as p:
|
|
|
185
413
|
page.screenshot(path=out_path, full_page=False)
|
|
186
414
|
browser.close()
|
|
187
415
|
`;
|
|
188
|
-
const proc = spawnSync(
|
|
416
|
+
const proc = spawnSync(pythonBin, ["-c", pyScript], {
|
|
189
417
|
env: {
|
|
190
418
|
...process.env,
|
|
191
419
|
TYPST_PREVIEW_APP_URL: params.appUrl,
|