clewy-lang 1.0.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 +245 -0
- package/package.json +17 -0
- package/src/cli.js +411 -0
- package/src/compiler.js +536 -0
- package/src/parser.js +359 -0
- package/src/runtime.js +180 -0
package/src/parser.js
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clewy Parser
|
|
3
|
+
* Converts .cly source text into an AST (Abstract Syntax Tree)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const TOKEN_TYPES = {
|
|
7
|
+
KEYWORD: 'KEYWORD',
|
|
8
|
+
STRING: 'STRING',
|
|
9
|
+
NUMBER: 'NUMBER',
|
|
10
|
+
ARROW: 'ARROW',
|
|
11
|
+
IDENTIFIER: 'IDENTIFIER',
|
|
12
|
+
END: 'END',
|
|
13
|
+
NEWLINE: 'NEWLINE',
|
|
14
|
+
EOF: 'EOF',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const KEYWORDS = new Set([
|
|
18
|
+
'page', 'text', 'button', 'input', 'show', 'loop',
|
|
19
|
+
'component', 'use', 'end', 'style', 'link', 'image',
|
|
20
|
+
'if', 'var', 'set', 'nav', 'footer', 'section',
|
|
21
|
+
'title', 'subtitle', 'alert',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
// ─── Lexer ────────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
function tokenize(source) {
|
|
27
|
+
const tokens = [];
|
|
28
|
+
let i = 0;
|
|
29
|
+
let line = 1;
|
|
30
|
+
|
|
31
|
+
while (i < source.length) {
|
|
32
|
+
// Skip whitespace (but not newlines)
|
|
33
|
+
if (source[i] === ' ' || source[i] === '\t' || source[i] === '\r') {
|
|
34
|
+
i++;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Comments
|
|
39
|
+
if (source[i] === '#') {
|
|
40
|
+
while (i < source.length && source[i] !== '\n') i++;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Newlines
|
|
45
|
+
if (source[i] === '\n') {
|
|
46
|
+
tokens.push({ type: TOKEN_TYPES.NEWLINE, value: '\n', line });
|
|
47
|
+
line++;
|
|
48
|
+
i++;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Arrow =>
|
|
53
|
+
if (source[i] === '=' && source[i + 1] === '>') {
|
|
54
|
+
tokens.push({ type: TOKEN_TYPES.ARROW, value: '=>', line });
|
|
55
|
+
i += 2;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// String literals
|
|
60
|
+
if (source[i] === '"' || source[i] === "'") {
|
|
61
|
+
const quote = source[i];
|
|
62
|
+
i++;
|
|
63
|
+
let str = '';
|
|
64
|
+
while (i < source.length && source[i] !== quote) {
|
|
65
|
+
if (source[i] === '\\' && i + 1 < source.length) {
|
|
66
|
+
i++;
|
|
67
|
+
const escapes = { n: '\n', t: '\t', '"': '"', "'": "'", '\\': '\\' };
|
|
68
|
+
str += escapes[source[i]] || source[i];
|
|
69
|
+
} else {
|
|
70
|
+
str += source[i];
|
|
71
|
+
}
|
|
72
|
+
i++;
|
|
73
|
+
}
|
|
74
|
+
i++; // closing quote
|
|
75
|
+
tokens.push({ type: TOKEN_TYPES.STRING, value: str, line });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Numbers
|
|
80
|
+
if (/[0-9]/.test(source[i])) {
|
|
81
|
+
let num = '';
|
|
82
|
+
while (i < source.length && /[0-9.]/.test(source[i])) {
|
|
83
|
+
num += source[i++];
|
|
84
|
+
}
|
|
85
|
+
tokens.push({ type: TOKEN_TYPES.NUMBER, value: parseFloat(num), line });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Keywords and identifiers
|
|
90
|
+
if (/[a-zA-Z_]/.test(source[i])) {
|
|
91
|
+
let word = '';
|
|
92
|
+
while (i < source.length && /[a-zA-Z0-9_-]/.test(source[i])) {
|
|
93
|
+
word += source[i++];
|
|
94
|
+
}
|
|
95
|
+
const type = KEYWORDS.has(word) ? TOKEN_TYPES.KEYWORD : TOKEN_TYPES.IDENTIFIER;
|
|
96
|
+
tokens.push({ type, value: word, line });
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Unknown character — skip
|
|
101
|
+
i++;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
tokens.push({ type: TOKEN_TYPES.EOF, value: null, line });
|
|
105
|
+
return tokens;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── Parser ───────────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
function parse(source) {
|
|
111
|
+
const tokens = tokenize(source);
|
|
112
|
+
let pos = 0;
|
|
113
|
+
|
|
114
|
+
function peek() { return tokens[pos]; }
|
|
115
|
+
function consume() { return tokens[pos++]; }
|
|
116
|
+
function expect(type, value) {
|
|
117
|
+
const t = peek();
|
|
118
|
+
if (t.type !== type || (value !== undefined && t.value !== value)) {
|
|
119
|
+
throw new Error(`Line ${t.line}: Expected ${value || type}, got "${t.value}"`);
|
|
120
|
+
}
|
|
121
|
+
return consume();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function skipNewlines() {
|
|
125
|
+
while (peek().type === TOKEN_TYPES.NEWLINE) consume();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parseStatement() {
|
|
129
|
+
skipNewlines();
|
|
130
|
+
const t = peek();
|
|
131
|
+
|
|
132
|
+
if (t.type === TOKEN_TYPES.EOF) return null;
|
|
133
|
+
if (t.type === TOKEN_TYPES.KEYWORD && t.value === 'end') return null;
|
|
134
|
+
|
|
135
|
+
if (t.type !== TOKEN_TYPES.KEYWORD) {
|
|
136
|
+
// skip unknown tokens gracefully
|
|
137
|
+
consume();
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
consume(); // eat keyword
|
|
142
|
+
const keyword = t.value;
|
|
143
|
+
|
|
144
|
+
switch (keyword) {
|
|
145
|
+
case 'page': return parsePage();
|
|
146
|
+
case 'text': return parseText();
|
|
147
|
+
case 'title': return parseTitle();
|
|
148
|
+
case 'subtitle':return parseSubtitle();
|
|
149
|
+
case 'button': return parseButton();
|
|
150
|
+
case 'input': return parseInput();
|
|
151
|
+
case 'show': return parseShow();
|
|
152
|
+
case 'alert': return parseAlert();
|
|
153
|
+
case 'loop': return parseLoop();
|
|
154
|
+
case 'component': return parseComponent();
|
|
155
|
+
case 'use': return parseUse();
|
|
156
|
+
case 'style': return parseStyle();
|
|
157
|
+
case 'link': return parseLink();
|
|
158
|
+
case 'image': return parseImage();
|
|
159
|
+
case 'if': return parseIf();
|
|
160
|
+
case 'var': return parseVar();
|
|
161
|
+
case 'set': return parseSet();
|
|
162
|
+
case 'nav': return parseNav();
|
|
163
|
+
case 'footer': return parseFooter();
|
|
164
|
+
case 'section': return parseSection();
|
|
165
|
+
default: return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function parseStringArg() {
|
|
170
|
+
if (peek().type === TOKEN_TYPES.STRING) return consume().value;
|
|
171
|
+
if (peek().type === TOKEN_TYPES.IDENTIFIER) return consume().value;
|
|
172
|
+
return '';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function parseAction() {
|
|
176
|
+
// Parses what comes after =>
|
|
177
|
+
const t = peek();
|
|
178
|
+
if (t.type !== TOKEN_TYPES.KEYWORD) return null;
|
|
179
|
+
consume();
|
|
180
|
+
switch (t.value) {
|
|
181
|
+
case 'show': return { type: 'show', text: parseStringArg() };
|
|
182
|
+
case 'alert': return { type: 'alert', text: parseStringArg() };
|
|
183
|
+
case 'set': return { type: 'set', name: parseStringArg(), value: parseStringArg() };
|
|
184
|
+
default: return { type: t.value };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function parsePage() {
|
|
189
|
+
const label = parseStringArg();
|
|
190
|
+
return { type: 'page', label };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function parseText() {
|
|
194
|
+
const content = parseStringArg();
|
|
195
|
+
return { type: 'text', content };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function parseTitle() {
|
|
199
|
+
const content = parseStringArg();
|
|
200
|
+
return { type: 'title', content };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function parseSubtitle() {
|
|
204
|
+
const content = parseStringArg();
|
|
205
|
+
return { type: 'subtitle', content };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function parseButton() {
|
|
209
|
+
const label = parseStringArg();
|
|
210
|
+
let action = null;
|
|
211
|
+
if (peek().type === TOKEN_TYPES.ARROW) {
|
|
212
|
+
consume();
|
|
213
|
+
action = parseAction();
|
|
214
|
+
}
|
|
215
|
+
return { type: 'button', label, action };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function parseInput() {
|
|
219
|
+
const placeholder = parseStringArg();
|
|
220
|
+
let name = null;
|
|
221
|
+
if (peek().type === TOKEN_TYPES.KEYWORD && peek().value === 'as') {
|
|
222
|
+
consume();
|
|
223
|
+
name = parseStringArg();
|
|
224
|
+
} else if (peek().type === TOKEN_TYPES.IDENTIFIER) {
|
|
225
|
+
name = consume().value;
|
|
226
|
+
}
|
|
227
|
+
return { type: 'input', placeholder, name };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function parseShow() {
|
|
231
|
+
const text = parseStringArg();
|
|
232
|
+
return { type: 'show', text };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function parseAlert() {
|
|
236
|
+
const text = parseStringArg();
|
|
237
|
+
return { type: 'alert', text };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function parseLoop() {
|
|
241
|
+
let count = 3;
|
|
242
|
+
if (peek().type === TOKEN_TYPES.NUMBER) count = consume().value;
|
|
243
|
+
let body = [];
|
|
244
|
+
if (peek().type === TOKEN_TYPES.ARROW) {
|
|
245
|
+
consume();
|
|
246
|
+
const stmt = parseStatement();
|
|
247
|
+
if (stmt) body = [stmt];
|
|
248
|
+
} else {
|
|
249
|
+
// multiline loop body
|
|
250
|
+
skipNewlines();
|
|
251
|
+
while (peek().type !== TOKEN_TYPES.EOF) {
|
|
252
|
+
if (peek().type === TOKEN_TYPES.KEYWORD && peek().value === 'end') { consume(); break; }
|
|
253
|
+
const stmt = parseStatement();
|
|
254
|
+
if (stmt) body.push(stmt);
|
|
255
|
+
skipNewlines();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return { type: 'loop', count, body };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function parseComponent() {
|
|
262
|
+
const name = parseStringArg() || (peek().type === TOKEN_TYPES.IDENTIFIER ? consume().value : 'Component');
|
|
263
|
+
const body = [];
|
|
264
|
+
skipNewlines();
|
|
265
|
+
while (peek().type !== TOKEN_TYPES.EOF) {
|
|
266
|
+
if (peek().type === TOKEN_TYPES.KEYWORD && peek().value === 'end') { consume(); break; }
|
|
267
|
+
const stmt = parseStatement();
|
|
268
|
+
if (stmt) body.push(stmt);
|
|
269
|
+
skipNewlines();
|
|
270
|
+
}
|
|
271
|
+
return { type: 'component', name, body };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function parseUse() {
|
|
275
|
+
const name = parseStringArg() || (peek().type === TOKEN_TYPES.IDENTIFIER ? consume().value : '');
|
|
276
|
+
return { type: 'use', name };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function parseStyle() {
|
|
280
|
+
const prop = parseStringArg();
|
|
281
|
+
const value = parseStringArg();
|
|
282
|
+
return { type: 'style', prop, value };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function parseLink() {
|
|
286
|
+
const label = parseStringArg();
|
|
287
|
+
const href = parseStringArg() || '#';
|
|
288
|
+
return { type: 'link', label, href };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function parseImage() {
|
|
292
|
+
const src = parseStringArg();
|
|
293
|
+
const alt = parseStringArg() || '';
|
|
294
|
+
return { type: 'image', src, alt };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function parseIf() {
|
|
298
|
+
const condition = parseStringArg();
|
|
299
|
+
const body = [];
|
|
300
|
+
skipNewlines();
|
|
301
|
+
while (peek().type !== TOKEN_TYPES.EOF) {
|
|
302
|
+
if (peek().type === TOKEN_TYPES.KEYWORD && peek().value === 'end') { consume(); break; }
|
|
303
|
+
const stmt = parseStatement();
|
|
304
|
+
if (stmt) body.push(stmt);
|
|
305
|
+
skipNewlines();
|
|
306
|
+
}
|
|
307
|
+
return { type: 'if', condition, body };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function parseVar() {
|
|
311
|
+
const name = parseStringArg() || (peek().type === TOKEN_TYPES.IDENTIFIER ? consume().value : 'x');
|
|
312
|
+
const value = parseStringArg();
|
|
313
|
+
return { type: 'var', name, value };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function parseSet() {
|
|
317
|
+
const name = parseStringArg() || (peek().type === TOKEN_TYPES.IDENTIFIER ? consume().value : 'x');
|
|
318
|
+
const value = parseStringArg();
|
|
319
|
+
return { type: 'set', name, value };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function parseNav() {
|
|
323
|
+
const items = [];
|
|
324
|
+
// Collect link items until newline or end
|
|
325
|
+
while (peek().type === TOKEN_TYPES.STRING || peek().type === TOKEN_TYPES.IDENTIFIER) {
|
|
326
|
+
items.push(consume().value);
|
|
327
|
+
}
|
|
328
|
+
return { type: 'nav', items };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function parseFooter() {
|
|
332
|
+
const content = parseStringArg();
|
|
333
|
+
return { type: 'footer', content };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function parseSection() {
|
|
337
|
+
const label = parseStringArg();
|
|
338
|
+
const body = [];
|
|
339
|
+
skipNewlines();
|
|
340
|
+
while (peek().type !== TOKEN_TYPES.EOF) {
|
|
341
|
+
if (peek().type === TOKEN_TYPES.KEYWORD && peek().value === 'end') { consume(); break; }
|
|
342
|
+
const stmt = parseStatement();
|
|
343
|
+
if (stmt) body.push(stmt);
|
|
344
|
+
skipNewlines();
|
|
345
|
+
}
|
|
346
|
+
return { type: 'section', label, body };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ── Top-level parse loop
|
|
350
|
+
const ast = { type: 'program', body: [] };
|
|
351
|
+
while (peek().type !== TOKEN_TYPES.EOF) {
|
|
352
|
+
const stmt = parseStatement();
|
|
353
|
+
if (stmt) ast.body.push(stmt);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return ast;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
module.exports = { parse, tokenize };
|
package/src/runtime.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clewy Runtime
|
|
3
|
+
* Dev server with live-reload for .cly projects
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { compile } = require('./compiler');
|
|
10
|
+
|
|
11
|
+
// ─── MIME types ───────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const MIME = {
|
|
14
|
+
'.html': 'text/html',
|
|
15
|
+
'.css': 'text/css',
|
|
16
|
+
'.js': 'application/javascript',
|
|
17
|
+
'.png': 'image/png',
|
|
18
|
+
'.jpg': 'image/jpeg',
|
|
19
|
+
'.jpeg': 'image/jpeg',
|
|
20
|
+
'.gif': 'image/gif',
|
|
21
|
+
'.svg': 'image/svg+xml',
|
|
22
|
+
'.ico': 'image/x-icon',
|
|
23
|
+
'.json': 'application/json',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// ─── Build pipeline ──────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
function buildProject(projectDir) {
|
|
29
|
+
const clyPath = path.join(projectDir, 'main.cly');
|
|
30
|
+
|
|
31
|
+
if (!fs.existsSync(clyPath)) {
|
|
32
|
+
console.error(` ✗ No main.cly found in ${projectDir}`);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const source = fs.readFileSync(clyPath, 'utf8');
|
|
38
|
+
const { html, css, js } = compile(source);
|
|
39
|
+
|
|
40
|
+
fs.writeFileSync(path.join(projectDir, 'index.html'), html);
|
|
41
|
+
fs.writeFileSync(path.join(projectDir, 'style.css'), css);
|
|
42
|
+
fs.writeFileSync(path.join(projectDir, 'script.js'), js);
|
|
43
|
+
|
|
44
|
+
return true;
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error(` ✗ Compile error: ${err.message}`);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Live-reload injection ────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
const LIVERELOAD_SCRIPT = `
|
|
54
|
+
<!-- Clewy live reload -->
|
|
55
|
+
<script>
|
|
56
|
+
(function() {
|
|
57
|
+
var src = new EventSource('/__clewy_sse');
|
|
58
|
+
src.onmessage = function(e) {
|
|
59
|
+
if (e.data === 'reload') window.location.reload();
|
|
60
|
+
};
|
|
61
|
+
})();
|
|
62
|
+
</script>
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
function injectLiveReload(html) {
|
|
66
|
+
return html.replace('</body>', LIVERELOAD_SCRIPT + '</body>');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── Dev server ───────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
function startServer(projectDir, port = 3000) {
|
|
72
|
+
let sseClients = [];
|
|
73
|
+
|
|
74
|
+
// Initial build
|
|
75
|
+
console.log('\n 🔨 Building...');
|
|
76
|
+
const ok = buildProject(projectDir);
|
|
77
|
+
if (ok) console.log(' ✓ Build complete\n');
|
|
78
|
+
|
|
79
|
+
// File watcher
|
|
80
|
+
const clyPath = path.join(projectDir, 'main.cly');
|
|
81
|
+
let debounceTimer = null;
|
|
82
|
+
|
|
83
|
+
fs.watch(clyPath, () => {
|
|
84
|
+
clearTimeout(debounceTimer);
|
|
85
|
+
debounceTimer = setTimeout(() => {
|
|
86
|
+
console.log(' ↻ Detected change, rebuilding...');
|
|
87
|
+
const rebuilt = buildProject(projectDir);
|
|
88
|
+
if (rebuilt) {
|
|
89
|
+
console.log(' ✓ Rebuilt — refreshing browser\n');
|
|
90
|
+
sseClients.forEach(res => res.write('data: reload\n\n'));
|
|
91
|
+
}
|
|
92
|
+
}, 80);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// HTTP server
|
|
96
|
+
const server = http.createServer((req, res) => {
|
|
97
|
+
const url = req.url === '/' ? '/index.html' : req.url;
|
|
98
|
+
|
|
99
|
+
// SSE endpoint for live reload
|
|
100
|
+
if (url === '/__clewy_sse') {
|
|
101
|
+
res.writeHead(200, {
|
|
102
|
+
'Content-Type': 'text/event-stream',
|
|
103
|
+
'Cache-Control': 'no-cache',
|
|
104
|
+
'Connection': 'keep-alive',
|
|
105
|
+
'Access-Control-Allow-Origin': '*',
|
|
106
|
+
});
|
|
107
|
+
res.write('data: connected\n\n');
|
|
108
|
+
sseClients.push(res);
|
|
109
|
+
req.on('close', () => {
|
|
110
|
+
sseClients = sseClients.filter(c => c !== res);
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const filePath = path.join(projectDir, url.split('?')[0]);
|
|
116
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
117
|
+
|
|
118
|
+
if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) {
|
|
119
|
+
const fallback = path.join(projectDir, 'index.html');
|
|
120
|
+
if (fs.existsSync(fallback)) {
|
|
121
|
+
let html = fs.readFileSync(fallback, 'utf8');
|
|
122
|
+
html = injectLiveReload(html);
|
|
123
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
124
|
+
res.end(html);
|
|
125
|
+
} else {
|
|
126
|
+
res.writeHead(404);
|
|
127
|
+
res.end('Not found');
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
let content = fs.readFileSync(filePath);
|
|
134
|
+
const mime = MIME[ext] || 'application/octet-stream';
|
|
135
|
+
|
|
136
|
+
if (ext === '.html') {
|
|
137
|
+
content = injectLiveReload(content.toString());
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
res.writeHead(200, { 'Content-Type': mime });
|
|
141
|
+
res.end(content);
|
|
142
|
+
} catch {
|
|
143
|
+
res.writeHead(500);
|
|
144
|
+
res.end('Server error');
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
server.listen(port, () => {
|
|
149
|
+
console.log(` ┌──────────────────────────────────────┐`);
|
|
150
|
+
console.log(` │ 🚀 Clewy dev server running │`);
|
|
151
|
+
console.log(` │ │`);
|
|
152
|
+
console.log(` │ Local: http://localhost:${port} │`);
|
|
153
|
+
console.log(` │ │`);
|
|
154
|
+
console.log(` │ Watching main.cly for changes... │`);
|
|
155
|
+
console.log(` │ Press Ctrl+C to stop │`);
|
|
156
|
+
console.log(` └──────────────────────────────────────┘\n`);
|
|
157
|
+
|
|
158
|
+
// Try to open browser
|
|
159
|
+
const open = process.platform === 'win32'
|
|
160
|
+
? `start http://localhost:${port}`
|
|
161
|
+
: process.platform === 'darwin'
|
|
162
|
+
? `open http://localhost:${port}`
|
|
163
|
+
: `xdg-open http://localhost:${port}`;
|
|
164
|
+
|
|
165
|
+
require('child_process').exec(open, () => {});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
server.on('error', (err) => {
|
|
169
|
+
if (err.code === 'EADDRINUSE') {
|
|
170
|
+
console.error(` ✗ Port ${port} is already in use. Try: clewy run --port ${port + 1}`);
|
|
171
|
+
} else {
|
|
172
|
+
console.error(` ✗ Server error: ${err.message}`);
|
|
173
|
+
}
|
|
174
|
+
process.exit(1);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return server;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = { buildProject, startServer };
|