@sprlab/wccompiler 0.12.1 → 0.14.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/lib/dev-server.js CHANGED
@@ -1,193 +1,193 @@
1
- /**
2
- * Dev Server — static HTTP server with SSE-based live-reload.
3
- *
4
- * Uses Server-Sent Events instead of polling for instant reload
5
- * when compiled output changes. No external dependencies.
6
- */
7
-
8
- import { createServer } from 'node:http';
9
- import { readFileSync, watch, existsSync } from 'node:fs';
10
- import { resolve, extname } from 'node:path';
11
-
12
- /**
13
- * @typedef {Object} DevServerOptions
14
- * @property {number} port
15
- * @property {string} root
16
- * @property {string} outputDir
17
- */
18
-
19
- /**
20
- * @typedef {Object} DevServerHandle
21
- * @property {import('node:http').Server} server
22
- * @property {() => void} close
23
- */
24
-
25
- const MIME_TYPES = {
26
- '.html': 'text/html; charset=utf-8',
27
- '.js': 'text/javascript; charset=utf-8',
28
- '.css': 'text/css; charset=utf-8',
29
- '.json': 'application/json; charset=utf-8',
30
- '.png': 'image/png',
31
- '.jpg': 'image/jpeg',
32
- '.svg': 'image/svg+xml',
33
- '.ico': 'image/x-icon',
34
- };
35
-
36
- const SSE_SNIPPET = `<script>
37
- (function() {
38
- var es = new EventSource('/__sse');
39
- var overlay = null;
40
- function showError(msg) {
41
- hideError();
42
- overlay = document.createElement('div');
43
- overlay.id = '__wcc_error_overlay';
44
- overlay.style.cssText = 'position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,0.85);color:#fff;font-family:monospace;font-size:14px;padding:32px;overflow:auto;display:flex;align-items:flex-start;justify-content:center;';
45
- var box = document.createElement('div');
46
- box.style.cssText = 'background:#1e1e1e;border:2px solid #f44;border-radius:8px;padding:24px;max-width:700px;width:100%;white-space:pre-wrap;word-break:break-word;';
47
- box.innerHTML = '<div style="color:#f44;font-size:16px;font-weight:bold;margin-bottom:12px;">\\u274C Compilation Error</div>' + msg.replace(/</g,'&lt;').replace(/>/g,'&gt;');
48
- overlay.appendChild(box);
49
- overlay.addEventListener('click', hideError);
50
- document.body.appendChild(overlay);
51
- }
52
- function hideError() {
53
- if (overlay) { overlay.remove(); overlay = null; }
54
- }
55
- es.onmessage = function(e) {
56
- if (e.data === 'reload') { hideError(); location.reload(); }
57
- else if (e.data.startsWith('error:')) { showError(e.data.slice(6).replace(/\\\\n/g,'\\n')); }
58
- };
59
- es.onerror = function() {
60
- es.close();
61
- setTimeout(function() { location.reload(); }, 1000);
62
- };
63
- })();
64
- </script>`;
65
-
66
- // Keep the poll snippet for backward compatibility (tests check for it)
67
- const POLL_SNIPPET = SSE_SNIPPET;
68
-
69
- /**
70
- * Start a development server with live-reload support.
71
- *
72
- * @param {DevServerOptions} options
73
- * @returns {DevServerHandle}
74
- */
75
- export function startDevServer({ port, root, outputDir }) {
76
- /** @type {Set<import('node:http').ServerResponse>} */
77
- const sseClients = new Set();
78
-
79
- /** Send a reload event to all connected SSE clients */
80
- function notifyReload() {
81
- for (const res of sseClients) {
82
- try {
83
- res.write('data: reload\n\n');
84
- } catch {
85
- sseClients.delete(res);
86
- }
87
- }
88
- }
89
-
90
- /** Send an error event to all connected SSE clients */
91
- function notifyError(message) {
92
- for (const res of sseClients) {
93
- try {
94
- res.write(`data: error:${message.replace(/\n/g, '\\n')}\n\n`);
95
- } catch {
96
- sseClients.delete(res);
97
- }
98
- }
99
- }
100
-
101
- const server = createServer((req, res) => {
102
- const url = req.url.split('?')[0];
103
-
104
- // SSE endpoint — keeps connection open, sends reload events
105
- if (url === '/__sse') {
106
- res.writeHead(200, {
107
- 'Content-Type': 'text/event-stream',
108
- 'Cache-Control': 'no-cache',
109
- 'Connection': 'keep-alive',
110
- 'Access-Control-Allow-Origin': '*',
111
- });
112
- res.write('data: connected\n\n');
113
- sseClients.add(res);
114
- req.on('close', () => sseClients.delete(res));
115
- return;
116
- }
117
-
118
- // Legacy poll endpoint (backward compat for tests)
119
- if (url === '/__poll') {
120
- const body = JSON.stringify({ t: Date.now() });
121
- const buf = Buffer.from(body);
122
- res.writeHead(200, {
123
- 'Content-Type': 'application/json',
124
- 'Content-Length': buf.byteLength,
125
- 'Cache-Control': 'no-cache',
126
- });
127
- res.end(buf);
128
- return;
129
- }
130
-
131
- // Static files
132
- const filePath = url === '/' ? '/index.html' : url;
133
- const fullPath = resolve(root, '.' + filePath);
134
-
135
- try {
136
- let buf = readFileSync(fullPath);
137
- const ext = extname(fullPath);
138
- const mime = MIME_TYPES[ext] || 'application/octet-stream';
139
-
140
- // Inject SSE snippet into HTML
141
- if (ext === '.html') {
142
- let html = buf.toString('utf-8');
143
- if (html.includes('</body>')) {
144
- html = html.replace('</body>', SSE_SNIPPET + '\n</body>');
145
- } else {
146
- html += '\n' + SSE_SNIPPET;
147
- }
148
- buf = Buffer.from(html, 'utf-8');
149
- }
150
-
151
- res.writeHead(200, {
152
- 'Content-Type': mime,
153
- 'Content-Length': buf.byteLength,
154
- });
155
- res.end(buf);
156
- } catch {
157
- const msg = 'Not Found';
158
- res.writeHead(404, {
159
- 'Content-Type': 'text/plain',
160
- 'Content-Length': Buffer.byteLength(msg),
161
- });
162
- res.end(msg);
163
- }
164
- });
165
-
166
- // Watch output dir — notify SSE clients on changes (debounced)
167
- let watcher = null;
168
- if (outputDir && existsSync(outputDir)) {
169
- let timer = null;
170
- watcher = watch(outputDir, { recursive: true }, () => {
171
- if (timer) clearTimeout(timer);
172
- timer = setTimeout(() => notifyReload(), 200);
173
- });
174
- }
175
-
176
- server.listen(port, () => {
177
- console.log(`Dev server running at http://localhost:${port}`);
178
- });
179
-
180
- return {
181
- server,
182
- notifyError,
183
- close() {
184
- // Close all SSE connections
185
- for (const res of sseClients) {
186
- try { res.end(); } catch {}
187
- }
188
- sseClients.clear();
189
- if (watcher) watcher.close();
190
- server.close();
191
- },
192
- };
193
- }
1
+ /**
2
+ * Dev Server — static HTTP server with SSE-based live-reload.
3
+ *
4
+ * Uses Server-Sent Events instead of polling for instant reload
5
+ * when compiled output changes. No external dependencies.
6
+ */
7
+
8
+ import { createServer } from 'node:http';
9
+ import { readFileSync, watch, existsSync } from 'node:fs';
10
+ import { resolve, extname } from 'node:path';
11
+
12
+ /**
13
+ * @typedef {Object} DevServerOptions
14
+ * @property {number} port
15
+ * @property {string} root
16
+ * @property {string} outputDir
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} DevServerHandle
21
+ * @property {import('node:http').Server} server
22
+ * @property {() => void} close
23
+ */
24
+
25
+ const MIME_TYPES = {
26
+ '.html': 'text/html; charset=utf-8',
27
+ '.js': 'text/javascript; charset=utf-8',
28
+ '.css': 'text/css; charset=utf-8',
29
+ '.json': 'application/json; charset=utf-8',
30
+ '.png': 'image/png',
31
+ '.jpg': 'image/jpeg',
32
+ '.svg': 'image/svg+xml',
33
+ '.ico': 'image/x-icon',
34
+ };
35
+
36
+ const SSE_SNIPPET = `<script>
37
+ (function() {
38
+ var es = new EventSource('/__sse');
39
+ var overlay = null;
40
+ function showError(msg) {
41
+ hideError();
42
+ overlay = document.createElement('div');
43
+ overlay.id = '__wcc_error_overlay';
44
+ overlay.style.cssText = 'position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,0.85);color:#fff;font-family:monospace;font-size:14px;padding:32px;overflow:auto;display:flex;align-items:flex-start;justify-content:center;';
45
+ var box = document.createElement('div');
46
+ box.style.cssText = 'background:#1e1e1e;border:2px solid #f44;border-radius:8px;padding:24px;max-width:700px;width:100%;white-space:pre-wrap;word-break:break-word;';
47
+ box.innerHTML = '<div style="color:#f44;font-size:16px;font-weight:bold;margin-bottom:12px;">\\u274C Compilation Error</div>' + msg.replace(/</g,'&lt;').replace(/>/g,'&gt;');
48
+ overlay.appendChild(box);
49
+ overlay.addEventListener('click', hideError);
50
+ document.body.appendChild(overlay);
51
+ }
52
+ function hideError() {
53
+ if (overlay) { overlay.remove(); overlay = null; }
54
+ }
55
+ es.onmessage = function(e) {
56
+ if (e.data === 'reload') { hideError(); location.reload(); }
57
+ else if (e.data.startsWith('error:')) { showError(e.data.slice(6).replace(/\\\\n/g,'\\n')); }
58
+ };
59
+ es.onerror = function() {
60
+ es.close();
61
+ setTimeout(function() { location.reload(); }, 1000);
62
+ };
63
+ })();
64
+ </script>`;
65
+
66
+ // Keep the poll snippet for backward compatibility (tests check for it)
67
+ const POLL_SNIPPET = SSE_SNIPPET;
68
+
69
+ /**
70
+ * Start a development server with live-reload support.
71
+ *
72
+ * @param {DevServerOptions} options
73
+ * @returns {DevServerHandle}
74
+ */
75
+ export function startDevServer({ port, root, outputDir }) {
76
+ /** @type {Set<import('node:http').ServerResponse>} */
77
+ const sseClients = new Set();
78
+
79
+ /** Send a reload event to all connected SSE clients */
80
+ function notifyReload() {
81
+ for (const res of sseClients) {
82
+ try {
83
+ res.write('data: reload\n\n');
84
+ } catch {
85
+ sseClients.delete(res);
86
+ }
87
+ }
88
+ }
89
+
90
+ /** Send an error event to all connected SSE clients */
91
+ function notifyError(message) {
92
+ for (const res of sseClients) {
93
+ try {
94
+ res.write(`data: error:${message.replace(/\n/g, '\\n')}\n\n`);
95
+ } catch {
96
+ sseClients.delete(res);
97
+ }
98
+ }
99
+ }
100
+
101
+ const server = createServer((req, res) => {
102
+ const url = req.url.split('?')[0];
103
+
104
+ // SSE endpoint — keeps connection open, sends reload events
105
+ if (url === '/__sse') {
106
+ res.writeHead(200, {
107
+ 'Content-Type': 'text/event-stream',
108
+ 'Cache-Control': 'no-cache',
109
+ 'Connection': 'keep-alive',
110
+ 'Access-Control-Allow-Origin': '*',
111
+ });
112
+ res.write('data: connected\n\n');
113
+ sseClients.add(res);
114
+ req.on('close', () => sseClients.delete(res));
115
+ return;
116
+ }
117
+
118
+ // Legacy poll endpoint (backward compat for tests)
119
+ if (url === '/__poll') {
120
+ const body = JSON.stringify({ t: Date.now() });
121
+ const buf = Buffer.from(body);
122
+ res.writeHead(200, {
123
+ 'Content-Type': 'application/json',
124
+ 'Content-Length': buf.byteLength,
125
+ 'Cache-Control': 'no-cache',
126
+ });
127
+ res.end(buf);
128
+ return;
129
+ }
130
+
131
+ // Static files
132
+ const filePath = url === '/' ? '/index.html' : url;
133
+ const fullPath = resolve(root, '.' + filePath);
134
+
135
+ try {
136
+ let buf = readFileSync(fullPath);
137
+ const ext = extname(fullPath);
138
+ const mime = MIME_TYPES[ext] || 'application/octet-stream';
139
+
140
+ // Inject SSE snippet into HTML
141
+ if (ext === '.html') {
142
+ let html = buf.toString('utf-8');
143
+ if (html.includes('</body>')) {
144
+ html = html.replace('</body>', SSE_SNIPPET + '\n</body>');
145
+ } else {
146
+ html += '\n' + SSE_SNIPPET;
147
+ }
148
+ buf = Buffer.from(html, 'utf-8');
149
+ }
150
+
151
+ res.writeHead(200, {
152
+ 'Content-Type': mime,
153
+ 'Content-Length': buf.byteLength,
154
+ });
155
+ res.end(buf);
156
+ } catch {
157
+ const msg = 'Not Found';
158
+ res.writeHead(404, {
159
+ 'Content-Type': 'text/plain',
160
+ 'Content-Length': Buffer.byteLength(msg),
161
+ });
162
+ res.end(msg);
163
+ }
164
+ });
165
+
166
+ // Watch output dir — notify SSE clients on changes (debounced)
167
+ let watcher = null;
168
+ if (outputDir && existsSync(outputDir)) {
169
+ let timer = null;
170
+ watcher = watch(outputDir, { recursive: true }, () => {
171
+ if (timer) clearTimeout(timer);
172
+ timer = setTimeout(() => notifyReload(), 200);
173
+ });
174
+ }
175
+
176
+ server.listen(port, () => {
177
+ console.log(`Dev server running at http://localhost:${port}`);
178
+ });
179
+
180
+ return {
181
+ server,
182
+ notifyError,
183
+ close() {
184
+ // Close all SSE connections
185
+ for (const res of sseClients) {
186
+ try { res.end(); } catch {}
187
+ }
188
+ sseClients.clear();
189
+ if (watcher) watcher.close();
190
+ server.close();
191
+ },
192
+ };
193
+ }