@ozsarman/clarityjs 0.6.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 +178 -0
- package/package.json +168 -0
- package/src/analyze.js +534 -0
- package/src/async-state.js +555 -0
- package/src/bundle-runtime.js +35 -0
- package/src/clarity-bundle.js +332 -0
- package/src/clarity-test.js +622 -0
- package/src/cli.js +453 -0
- package/src/codegen.js +1934 -0
- package/src/dev-server.js +362 -0
- package/src/devtools.js +765 -0
- package/src/edge.js +606 -0
- package/src/error-overlay.js +535 -0
- package/src/file-conventions.js +472 -0
- package/src/font.js +513 -0
- package/src/game-loop.js +106 -0
- package/src/head.js +393 -0
- package/src/hydrate.js +292 -0
- package/src/i18n.js +403 -0
- package/src/image.js +352 -0
- package/src/index.js +193 -0
- package/src/islands.js +284 -0
- package/src/isr.js +306 -0
- package/src/layout.js +342 -0
- package/src/lexer.js +572 -0
- package/src/linter.js +547 -0
- package/src/pages-router.js +229 -0
- package/src/parser.js +1108 -0
- package/src/router.js +732 -0
- package/src/runtime.js +1465 -0
- package/src/scoped-css.js +641 -0
- package/src/server-actions.js +439 -0
- package/src/server-data.js +225 -0
- package/src/sourcemap.js +130 -0
- package/src/ssg.js +310 -0
- package/src/ssr.js +621 -0
- package/src/store.js +276 -0
- package/src/transitions.js +438 -0
- package/src/ts-plugin.js +613 -0
- package/src/typegen.js +240 -0
- package/src/vite-plugin.js +447 -0
- package/types/index.d.ts +366 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clarity.js Development Server
|
|
3
|
+
*
|
|
4
|
+
* Zero-config dev server for .clarity projects. Features:
|
|
5
|
+
* - Serves static files from the project directory
|
|
6
|
+
* - Watches .clarity files for changes and auto-recompiles
|
|
7
|
+
* - Live reload via Server-Sent Events (SSE) — no WebSocket needed
|
|
8
|
+
* - Injects a tiny client script that reconnects and reloads on change
|
|
9
|
+
* - Serves compiled .clarity files as on-the-fly .js responses
|
|
10
|
+
* - Color-coded console output with timestamps
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* clarity dev [dir] [--port 3000]
|
|
14
|
+
*
|
|
15
|
+
* Author: Claude (Anthropic)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import http from 'http';
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import { compile } from './index.js';
|
|
22
|
+
|
|
23
|
+
// ─── MIME types ───────────────────────────────────────────────────────────────
|
|
24
|
+
const MIME = {
|
|
25
|
+
'.html': 'text/html; charset=utf-8',
|
|
26
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
27
|
+
'.mjs': 'application/javascript; charset=utf-8',
|
|
28
|
+
'.css': 'text/css; charset=utf-8',
|
|
29
|
+
'.json': 'application/json; charset=utf-8',
|
|
30
|
+
'.svg': 'image/svg+xml',
|
|
31
|
+
'.png': 'image/png',
|
|
32
|
+
'.ico': 'image/x-icon',
|
|
33
|
+
'.woff2':'font/woff2',
|
|
34
|
+
'.clarity': 'application/javascript; charset=utf-8', // compiled on the fly
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ─── Dev client script (injected into HTML responses) ────────────────────────
|
|
38
|
+
//
|
|
39
|
+
// True HMR — when a .clarity file changes the server recompiles it and sends the
|
|
40
|
+
// new module code via SSE. The client imports the new module as a Blob URL and
|
|
41
|
+
// calls any registered accept handlers. If no handler is registered for that
|
|
42
|
+
// path, it falls back to a full location.reload().
|
|
43
|
+
//
|
|
44
|
+
// Usage in your main.js (optional, for zero-reload HMR):
|
|
45
|
+
//
|
|
46
|
+
// window.__clarity_hmr?.accept('/my-app/App.clarity', (newModule) => {
|
|
47
|
+
// unmount(); // unmount old root
|
|
48
|
+
// mount(newModule.default, document.getElementById('app'));
|
|
49
|
+
// });
|
|
50
|
+
//
|
|
51
|
+
// Without registering an accept handler the client still hot-reloads the page,
|
|
52
|
+
// which is already faster than a cold browser reload because the server has
|
|
53
|
+
// already recompiled before sending.
|
|
54
|
+
const DEV_CLIENT = `
|
|
55
|
+
<script>
|
|
56
|
+
/* Clarity.js HMR client — auto-injected, remove in production */
|
|
57
|
+
window.__clarity_hmr = window.__clarity_hmr || {
|
|
58
|
+
_handlers: new Map(),
|
|
59
|
+
accept(path, fn) { this._handlers.set(path, fn); },
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
(function() {
|
|
63
|
+
let retries = 0;
|
|
64
|
+
function connect() {
|
|
65
|
+
const es = new EventSource('/__clarity_hmr__');
|
|
66
|
+
|
|
67
|
+
// Module-level HMR: server sends compiled code + path
|
|
68
|
+
es.addEventListener('module-update', async (e) => {
|
|
69
|
+
try {
|
|
70
|
+
const { path, code } = JSON.parse(e.data);
|
|
71
|
+
const blob = new Blob([code], { type: 'application/javascript' });
|
|
72
|
+
const url = URL.createObjectURL(blob);
|
|
73
|
+
const mod = await import(url);
|
|
74
|
+
URL.revokeObjectURL(url);
|
|
75
|
+
|
|
76
|
+
const handler = window.__clarity_hmr._handlers.get(path);
|
|
77
|
+
if (handler) {
|
|
78
|
+
console.log('[Clarity HMR] ♻️ Hot-updating', path);
|
|
79
|
+
handler(mod);
|
|
80
|
+
} else {
|
|
81
|
+
console.log('[Clarity HMR] 🔄 No accept handler for', path, '— reloading');
|
|
82
|
+
location.reload();
|
|
83
|
+
}
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.error('[Clarity HMR] Failed to apply update:', err);
|
|
86
|
+
location.reload();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Fallback: CSS / HTML changes → full reload
|
|
91
|
+
es.addEventListener('reload', () => {
|
|
92
|
+
console.log('[Clarity HMR] 🔄 Asset changed — reloading…');
|
|
93
|
+
location.reload();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
es.addEventListener('error', () => {
|
|
97
|
+
es.close();
|
|
98
|
+
if (retries++ < 30) setTimeout(connect, 1000);
|
|
99
|
+
});
|
|
100
|
+
es.addEventListener('open', () => { retries = 0; });
|
|
101
|
+
}
|
|
102
|
+
connect();
|
|
103
|
+
})();
|
|
104
|
+
</script>
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
// ─── SSE client registry ──────────────────────────────────────────────────────
|
|
108
|
+
const _sseClients = new Set();
|
|
109
|
+
|
|
110
|
+
function _broadcast(event, data = '') {
|
|
111
|
+
const msg = `event: ${event}\ndata: ${data}\n\n`;
|
|
112
|
+
for (const res of _sseClients) {
|
|
113
|
+
try { res.write(msg); } catch (_) { _sseClients.delete(res); }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── Logger ───────────────────────────────────────────────────────────────────
|
|
118
|
+
const C = {
|
|
119
|
+
reset: '\x1b[0m',
|
|
120
|
+
green: '\x1b[32m',
|
|
121
|
+
yellow: '\x1b[33m',
|
|
122
|
+
cyan: '\x1b[36m',
|
|
123
|
+
red: '\x1b[31m',
|
|
124
|
+
gray: '\x1b[90m',
|
|
125
|
+
bold: '\x1b[1m',
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
function log(color, label, msg) {
|
|
129
|
+
const ts = new Date().toLocaleTimeString('en', { hour12: false });
|
|
130
|
+
console.log(`${C.gray}${ts}${C.reset} ${color}${label}${C.reset} ${msg}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── File watcher ────────────────────────────────────────────────────────────
|
|
134
|
+
function watchDir(dir) {
|
|
135
|
+
// Use fs.watch recursively where supported (Node 18+ on Linux/macOS)
|
|
136
|
+
try {
|
|
137
|
+
fs.watch(dir, { recursive: true }, (event, filename) => {
|
|
138
|
+
if (!filename) return;
|
|
139
|
+
if (filename.endsWith('.clarity')) {
|
|
140
|
+
_handleClarityChange(path.join(dir, filename), filename, dir);
|
|
141
|
+
} else if (filename.endsWith('.css') || filename.endsWith('.html')) {
|
|
142
|
+
log(C.yellow, '◆ changed', filename);
|
|
143
|
+
_broadcast('reload');
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
log(C.cyan, '◆ watching', dir);
|
|
147
|
+
} catch (_) {
|
|
148
|
+
// Fallback: poll every 500ms
|
|
149
|
+
log(C.yellow, '◆ watch', 'recursive watch not available — falling back to polling');
|
|
150
|
+
const mtimes = new Map();
|
|
151
|
+
setInterval(() => _scanMtimes(dir, mtimes, dir), 500);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Recompile a changed .clarity file and broadcast the new module code via SSE.
|
|
156
|
+
// This lets the HMR client hot-swap just the changed module without a full reload.
|
|
157
|
+
function _handleClarityChange(fullPath, filename, rootDir) {
|
|
158
|
+
log(C.yellow, '◆ changed', filename);
|
|
159
|
+
try {
|
|
160
|
+
const source = fs.readFileSync(fullPath, 'utf8');
|
|
161
|
+
const urlPath = '/' + path.relative(rootDir, fullPath).replace(/\\/g, '/');
|
|
162
|
+
const { code } = compile(source, {
|
|
163
|
+
filename: fullPath,
|
|
164
|
+
runtimePath: '/clarity-runtime.js',
|
|
165
|
+
routerPath: '/clarity-router.js',
|
|
166
|
+
sourceMap: true,
|
|
167
|
+
});
|
|
168
|
+
_broadcast('module-update', JSON.stringify({ path: urlPath, code }));
|
|
169
|
+
log(C.green, '♻ HMR', filename);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
log(C.red, '✗ error', `${filename} — ${err.message}`);
|
|
172
|
+
// On compile error, send a reload so the browser shows the error overlay
|
|
173
|
+
_broadcast('reload');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function _scanMtimes(dir, mtimes, rootDir) {
|
|
178
|
+
try {
|
|
179
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
180
|
+
const full = path.join(dir, entry.name);
|
|
181
|
+
if (entry.isDirectory()) {
|
|
182
|
+
_scanMtimes(full, mtimes, rootDir);
|
|
183
|
+
} else if (entry.name.endsWith('.clarity')) {
|
|
184
|
+
const mtime = fs.statSync(full).mtimeMs;
|
|
185
|
+
if (mtimes.has(full) && mtimes.get(full) !== mtime) {
|
|
186
|
+
_handleClarityChange(full, entry.name, rootDir);
|
|
187
|
+
}
|
|
188
|
+
mtimes.set(full, mtime);
|
|
189
|
+
} else if (entry.name.endsWith('.css') || entry.name.endsWith('.html')) {
|
|
190
|
+
const mtime = fs.statSync(full).mtimeMs;
|
|
191
|
+
if (mtimes.has(full) && mtimes.get(full) !== mtime) {
|
|
192
|
+
log(C.yellow, '◆ changed', entry.name);
|
|
193
|
+
_broadcast('reload');
|
|
194
|
+
}
|
|
195
|
+
mtimes.set(full, mtime);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} catch (_) { /* skip unreadable dirs */ }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ─── Request handler ──────────────────────────────────────────────────────────
|
|
202
|
+
function _handleRequest(req, res, rootDir) {
|
|
203
|
+
const url = new URL(req.url, 'http://localhost');
|
|
204
|
+
|
|
205
|
+
// SSE endpoint — keep connection open for live reload events
|
|
206
|
+
if (url.pathname === '/__clarity_hmr__') {
|
|
207
|
+
res.writeHead(200, {
|
|
208
|
+
'Content-Type': 'text/event-stream',
|
|
209
|
+
'Cache-Control': 'no-cache',
|
|
210
|
+
'Connection': 'keep-alive',
|
|
211
|
+
'Access-Control-Allow-Origin': '*',
|
|
212
|
+
});
|
|
213
|
+
res.write('event: connected\ndata: ok\n\n');
|
|
214
|
+
_sseClients.add(res);
|
|
215
|
+
req.on('close', () => _sseClients.delete(res));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Resolve file path
|
|
220
|
+
let filePath = path.join(rootDir, url.pathname);
|
|
221
|
+
|
|
222
|
+
// Directory → try index.html
|
|
223
|
+
try {
|
|
224
|
+
const stat = fs.statSync(filePath);
|
|
225
|
+
if (stat.isDirectory()) filePath = path.join(filePath, 'index.html');
|
|
226
|
+
} catch (_) { /* file doesn't exist — handle below */ }
|
|
227
|
+
|
|
228
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
229
|
+
|
|
230
|
+
// .clarity files → compile on the fly and serve as JS
|
|
231
|
+
if (ext === '.clarity') {
|
|
232
|
+
try {
|
|
233
|
+
const source = fs.readFileSync(filePath, 'utf8');
|
|
234
|
+
const { code } = compile(source, {
|
|
235
|
+
filename: filePath,
|
|
236
|
+
runtimePath: '/clarity-runtime.js',
|
|
237
|
+
routerPath: '/clarity-router.js',
|
|
238
|
+
sourceMap: true,
|
|
239
|
+
});
|
|
240
|
+
res.writeHead(200, { 'Content-Type': MIME['.js'] });
|
|
241
|
+
res.end(code);
|
|
242
|
+
log(C.green, '✓ compiled', url.pathname);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
const errMsg = `// Clarity compile error: ${err.message}\nconsole.error(${JSON.stringify(err.message)});`;
|
|
245
|
+
res.writeHead(200, { 'Content-Type': MIME['.js'] });
|
|
246
|
+
res.end(errMsg);
|
|
247
|
+
log(C.red, '✗ error', `${url.pathname} — ${err.message}`);
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Serve runtime and router from the clarity-js package itself
|
|
253
|
+
if (url.pathname === '/clarity-runtime.js') {
|
|
254
|
+
_serveFile(res, new URL('../src/runtime.js', import.meta.url).pathname, '.js');
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (url.pathname === '/clarity-router.js') {
|
|
258
|
+
try {
|
|
259
|
+
const routerPath = new URL('../src/router.js', import.meta.url).pathname;
|
|
260
|
+
let content = fs.readFileSync(routerPath, 'utf8');
|
|
261
|
+
// Rewrite the relative runtime import so it resolves correctly in the browser.
|
|
262
|
+
// src/router.js says: import { signal, effect } from './runtime.js'
|
|
263
|
+
// In the browser context that would resolve to /runtime.js (404).
|
|
264
|
+
// We rewrite it to the absolute dev-server path instead.
|
|
265
|
+
content = content.replace(/from ['"]\.\/runtime\.js['"]/g, "from '/clarity-runtime.js'");
|
|
266
|
+
res.writeHead(200, { 'Content-Type': MIME['.js'] });
|
|
267
|
+
res.end(content);
|
|
268
|
+
} catch (err) {
|
|
269
|
+
res.writeHead(500);
|
|
270
|
+
res.end(`// could not read router: ${err.message}`);
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Regular static file
|
|
276
|
+
try {
|
|
277
|
+
let content = fs.readFileSync(filePath);
|
|
278
|
+
|
|
279
|
+
// Inject dev client into HTML files
|
|
280
|
+
if (ext === '.html') {
|
|
281
|
+
content = content.toString('utf8').replace('</body>', `${DEV_CLIENT}\n</body>`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
res.writeHead(200, { 'Content-Type': MIME[ext] ?? 'application/octet-stream' });
|
|
285
|
+
res.end(content);
|
|
286
|
+
log(C.gray, '→', url.pathname);
|
|
287
|
+
} catch (_) {
|
|
288
|
+
// 404
|
|
289
|
+
res.writeHead(404, { 'Content-Type': 'text/html' });
|
|
290
|
+
res.end(`<!DOCTYPE html><html><body>
|
|
291
|
+
<h1>404 — Not Found</h1>
|
|
292
|
+
<p><code>${url.pathname}</code> does not exist in <code>${rootDir}</code></p>
|
|
293
|
+
<p><a href="/">← Back</a></p>
|
|
294
|
+
</body></html>`);
|
|
295
|
+
log(C.yellow, '404', url.pathname);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function _serveFile(res, filePath, ext) {
|
|
300
|
+
try {
|
|
301
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
302
|
+
res.writeHead(200, { 'Content-Type': MIME[ext] ?? 'application/javascript' });
|
|
303
|
+
res.end(content);
|
|
304
|
+
} catch (err) {
|
|
305
|
+
res.writeHead(500);
|
|
306
|
+
res.end(`// could not read file: ${err.message}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Start the Clarity dev server.
|
|
314
|
+
*
|
|
315
|
+
* @param {object} opts
|
|
316
|
+
* @param {string} opts.dir — directory to serve (default: cwd)
|
|
317
|
+
* @param {number} opts.port — port (default: 3000)
|
|
318
|
+
* @param {boolean} opts.open — open browser on start
|
|
319
|
+
*/
|
|
320
|
+
export function startDevServer({ dir = process.cwd(), port = 3000, open = false } = {}) {
|
|
321
|
+
const rootDir = path.resolve(dir);
|
|
322
|
+
|
|
323
|
+
const server = http.createServer((req, res) => {
|
|
324
|
+
_handleRequest(req, res, rootDir);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
server.listen(port, () => {
|
|
328
|
+
console.log('');
|
|
329
|
+
console.log(` ${C.bold}${C.cyan}Clarity.js Dev Server${C.reset}`);
|
|
330
|
+
console.log(` ${C.green}➜${C.reset} Local: ${C.cyan}http://localhost:${port}${C.reset}`);
|
|
331
|
+
console.log(` ${C.green}➜${C.reset} Root: ${C.gray}${rootDir}${C.reset}`);
|
|
332
|
+
console.log(` ${C.green}➜${C.reset} Press ${C.bold}Ctrl+C${C.reset} to stop`);
|
|
333
|
+
console.log('');
|
|
334
|
+
|
|
335
|
+
watchDir(rootDir);
|
|
336
|
+
|
|
337
|
+
if (open) {
|
|
338
|
+
// Try to open browser cross-platform
|
|
339
|
+
const opener = process.platform === 'darwin' ? 'open'
|
|
340
|
+
: process.platform === 'win32' ? 'start'
|
|
341
|
+
: 'xdg-open';
|
|
342
|
+
import('child_process').then(({ exec }) => exec(`${opener} http://localhost:${port}`));
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
server.on('error', (err) => {
|
|
347
|
+
if (err.code === 'EADDRINUSE') {
|
|
348
|
+
console.error(`${C.red}✗ Port ${port} is already in use.${C.reset} Try --port ${port + 1}`);
|
|
349
|
+
} else {
|
|
350
|
+
console.error(`${C.red}✗ Server error:${C.reset}`, err.message);
|
|
351
|
+
}
|
|
352
|
+
process.exit(1);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Graceful shutdown
|
|
356
|
+
process.on('SIGINT', () => {
|
|
357
|
+
console.log(`\n${C.yellow}◆ Shutting down…${C.reset}`);
|
|
358
|
+
server.close(() => process.exit(0));
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
return server;
|
|
362
|
+
}
|