qtex 1.1.3 → 1.1.5
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 +2 -2
- package/index.js +11 -10
- package/package.json +2 -2
- package/src/ws-client.js +136 -0
package/README.md
CHANGED
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
|
|
30
30
|
**macOS / Linux**
|
|
31
31
|
```bash
|
|
32
|
-
curl -fsSL https://
|
|
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://
|
|
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
|
@@ -8,7 +8,7 @@ import { compile } from './src/compiler.js';
|
|
|
8
8
|
import { startServer } from './src/server.js';
|
|
9
9
|
import { autoUpdate, selfUpdate } from './src/updater.js';
|
|
10
10
|
import { exec } from 'node:child_process';
|
|
11
|
-
import packageJson from './package.json'
|
|
11
|
+
import packageJson from './package.json' with { type: 'json' };
|
|
12
12
|
|
|
13
13
|
// --- Entry Point ---
|
|
14
14
|
const args = process.argv.slice(2);
|
|
@@ -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
|
-
|
|
72
|
+
// Initial compile over WS
|
|
73
|
+
await wsClient.sendProject();
|
|
69
74
|
|
|
70
75
|
// Auto-open browser
|
|
71
|
-
const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
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
|
-
|
|
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
|
});
|
|
@@ -95,7 +96,7 @@ ${colors.bold}OPTIONS:${colors.reset}
|
|
|
95
96
|
// Auto-open generated PDF in the system browser
|
|
96
97
|
const outputFileName = values.output || 'output.pdf';
|
|
97
98
|
const outputPath = resolve(process.cwd(), directory, outputFileName);
|
|
98
|
-
const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
99
|
+
const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start ""' : 'xdg-open';
|
|
99
100
|
exec(`${openCmd} "${outputPath}"`);
|
|
100
101
|
}
|
|
101
102
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qtex",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
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
|
+
}
|
package/src/ws-client.js
ADDED
|
@@ -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
|
+
}
|