@fiyuu/runtime 0.1.1 → 0.4.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.
@@ -1,528 +0,0 @@
1
- /**
2
- * Fiyuu Client Runtime
3
- *
4
- * A small script injected into every page that provides:
5
- * - fiyuu.theme — dark/light mode management
6
- * - fiyuu.state — simple reactive state with DOM binding
7
- * - fiyuu.bind — shorthand for updating element text / html
8
- * - fiyuu.router — client-side navigation without page reload
9
- * - fiyuu.partial — replace a DOM element with a fetched route's content
10
- * - fiyuu.onError — global client-side error handler
11
- * - fiyuu.ws — WebSocket connection helper
12
- *
13
- * Everything is accessible via window.fiyuu in page scripts.
14
- */
15
-
16
- export function buildClientRuntime(websocketPath: string): string {
17
- return `(function(){
18
- window.fiyuu = {
19
-
20
- // ── Theme ──────────────────────────────────────────────────────────────────
21
- // fiyuu.theme.get() → "light" | "dark"
22
- // fiyuu.theme.set("dark") → sets theme, saves to localStorage
23
- // fiyuu.theme.toggle() → flips between light and dark
24
- // fiyuu.theme.bindToggle("id") → wires a button to toggle + updates its label
25
- // fiyuu.theme.onChange(fn) → calls fn whenever theme changes
26
- theme: {
27
- get: function() {
28
- return document.documentElement.classList.contains("dark") ? "dark" : "light";
29
- },
30
- set: function(value) {
31
- var isDark = value === "dark";
32
- document.documentElement.classList.toggle("dark", isDark);
33
- document.documentElement.setAttribute("data-theme", value);
34
- try { localStorage.setItem("fiyuu-theme", value); } catch(e) {}
35
- document.dispatchEvent(new CustomEvent("fiyuu:theme", { detail: { theme: value } }));
36
- },
37
- toggle: function() {
38
- this.set(this.get() === "dark" ? "light" : "dark");
39
- },
40
- bindToggle: function(elementId) {
41
- var el = document.getElementById(elementId);
42
- if (!el) return;
43
- var self = this;
44
- var sync = function() { el.textContent = self.get() === "dark" ? "Light" : "Dark"; };
45
- sync();
46
- el.addEventListener("click", function() { self.toggle(); sync(); });
47
- },
48
- onChange: function(callback) {
49
- document.addEventListener("fiyuu:theme", function(e) { callback(e.detail.theme); });
50
- }
51
- },
52
-
53
- // ── Bind ───────────────────────────────────────────────────────────────────
54
- // fiyuu.bind("element-id", value) → sets element's text content
55
- // fiyuu.bind("element-id", value, true) → sets innerHTML instead
56
- bind: function(elementId, value, asHtml) {
57
- var el = document.getElementById(elementId);
58
- if (!el) return;
59
- if (asHtml) { el.innerHTML = String(value != null ? value : ""); }
60
- else { el.textContent = String(value != null ? value : ""); }
61
- },
62
-
63
- // ── State ──────────────────────────────────────────────────────────────────
64
- // var counter = fiyuu.state("counter", 0)
65
- // counter.get() → current value
66
- // counter.set(5) → updates value, fires "fiyuu:state:counter" event
67
- // counter.bind("element-id") → auto-updates element when state changes
68
- // counter.onChange(fn) → calls fn(newValue) on every update
69
- state: function(key, initialValue) {
70
- var current = initialValue;
71
- var eventName = "fiyuu:state:" + key;
72
- return {
73
- get: function() { return current; },
74
- set: function(next) {
75
- current = next;
76
- document.dispatchEvent(new CustomEvent(eventName, { detail: next }));
77
- },
78
- bind: function(elementId) {
79
- window.fiyuu.bind(elementId, current);
80
- document.addEventListener(eventName, function(e) {
81
- window.fiyuu.bind(elementId, e.detail);
82
- });
83
- return this;
84
- },
85
- onChange: function(callback) {
86
- document.addEventListener(eventName, function(e) { callback(e.detail); });
87
- return this;
88
- }
89
- };
90
- },
91
-
92
- // ── Partial ────────────────────────────────────────────────────────────────
93
- // fiyuu.partial("element-id", "/route") → fetches route body, replaces element
94
- // fiyuu.partial("element-id", "/route", { loading: "<p>Loading…</p>" })
95
- partial: async function(elementId, url, options) {
96
- var el = document.getElementById(elementId);
97
- if (!el) return;
98
- if (options && options.loading) el.innerHTML = options.loading;
99
- try {
100
- var parsed = new URL(url, location.href);
101
- var res = await fetch(parsed.pathname + parsed.search, {
102
- headers: { "x-fiyuu-navigate": "1" },
103
- credentials: "same-origin",
104
- });
105
- if (!res.ok || res.headers.get("x-fiyuu-navigate") !== "1") return;
106
- var payload = await res.json();
107
- el.innerHTML = payload.body || "";
108
- var scripts = el.querySelectorAll("script");
109
- for (var i = 0; i < scripts.length; i++) {
110
- var old = scripts[i];
111
- var next = document.createElement("script");
112
- for (var j = 0; j < old.attributes.length; j++) next.setAttribute(old.attributes[j].name, old.attributes[j].value);
113
- next.textContent = old.textContent;
114
- old.parentNode.replaceChild(next, old);
115
- }
116
- } catch(e) {}
117
- },
118
-
119
- // ── onError ────────────────────────────────────────────────────────────────
120
- // fiyuu.onError(function(event) { ... }) → called on unhandled JS errors
121
- onError: function(callback) {
122
- window.addEventListener("error", function(event) {
123
- callback({ message: event.message, error: event.error, source: event.filename, line: event.lineno });
124
- });
125
- window.addEventListener("unhandledrejection", function(event) {
126
- var reason = event.reason instanceof Error ? event.reason : new Error(String(event.reason || "Unhandled rejection"));
127
- callback({ message: reason.message, error: reason, source: null, line: null });
128
- });
129
- },
130
-
131
- // ── Router ─────────────────────────────────────────────────────────────────
132
- // fiyuu.router.navigate("/about") → client-side navigation (no reload)
133
- // fiyuu.router.on("navigate", fn) → called after each navigation with { route, render }
134
- // fiyuu.router.on("before", fn) → called before navigation; return false to cancel
135
- // Links with data-fiyuu-link (or all same-origin <a> tags) are intercepted automatically.
136
- router: (function() {
137
- var listeners = { navigate: [], before: [] };
138
- var navCache = new Map();
139
- var inflightNav = new Map();
140
- var inflightPrefetch = new Map();
141
- var prefetchedRoutes = new Set();
142
- var MAX_NAV_CACHE = 24;
143
-
144
- function canPrefetch() {
145
- var connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
146
- if (!connection) return true;
147
- if (connection.saveData) return false;
148
- var type = connection.effectiveType || "";
149
- return type !== "slow-2g" && type !== "2g";
150
- }
151
-
152
- function emit(event, detail) {
153
- for (var i = 0; i < listeners[event].length; i++) {
154
- if (listeners[event][i](detail) === false) return false;
155
- }
156
- return true;
157
- }
158
-
159
- function rerunScripts(container) {
160
- var scripts = container.querySelectorAll("script");
161
- for (var i = 0; i < scripts.length; i++) {
162
- var old = scripts[i];
163
- var next = document.createElement("script");
164
- for (var j = 0; j < old.attributes.length; j++) {
165
- next.setAttribute(old.attributes[j].name, old.attributes[j].value);
166
- }
167
- next.textContent = old.textContent;
168
- old.parentNode.replaceChild(next, old);
169
- }
170
- }
171
-
172
- function rememberRoute(key, payload) {
173
- if (!key) return;
174
- if (navCache.has(key)) navCache.delete(key);
175
- navCache.set(key, payload);
176
- if (navCache.size > MAX_NAV_CACHE) {
177
- var firstKey = navCache.keys().next();
178
- if (!firstKey.done) navCache.delete(firstKey.value);
179
- }
180
- }
181
-
182
- function scheduleIdle(task) {
183
- if (typeof window.requestIdleCallback === "function") {
184
- window.requestIdleCallback(task, { timeout: 1200 });
185
- } else {
186
- setTimeout(task, 80);
187
- }
188
- }
189
-
190
- function prefetchRouteFromHref(href) {
191
- if (!canPrefetch()) return;
192
- if (!href || href.startsWith("#") || href.startsWith("mailto:") || href.startsWith("tel:")) return;
193
- var parsed = new URL(href, location.href);
194
- if (parsed.origin !== location.origin) return;
195
- var cacheKey = parsed.pathname + parsed.search;
196
- if (prefetchedRoutes.has(cacheKey)) return;
197
- if (navCache.has(cacheKey) || inflightNav.has(cacheKey) || inflightPrefetch.has(cacheKey)) return;
198
-
199
- scheduleIdle(function() {
200
- var pending = fetch(cacheKey, {
201
- headers: { "x-fiyuu-navigate": "1" },
202
- credentials: "same-origin",
203
- }).then(function(res) {
204
- if (!res.ok || res.headers.get("x-fiyuu-navigate") !== "1") return null;
205
- return res.json();
206
- }).then(function(payload) {
207
- if (payload) {
208
- prefetchedRoutes.add(cacheKey);
209
- rememberRoute(cacheKey, payload);
210
- }
211
- }).catch(function() {
212
- return null;
213
- }).finally(function() {
214
- inflightPrefetch.delete(cacheKey);
215
- });
216
- inflightPrefetch.set(cacheKey, pending);
217
- });
218
- }
219
-
220
- function applyPayload(payload, parsed, push) {
221
- var app = document.getElementById("app");
222
- if (app) {
223
- app.innerHTML = payload.body || "";
224
- rerunScripts(app);
225
- }
226
- document.title = payload.title || document.title;
227
- window.__FIYUU_ROUTE__ = payload.route;
228
- window.__FIYUU_DATA__ = payload.data;
229
- window.__FIYUU_RENDER__ = payload.render;
230
- if (push !== false) history.pushState({ route: payload.route }, payload.title || "", parsed.pathname + parsed.search);
231
- window.scrollTo(0, 0);
232
- emit("navigate", { route: payload.route, render: payload.render, title: payload.title });
233
- }
234
-
235
- async function navigate(url, push) {
236
- var href = typeof url === "string" ? url : url.href;
237
- var parsed = new URL(href, location.href);
238
- var cacheKey = parsed.pathname + parsed.search;
239
- if (parsed.origin !== location.origin) { location.href = href; return; }
240
- if (push !== false && cacheKey === location.pathname + location.search) return;
241
- if (emit("before", { route: parsed.pathname }) === false) return;
242
-
243
- if (navCache.has(cacheKey)) {
244
- applyPayload(navCache.get(cacheKey), parsed, push);
245
- return;
246
- }
247
-
248
- try {
249
- var prefetched = inflightPrefetch.get(cacheKey);
250
- if (prefetched) {
251
- var prefetchedPayload = await prefetched;
252
- inflightPrefetch.delete(cacheKey);
253
- if (prefetchedPayload) {
254
- rememberRoute(cacheKey, prefetchedPayload);
255
- applyPayload(prefetchedPayload, parsed, push);
256
- return;
257
- }
258
- }
259
-
260
- var pending = inflightNav.get(cacheKey);
261
- if (!pending) {
262
- pending = fetch(cacheKey, {
263
- headers: { "x-fiyuu-navigate": "1" },
264
- credentials: "same-origin",
265
- });
266
- inflightNav.set(cacheKey, pending);
267
- }
268
- var res = await pending;
269
- inflightNav.delete(cacheKey);
270
- if (!res.ok || res.headers.get("x-fiyuu-navigate") !== "1") {
271
- location.href = href;
272
- return;
273
- }
274
- var payload = await res.json();
275
- rememberRoute(cacheKey, payload);
276
- applyPayload(payload, parsed, push);
277
- } catch(e) {
278
- inflightNav.delete(cacheKey);
279
- location.href = href;
280
- }
281
- }
282
-
283
- function intercept() {
284
- document.addEventListener("mouseover", function(event) {
285
- var target = event.target.closest("a[href]");
286
- if (!target) return;
287
- if (target.hasAttribute("data-fiyuu-no-prefetch")) return;
288
- prefetchRouteFromHref(target.getAttribute("href"));
289
- });
290
-
291
- document.addEventListener("focusin", function(event) {
292
- var target = event.target.closest("a[href]");
293
- if (!target) return;
294
- if (target.hasAttribute("data-fiyuu-no-prefetch")) return;
295
- prefetchRouteFromHref(target.getAttribute("href"));
296
- });
297
-
298
- if (typeof window.IntersectionObserver === "function") {
299
- var observer = new IntersectionObserver(function(entries) {
300
- entries.forEach(function(entry) {
301
- if (!entry.isIntersecting) return;
302
- var link = entry.target;
303
- if (link.hasAttribute("data-fiyuu-no-prefetch")) {
304
- observer.unobserve(link);
305
- return;
306
- }
307
- prefetchRouteFromHref(link.getAttribute("href"));
308
- observer.unobserve(link);
309
- });
310
- }, { rootMargin: "280px" });
311
-
312
- document.querySelectorAll("a[href]").forEach(function(link) {
313
- observer.observe(link);
314
- });
315
- }
316
-
317
- document.addEventListener("click", function(event) {
318
- var target = event.target.closest("a[href]");
319
- if (!target) return;
320
- var href = target.getAttribute("href");
321
- if (!href || href.startsWith("#") || href.startsWith("mailto:") || href.startsWith("tel:")) return;
322
- var parsed = new URL(href, location.href);
323
- if (parsed.origin !== location.origin) return;
324
- if (target.hasAttribute("data-fiyuu-reload")) return;
325
- event.preventDefault();
326
- navigate(parsed.pathname + parsed.search, true);
327
- });
328
- window.addEventListener("popstate", function(event) {
329
- navigate(location.pathname + location.search, false);
330
- });
331
- }
332
-
333
- document.addEventListener("DOMContentLoaded", intercept);
334
-
335
- return {
336
- navigate: function(url) { return navigate(url, true); },
337
- on: function(event, fn) {
338
- if (listeners[event]) listeners[event].push(fn);
339
- return this;
340
- },
341
- };
342
- })(),
343
-
344
- // ── Action ─────────────────────────────────────────────────────────────────
345
- // fiyuu.action('/blog', payload)
346
- // fiyuu.action('/blog', payload, { status: 'el-id', loading: 'Processing...', onSuccess: fn, onError: fn })
347
- // → POSTs JSON payload to route, returns response body (never throws)
348
- // → status: element id to display loading/result messages
349
- // → loading: message shown while request is in flight
350
- // → errorMessage: message shown on failure (default: 'İşlem başarısız oldu.')
351
- // → onSuccess(body): called when body.success === true
352
- // → onError(err): called on network/HTTP error
353
- action: async function(route, payload, options) {
354
- var opts = options || {};
355
- var statusEl = opts.status ? document.getElementById(opts.status) : null;
356
- if (statusEl && opts.loading != null) statusEl.textContent = opts.loading;
357
- try {
358
- var res = await fetch(route, {
359
- method: "POST",
360
- headers: { "content-type": "application/json", "accept": "application/json" },
361
- body: JSON.stringify(payload)
362
- });
363
- if (!res.ok) throw new Error("HTTP " + res.status);
364
- var body = await res.json();
365
- if (statusEl && body.message) statusEl.textContent = body.message;
366
- if (body.success && opts.onSuccess) opts.onSuccess(body);
367
- return body;
368
- } catch(err) {
369
- if (statusEl) statusEl.textContent = opts.errorMessage || "İşlem başarısız oldu.";
370
- if (opts.onError) opts.onError(err);
371
- return { success: false };
372
- }
373
- },
374
-
375
- // ── Modal ──────────────────────────────────────────────────────────────────
376
- // fiyuu.modal.open('my-modal') → shows element (display:flex)
377
- // fiyuu.modal.close('my-modal') → hides element (display:none)
378
- // fiyuu.modal.toggle('my-modal') → toggles visibility
379
- //
380
- // Declarative (auto-wired on DOMContentLoaded):
381
- // <div id="my-modal" data-fiyuu-modal style="display:none">…</div>
382
- // <button data-fiyuu-open="my-modal">Open</button>
383
- // <button data-fiyuu-close="my-modal">Close</button>
384
- // Clicking backdrop (the modal element itself) auto-closes it.
385
- modal: {
386
- open: function(id) {
387
- var el = document.getElementById(id);
388
- if (el) el.style.display = "flex";
389
- },
390
- close: function(id) {
391
- var el = document.getElementById(id);
392
- if (el) el.style.display = "none";
393
- },
394
- toggle: function(id) {
395
- var el = document.getElementById(id);
396
- if (!el) return;
397
- el.style.display = el.style.display === "none" ? "flex" : "none";
398
- }
399
- },
400
-
401
- // ── data ───────────────────────────────────────────────────────────────────
402
- // fiyuu.data('my-id') → parses JSON from <script type="application/json" id="my-id">
403
- // Use clientData() helper in page.tsx to embed server data safely.
404
- data: function(id) {
405
- var el = document.getElementById(id);
406
- if (!el) return null;
407
- try { return JSON.parse(el.textContent || "null"); } catch(e) { return null; }
408
- },
409
-
410
- // ── WebSocket ──────────────────────────────────────────────────────────────
411
- // var ws = fiyuu.ws()
412
- // ws.on("counter:tick", (data) => { ... }) → listens for a message type
413
- // ws.onOpen(fn) / ws.onClose(fn) / ws.onError(fn)
414
- // ws.send({ type: "ping" })
415
- // ws.status() → "connecting" | "connected" | "closed" | "unavailable"
416
- ws: function(overridePath) {
417
- var wsPath = overridePath || window.__FIYUU_WS_PATH__ || ${JSON.stringify(websocketPath)};
418
- var protocol = location.protocol === "https:" ? "wss" : "ws";
419
- var socket = new WebSocket(protocol + "://" + location.host + wsPath);
420
- var handlers = {};
421
- var statusValue = "connecting";
422
-
423
- socket.addEventListener("open", function() { statusValue = "connected"; });
424
- socket.addEventListener("close", function() {
425
- if (statusValue !== "unavailable") statusValue = "closed";
426
- });
427
- socket.addEventListener("error", function() { statusValue = "unavailable"; });
428
- socket.addEventListener("message", function(event) {
429
- try {
430
- var payload = JSON.parse(event.data);
431
- if (payload && payload.type && handlers[payload.type]) {
432
- handlers[payload.type](payload);
433
- }
434
- } catch(e) {}
435
- });
436
-
437
- return {
438
- on: function(type, handler) { handlers[type] = handler; return this; },
439
- onOpen: function(handler) { socket.addEventListener("open", handler); return this; },
440
- onClose: function(handler) { socket.addEventListener("close", handler); return this; },
441
- onError: function(handler) { socket.addEventListener("error", handler); return this; },
442
- send: function(data) { socket.send(JSON.stringify(data)); return this; },
443
- status: function() { return statusValue; },
444
- socket: socket
445
- };
446
- },
447
-
448
- // ── Channel (realtime) ─────────────────────────────────────────────────────
449
- // fiyuu.channel('chat').on('new-message', (data) => { ... })
450
- // fiyuu.channel('chat').emit('message', { text: 'hello' })
451
- channel: function(name) {
452
- var channelHandlers = {};
453
- var ws = window.fiyuu.ws();
454
-
455
- ws.socket.addEventListener("message", function(event) {
456
- try {
457
- var payload = JSON.parse(event.data);
458
- if (payload && payload.channel === name && payload.event && channelHandlers[payload.event]) {
459
- channelHandlers[payload.event](payload.data);
460
- }
461
- } catch(e) {}
462
- });
463
-
464
- return {
465
- on: function(event, handler) {
466
- channelHandlers[event] = handler;
467
- return this;
468
- },
469
- emit: function(event, data) {
470
- ws.send({ channel: name, event: event, data: data, ts: Date.now() });
471
- return this;
472
- },
473
- off: function(event) {
474
- delete channelHandlers[event];
475
- return this;
476
- }
477
- };
478
- }
479
-
480
- };
481
-
482
- // ── Declarative wiring (runs after DOM is ready) ───────────────────────────
483
- // Wires [data-fiyuu-open], [data-fiyuu-close], [data-fiyuu-modal] automatically.
484
- // Also handles forms with [data-fiyuu-action] for zero-boilerplate action calls.
485
- document.addEventListener("DOMContentLoaded", function() {
486
- // Modal openers: <button data-fiyuu-open="modal-id">
487
- document.querySelectorAll("[data-fiyuu-open]").forEach(function(el) {
488
- el.addEventListener("click", function() {
489
- window.fiyuu.modal.open(el.getAttribute("data-fiyuu-open"));
490
- });
491
- });
492
- // Modal closers: <button data-fiyuu-close="modal-id">
493
- document.querySelectorAll("[data-fiyuu-close]").forEach(function(el) {
494
- el.addEventListener("click", function() {
495
- window.fiyuu.modal.close(el.getAttribute("data-fiyuu-close"));
496
- });
497
- });
498
- // Backdrop close: <div id="modal-id" data-fiyuu-modal …>
499
- document.querySelectorAll("[data-fiyuu-modal]").forEach(function(modal) {
500
- modal.addEventListener("click", function(e) {
501
- if (e.target === modal) window.fiyuu.modal.close(modal.id);
502
- });
503
- });
504
- // Declarative form actions:
505
- // <form data-fiyuu-action="/route" data-fiyuu-status="el-id" data-fiyuu-success="reload|close:modal-id">
506
- document.querySelectorAll("form[data-fiyuu-action]").forEach(function(form) {
507
- form.addEventListener("submit", async function(e) {
508
- e.preventDefault();
509
- var route = form.getAttribute("data-fiyuu-action");
510
- var statusId = form.getAttribute("data-fiyuu-status") || undefined;
511
- var successAction = form.getAttribute("data-fiyuu-success") || "reload";
512
- var loadingMsg = form.getAttribute("data-fiyuu-loading") || "İşleniyor...";
513
- var data = {};
514
- new FormData(form).forEach(function(value, key) { data[key] = value; });
515
- form.querySelectorAll("input[type=checkbox]").forEach(function(input) {
516
- data[input.name] = input.checked;
517
- });
518
- var body = await window.fiyuu.action(route, data, { status: statusId, loading: loadingMsg });
519
- if (body.success) {
520
- if (successAction === "reload") { setTimeout(function() { location.reload(); }, 300); }
521
- else if (successAction.startsWith("close:")) { window.fiyuu.modal.close(successAction.slice(6)); }
522
- }
523
- });
524
- });
525
- });
526
-
527
- })();`;
528
- }
package/src/index.ts DELETED
@@ -1,4 +0,0 @@
1
- export * from "./server.js";
2
- export * from "./bundler.js";
3
- export * from "./inspector.js";
4
- export * from "./service.js";