qtex 1.1.3 → 1.1.4

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 CHANGED
@@ -29,12 +29,12 @@
29
29
 
30
30
  **macOS / Linux**
31
31
  ```bash
32
- curl -fsSL https://raw.githubusercontent.com/srsergiolazaro/qtex/main/install.sh | bash
32
+ curl -fsSL https://srsergiolazaro.github.io/qtex/install.sh | bash
33
33
  ```
34
34
 
35
35
  **Windows (PowerShell)**
36
36
  ```powershell
37
- irm https://raw.githubusercontent.com/srsergiolazaro/qtex/main/install.ps1 | iex
37
+ irm https://srsergiolazaro.github.io/qtex/install.ps1 | iex
38
38
  ```
39
39
 
40
40
  **Or use directly with npx (zero install):**
package/index.js CHANGED
@@ -60,20 +60,24 @@ ${colors.bold}OPTIONS:${colors.reset}
60
60
  autoUpdate(packageJson.version);
61
61
 
62
62
  if (values.watch) {
63
+ const { TachyonWS } = await import('./src/ws-client.js');
64
+ const wsClient = new TachyonWS(directory, values);
65
+ await wsClient.connect();
66
+
63
67
  const server = startServer();
64
68
  const viewUrl = `http://localhost:${server.port}/view`;
65
69
  ui.info(`Watching for changes in: ${colors.bold}${directory}${colors.reset}`);
66
70
  ui.info(`View PDF at: ${colors.blue}${colors.underline}${viewUrl}${colors.reset}`);
67
71
 
68
- await compile(directory, values);
72
+ // Initial compile over WS
73
+ await wsClient.sendProject();
69
74
 
70
75
  // Auto-open browser
71
76
  const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
72
77
  exec(`${openCmd} "${viewUrl}"`);
73
78
 
74
- let isCompiling = false;
75
79
  watch(directory, { recursive: true }, async (event, filename) => {
76
- if (filename && !filename.startsWith('.') && !isCompiling) {
80
+ if (filename && !filename.startsWith('.') && !wsClient.isCompiling) {
77
81
  const ext = extname(filename).toLowerCase();
78
82
  const outputFileName = values.output || 'output.pdf';
79
83
 
@@ -82,10 +86,7 @@ ${colors.bold}OPTIONS:${colors.reset}
82
86
  const isOutputFile = basename(filename) === basename(outputFileName);
83
87
 
84
88
  if (watchExts.includes(ext) && !isOutputFile) {
85
- isCompiling = true;
86
- console.log(`\n${colors.blue}🔄 Change detected in ${filename}, recompiling...${colors.reset}`);
87
- await compile(directory, values);
88
- isCompiling = false;
89
+ await wsClient.sendProject();
89
90
  }
90
91
  }
91
92
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qtex",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "Ultra-fast cloud LaTeX compiler. Compile .tex documents in milliseconds without installing TeXLive or MikTeX. Zero config, sub-second builds, live watch mode.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -54,4 +54,4 @@
54
54
  "engines": {
55
55
  "node": ">=18"
56
56
  }
57
- }
57
+ }
@@ -0,0 +1,136 @@
1
+ import { resolve, extname, join, basename } from 'node:path';
2
+ import { colors, ui, Spinner } from './ui.js';
3
+ import { notifyClients } from './server.js';
4
+ import { readFile, writeFile } from 'node:fs/promises';
5
+
6
+ const WS_URL = 'wss://latex.taptapp.xyz/ws'; // Production URL
7
+
8
+ export class TachyonWS {
9
+ constructor(directory, options) {
10
+ this.directory = directory;
11
+ this.options = options;
12
+ this.socket = null;
13
+ this.isConnected = false;
14
+ this.spinner = new Spinner('');
15
+ this.isCompiling = false;
16
+ }
17
+
18
+ async connect() {
19
+ return new Promise((res, rej) => {
20
+ ui.info(`${colors.cyan}Connecting to Tachyon-Tex Live Engine...${colors.reset}`);
21
+ this.socket = new WebSocket(this.options.serverUrl || WS_URL);
22
+
23
+ this.socket.onopen = () => {
24
+ this.isConnected = true;
25
+ ui.info(`${colors.green}✅ Connected to Live Engine${colors.reset}`);
26
+ res();
27
+ };
28
+
29
+ this.socket.onmessage = async (event) => {
30
+ const data = JSON.parse(event.data);
31
+
32
+ if (data.type === 'compile_success') {
33
+ if (this.spinner.timer) {
34
+ this.spinner.succeed(`${colors.green}PDF updated in ${colors.bold}${data.compile_time_ms}ms${colors.reset}`);
35
+ }
36
+
37
+ const outputFileName = this.options.output || 'output.pdf';
38
+ const outputPath = resolve(process.cwd(), outputFileName);
39
+
40
+ // Decode base64 PDF
41
+ const buffer = Buffer.from(data.pdf, 'base64');
42
+ await writeFile(outputPath, buffer);
43
+
44
+ // Notify local preview clients
45
+ notifyClients(outputPath);
46
+ this.isCompiling = false;
47
+ } else if (data.type === 'compile_error') {
48
+ if (this.spinner.timer) {
49
+ this.spinner.fail('Compilation failed');
50
+ }
51
+ console.log(`\n${colors.yellow}--- LaTeX Error ---${colors.reset}`);
52
+ console.log(`${colors.red}${data.error}${colors.reset}`);
53
+ console.log(`${colors.yellow}-------------------\n${colors.reset}`);
54
+ this.isCompiling = false;
55
+ }
56
+ };
57
+
58
+ this.socket.onerror = (err) => {
59
+ ui.error(`WebSocket Error: ${err.message}`);
60
+ rej(err);
61
+ };
62
+
63
+ this.socket.onclose = () => {
64
+ this.isConnected = false;
65
+ ui.warn('WebSocket connection closed. Retrying in 2s...');
66
+ setTimeout(() => this.connect(), 2000);
67
+ };
68
+ });
69
+ }
70
+
71
+ async sendProject() {
72
+ if (!this.isConnected || this.isCompiling) return;
73
+
74
+ this.isCompiling = true;
75
+ this.spinner = new Spinner(`${colors.blue}Syncing project...`).start();
76
+
77
+ try {
78
+ const project = await this.buildProjectPayload();
79
+ this.socket.send(JSON.stringify(project));
80
+ } catch (e) {
81
+ this.spinner.fail(`Failed to sync: ${e.message}`);
82
+ this.isCompiling = false;
83
+ }
84
+ }
85
+
86
+ async buildProjectPayload() {
87
+ const absoluteDir = resolve(this.directory);
88
+ const files = await this.getProjectFiles(absoluteDir);
89
+ const payload = {
90
+ main: "main.tex", // Heuristic could be improved
91
+ files: {}
92
+ };
93
+
94
+ for (const fileObj of files) {
95
+ const ext = extname(fileObj.path).toLowerCase();
96
+ const textExts = ['.tex', '.sty', '.cls', '.bib', '.txt'];
97
+ const base = basename(fileObj.path);
98
+
99
+ if (textExts.includes(ext)) {
100
+ payload.files[fileObj.relative] = await readFile(fileObj.path, 'utf8');
101
+ if (base === 'main.tex') payload.main = fileObj.relative;
102
+ } else {
103
+ // Binary (images)
104
+ const buffer = await readFile(fileObj.path);
105
+ payload.files[fileObj.relative] = buffer.toString('base64');
106
+ }
107
+ }
108
+
109
+ return payload;
110
+ }
111
+
112
+ async getProjectFiles(dir, baseDir = dir) {
113
+ const { readdir } = await import('node:fs/promises');
114
+ const entries = await readdir(dir, { withFileTypes: true });
115
+ const files = [];
116
+
117
+ for (const entry of entries) {
118
+ const res = join(dir, entry.name);
119
+ if (entry.isDirectory()) {
120
+ if (entry.name !== 'node_modules' && !entry.name.startsWith('.')) {
121
+ files.push(...(await this.getProjectFiles(res, baseDir)));
122
+ }
123
+ } else {
124
+ const ext = extname(entry.name).toLowerCase();
125
+ const allowed = ['.tex', '.bib', '.sty', '.cls', '.pdf', '.png', '.jpg', '.jpeg'];
126
+ if (allowed.includes(ext)) {
127
+ files.push({
128
+ path: res,
129
+ relative: join(dir.replace(baseDir, ''), entry.name).replace(/^[\\\/]/, '').replace(/\\/g, '/')
130
+ });
131
+ }
132
+ }
133
+ }
134
+ return files;
135
+ }
136
+ }