@netrojs/fnetro 0.1.2
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/LICENSE +21 -0
- package/README.md +179 -0
- package/client.ts +307 -0
- package/core.ts +734 -0
- package/dist/client.d.ts +196 -0
- package/dist/client.js +673 -0
- package/dist/core.d.ts +200 -0
- package/dist/core.js +495 -0
- package/dist/server.d.ts +231 -0
- package/dist/server.js +720 -0
- package/package.json +91 -0
- package/server.ts +415 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
// server.ts
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { jsx } from "hono/jsx";
|
|
4
|
+
import { renderToString } from "hono/jsx/dom/server";
|
|
5
|
+
|
|
6
|
+
// core.ts
|
|
7
|
+
var RAW = /* @__PURE__ */ Symbol("raw");
|
|
8
|
+
var IS_REACTIVE = /* @__PURE__ */ Symbol("isReactive");
|
|
9
|
+
var IS_READONLY = /* @__PURE__ */ Symbol("isReadonly");
|
|
10
|
+
var IS_REF = /* @__PURE__ */ Symbol("isRef");
|
|
11
|
+
var MARK_RAW = /* @__PURE__ */ Symbol("markRaw");
|
|
12
|
+
var targetMap = /* @__PURE__ */ new WeakMap();
|
|
13
|
+
var activeEffect = null;
|
|
14
|
+
var shouldTrack = true;
|
|
15
|
+
var trackStack = [];
|
|
16
|
+
function pauseTracking() {
|
|
17
|
+
trackStack.push(shouldTrack);
|
|
18
|
+
shouldTrack = false;
|
|
19
|
+
}
|
|
20
|
+
function resetTracking() {
|
|
21
|
+
shouldTrack = trackStack.pop() ?? true;
|
|
22
|
+
}
|
|
23
|
+
function track(target, key) {
|
|
24
|
+
if (!shouldTrack || !activeEffect) return;
|
|
25
|
+
let depsMap = targetMap.get(target);
|
|
26
|
+
if (!depsMap) targetMap.set(target, depsMap = /* @__PURE__ */ new Map());
|
|
27
|
+
let dep = depsMap.get(key);
|
|
28
|
+
if (!dep) depsMap.set(key, dep = /* @__PURE__ */ new Set());
|
|
29
|
+
trackEffect(activeEffect, dep);
|
|
30
|
+
}
|
|
31
|
+
function trackEffect(effect2, dep) {
|
|
32
|
+
if (!dep.has(effect2)) {
|
|
33
|
+
dep.add(effect2);
|
|
34
|
+
effect2.deps.push(dep);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function trigger(target, key, newVal, oldVal) {
|
|
38
|
+
const depsMap = targetMap.get(target);
|
|
39
|
+
if (!depsMap) return;
|
|
40
|
+
const effects = [];
|
|
41
|
+
const computedEffects = [];
|
|
42
|
+
depsMap.get(key)?.forEach((e) => {
|
|
43
|
+
if (e !== activeEffect) {
|
|
44
|
+
e.computed ? computedEffects.push(e) : effects.push(e);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
[...computedEffects, ...effects].forEach((e) => {
|
|
48
|
+
if (e.active) e.scheduler ? e.scheduler() : e.run();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
var ReactiveEffect = class {
|
|
52
|
+
constructor(fn, scheduler, scope) {
|
|
53
|
+
this.fn = fn;
|
|
54
|
+
this.scheduler = scheduler;
|
|
55
|
+
this.scope = scope;
|
|
56
|
+
scope?.effects.push(this);
|
|
57
|
+
}
|
|
58
|
+
deps = [];
|
|
59
|
+
active = true;
|
|
60
|
+
cleanup;
|
|
61
|
+
computed = false;
|
|
62
|
+
run() {
|
|
63
|
+
if (!this.active) return this.fn();
|
|
64
|
+
const prevEffect = activeEffect;
|
|
65
|
+
const prevShouldTrack = shouldTrack;
|
|
66
|
+
shouldTrack = true;
|
|
67
|
+
activeEffect = this;
|
|
68
|
+
this.cleanup?.();
|
|
69
|
+
this.cleanup = void 0;
|
|
70
|
+
this.deps.length = 0;
|
|
71
|
+
try {
|
|
72
|
+
const result = this.fn();
|
|
73
|
+
if (typeof result === "function") this.cleanup = result;
|
|
74
|
+
return result;
|
|
75
|
+
} finally {
|
|
76
|
+
activeEffect = prevEffect;
|
|
77
|
+
shouldTrack = prevShouldTrack;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
stop() {
|
|
81
|
+
if (this.active) {
|
|
82
|
+
cleanupEffect(this);
|
|
83
|
+
this.active = false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
function cleanupEffect(e) {
|
|
88
|
+
e.deps.forEach((dep) => dep.delete(e));
|
|
89
|
+
e.deps.length = 0;
|
|
90
|
+
}
|
|
91
|
+
var activeScope;
|
|
92
|
+
var EffectScope = class {
|
|
93
|
+
effects = [];
|
|
94
|
+
cleanups = [];
|
|
95
|
+
active = true;
|
|
96
|
+
run(fn) {
|
|
97
|
+
const prev = activeScope;
|
|
98
|
+
activeScope = this;
|
|
99
|
+
try {
|
|
100
|
+
return fn();
|
|
101
|
+
} finally {
|
|
102
|
+
activeScope = prev;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
stop() {
|
|
106
|
+
if (this.active) {
|
|
107
|
+
this.effects.forEach((e) => e.stop());
|
|
108
|
+
this.cleanups.forEach((fn) => fn());
|
|
109
|
+
this.active = false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
onCleanup(fn) {
|
|
113
|
+
this.cleanups.push(fn);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
function effectScope() {
|
|
117
|
+
return new EffectScope();
|
|
118
|
+
}
|
|
119
|
+
function effect(fn) {
|
|
120
|
+
const e = new ReactiveEffect(fn, void 0, activeScope);
|
|
121
|
+
e.run();
|
|
122
|
+
return () => e.stop();
|
|
123
|
+
}
|
|
124
|
+
function watchEffect(fn, opts) {
|
|
125
|
+
const e = new ReactiveEffect(fn, void 0, activeScope);
|
|
126
|
+
e.run();
|
|
127
|
+
return () => e.stop();
|
|
128
|
+
}
|
|
129
|
+
var refTarget = /* @__PURE__ */ Symbol("refTarget");
|
|
130
|
+
var RefImpl = class {
|
|
131
|
+
constructor(value, shallow = false) {
|
|
132
|
+
this.shallow = shallow;
|
|
133
|
+
this._value = shallow ? value : toReactive(value);
|
|
134
|
+
}
|
|
135
|
+
[IS_REF] = true;
|
|
136
|
+
_value;
|
|
137
|
+
_subscribers = /* @__PURE__ */ new Set();
|
|
138
|
+
get value() {
|
|
139
|
+
track(this, refTarget);
|
|
140
|
+
this._subscribers.forEach((fn) => {
|
|
141
|
+
});
|
|
142
|
+
return this._value;
|
|
143
|
+
}
|
|
144
|
+
set value(next) {
|
|
145
|
+
const newVal = this.shallow ? next : toReactive(next);
|
|
146
|
+
if (!hasChanged(newVal, this._value)) return;
|
|
147
|
+
this._value = newVal;
|
|
148
|
+
trigger(this, refTarget, newVal, this._value);
|
|
149
|
+
this._subscribers.forEach((fn) => fn());
|
|
150
|
+
}
|
|
151
|
+
/** Subscribe for useSyncExternalStore */
|
|
152
|
+
subscribe(fn) {
|
|
153
|
+
this._subscribers.add(fn);
|
|
154
|
+
return () => this._subscribers.delete(fn);
|
|
155
|
+
}
|
|
156
|
+
peek() {
|
|
157
|
+
return this._value;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
function ref(value) {
|
|
161
|
+
return isRef(value) ? value : new RefImpl(value);
|
|
162
|
+
}
|
|
163
|
+
function shallowRef(value) {
|
|
164
|
+
return new RefImpl(value, true);
|
|
165
|
+
}
|
|
166
|
+
function triggerRef(r) {
|
|
167
|
+
if (r instanceof RefImpl) {
|
|
168
|
+
trigger(r, refTarget);
|
|
169
|
+
r._subscribers.forEach((fn) => fn());
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function isRef(r) {
|
|
173
|
+
return !!r && typeof r === "object" && r[IS_REF] === true;
|
|
174
|
+
}
|
|
175
|
+
function unref(r) {
|
|
176
|
+
return isRef(r) ? r.value : r;
|
|
177
|
+
}
|
|
178
|
+
function toRef(obj, key) {
|
|
179
|
+
const r = new RefImpl(void 0, false);
|
|
180
|
+
Object.defineProperty(r, "value", {
|
|
181
|
+
get() {
|
|
182
|
+
track(r, refTarget);
|
|
183
|
+
return obj[key];
|
|
184
|
+
},
|
|
185
|
+
set(v) {
|
|
186
|
+
obj[key] = v;
|
|
187
|
+
trigger(r, refTarget, v, obj[key]);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
return r;
|
|
191
|
+
}
|
|
192
|
+
function toRefs(obj) {
|
|
193
|
+
const result = {};
|
|
194
|
+
for (const key in obj) result[key] = toRef(obj, key);
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
var ComputedRefImpl = class {
|
|
198
|
+
constructor(getter, setter) {
|
|
199
|
+
this.setter = setter;
|
|
200
|
+
this.effect = new ReactiveEffect(getter, () => {
|
|
201
|
+
if (!this._dirty) {
|
|
202
|
+
this._dirty = true;
|
|
203
|
+
trigger(this, refTarget);
|
|
204
|
+
this._subscribers.forEach((fn) => fn());
|
|
205
|
+
}
|
|
206
|
+
}, activeScope);
|
|
207
|
+
this.effect.computed = true;
|
|
208
|
+
}
|
|
209
|
+
[IS_REF] = true;
|
|
210
|
+
effect;
|
|
211
|
+
_value;
|
|
212
|
+
_dirty = true;
|
|
213
|
+
_subscribers = /* @__PURE__ */ new Set();
|
|
214
|
+
get value() {
|
|
215
|
+
track(this, refTarget);
|
|
216
|
+
if (this._dirty) {
|
|
217
|
+
this._dirty = false;
|
|
218
|
+
this._value = this.effect.run();
|
|
219
|
+
}
|
|
220
|
+
return this._value;
|
|
221
|
+
}
|
|
222
|
+
set value(v) {
|
|
223
|
+
this.setter?.(v);
|
|
224
|
+
}
|
|
225
|
+
subscribe(fn) {
|
|
226
|
+
this._subscribers.add(fn);
|
|
227
|
+
return () => this._subscribers.delete(fn);
|
|
228
|
+
}
|
|
229
|
+
peek() {
|
|
230
|
+
return this._value;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
function computed(arg) {
|
|
234
|
+
if (typeof arg === "function") {
|
|
235
|
+
return new ComputedRefImpl(arg);
|
|
236
|
+
}
|
|
237
|
+
return new ComputedRefImpl(arg.get, arg.set);
|
|
238
|
+
}
|
|
239
|
+
var reactiveMap = /* @__PURE__ */ new WeakMap();
|
|
240
|
+
var readonlyMap = /* @__PURE__ */ new WeakMap();
|
|
241
|
+
var shallowReactiveMap = /* @__PURE__ */ new WeakMap();
|
|
242
|
+
function toReactive(value) {
|
|
243
|
+
return value !== null && typeof value === "object" ? reactive(value) : value;
|
|
244
|
+
}
|
|
245
|
+
var arrayInstrumentations = {};
|
|
246
|
+
["includes", "indexOf", "lastIndexOf"].forEach((method) => {
|
|
247
|
+
arrayInstrumentations[method] = function(...args) {
|
|
248
|
+
const arr = toRaw(this);
|
|
249
|
+
for (let i = 0; i < this.length; i++) track(arr, i);
|
|
250
|
+
let res = arr[method](...args);
|
|
251
|
+
if (res === -1 || res === false) res = arr[method](...args.map(toRaw));
|
|
252
|
+
return res;
|
|
253
|
+
};
|
|
254
|
+
});
|
|
255
|
+
["push", "pop", "shift", "unshift", "splice"].forEach((method) => {
|
|
256
|
+
arrayInstrumentations[method] = function(...args) {
|
|
257
|
+
pauseTracking();
|
|
258
|
+
const res = toRaw(this)[method].apply(this, args);
|
|
259
|
+
resetTracking();
|
|
260
|
+
return res;
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
function createHandler(shallow = false, readonly2 = false) {
|
|
264
|
+
return {
|
|
265
|
+
get(target, key, receiver) {
|
|
266
|
+
if (key === RAW) return target;
|
|
267
|
+
if (key === IS_REACTIVE) return !readonly2;
|
|
268
|
+
if (key === IS_READONLY) return readonly2;
|
|
269
|
+
if (key === MARK_RAW) return target[MARK_RAW];
|
|
270
|
+
const isArray = Array.isArray(target);
|
|
271
|
+
if (!readonly2 && isArray && hasOwn(arrayInstrumentations, key)) {
|
|
272
|
+
return Reflect.get(arrayInstrumentations, key, receiver);
|
|
273
|
+
}
|
|
274
|
+
const res = Reflect.get(target, key, receiver);
|
|
275
|
+
if (typeof key === "symbol" || key === "__proto__") return res;
|
|
276
|
+
if (!readonly2) track(target, key);
|
|
277
|
+
if (shallow) return res;
|
|
278
|
+
if (isRef(res)) return isArray ? res : res.value;
|
|
279
|
+
return res !== null && typeof res === "object" && !res[MARK_RAW] ? readonly2 ? readonlyProxy(res) : reactive(res) : res;
|
|
280
|
+
},
|
|
281
|
+
set(target, key, value, receiver) {
|
|
282
|
+
if (readonly2) {
|
|
283
|
+
console.warn(`[fnetro] Cannot set "${String(key)}" on readonly object`);
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
const oldVal = target[key];
|
|
287
|
+
const result = Reflect.set(target, key, value, receiver);
|
|
288
|
+
if (hasChanged(value, oldVal)) trigger(target, key, value, oldVal);
|
|
289
|
+
return result;
|
|
290
|
+
},
|
|
291
|
+
deleteProperty(target, key) {
|
|
292
|
+
if (readonly2) return true;
|
|
293
|
+
const hadKey = hasOwn(target, key);
|
|
294
|
+
const result = Reflect.deleteProperty(target, key);
|
|
295
|
+
if (hadKey && result) trigger(target, key);
|
|
296
|
+
return result;
|
|
297
|
+
},
|
|
298
|
+
has(target, key) {
|
|
299
|
+
const res = Reflect.has(target, key);
|
|
300
|
+
track(target, key);
|
|
301
|
+
return res;
|
|
302
|
+
},
|
|
303
|
+
ownKeys(target) {
|
|
304
|
+
track(target, Array.isArray(target) ? "length" : "__iterate__");
|
|
305
|
+
return Reflect.ownKeys(target);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function reactive(target) {
|
|
310
|
+
if (isReadonly(target)) return target;
|
|
311
|
+
if (target[MARK_RAW]) return target;
|
|
312
|
+
if (reactiveMap.has(target)) return reactiveMap.get(target);
|
|
313
|
+
const proxy = new Proxy(target, createHandler());
|
|
314
|
+
reactiveMap.set(target, proxy);
|
|
315
|
+
return proxy;
|
|
316
|
+
}
|
|
317
|
+
function shallowReactive(target) {
|
|
318
|
+
if (shallowReactiveMap.has(target)) return shallowReactiveMap.get(target);
|
|
319
|
+
const proxy = new Proxy(target, createHandler(true));
|
|
320
|
+
shallowReactiveMap.set(target, proxy);
|
|
321
|
+
return proxy;
|
|
322
|
+
}
|
|
323
|
+
function readonlyProxy(target) {
|
|
324
|
+
if (readonlyMap.has(target)) return readonlyMap.get(target);
|
|
325
|
+
const proxy = new Proxy(target, createHandler(false, true));
|
|
326
|
+
readonlyMap.set(target, proxy);
|
|
327
|
+
return proxy;
|
|
328
|
+
}
|
|
329
|
+
function readonly(target) {
|
|
330
|
+
return readonlyProxy(target);
|
|
331
|
+
}
|
|
332
|
+
function markRaw(value) {
|
|
333
|
+
;
|
|
334
|
+
value[MARK_RAW] = true;
|
|
335
|
+
return value;
|
|
336
|
+
}
|
|
337
|
+
function toRaw(observed) {
|
|
338
|
+
const raw = observed?.[RAW];
|
|
339
|
+
return raw ? toRaw(raw) : observed;
|
|
340
|
+
}
|
|
341
|
+
function isReactive(value) {
|
|
342
|
+
if (isReadonly(value)) return isReactive(value[RAW]);
|
|
343
|
+
return !!(value && value[IS_REACTIVE]);
|
|
344
|
+
}
|
|
345
|
+
function isReadonly(value) {
|
|
346
|
+
return !!(value && value[IS_READONLY]);
|
|
347
|
+
}
|
|
348
|
+
function traverse(value, seen = /* @__PURE__ */ new Set()) {
|
|
349
|
+
if (!value || typeof value !== "object" || seen.has(value)) return value;
|
|
350
|
+
seen.add(value);
|
|
351
|
+
if (isRef(value)) {
|
|
352
|
+
traverse(value.value, seen);
|
|
353
|
+
return value;
|
|
354
|
+
}
|
|
355
|
+
if (Array.isArray(value)) {
|
|
356
|
+
value.forEach((v) => traverse(v, seen));
|
|
357
|
+
return value;
|
|
358
|
+
}
|
|
359
|
+
for (const key in value) traverse(value[key], seen);
|
|
360
|
+
return value;
|
|
361
|
+
}
|
|
362
|
+
function normalizeSource(src) {
|
|
363
|
+
if (Array.isArray(src)) return () => src.map((s) => isRef(s) ? s.value : s());
|
|
364
|
+
if (isRef(src)) return () => src.value;
|
|
365
|
+
return src;
|
|
366
|
+
}
|
|
367
|
+
function watch(source, cb, opts = {}) {
|
|
368
|
+
const getter = opts.deep ? () => traverse(normalizeSource(source)()) : normalizeSource(source);
|
|
369
|
+
let oldVal = void 0;
|
|
370
|
+
let cleanupFn;
|
|
371
|
+
const cleanup = (fn) => {
|
|
372
|
+
cleanupFn = fn;
|
|
373
|
+
};
|
|
374
|
+
const job = () => {
|
|
375
|
+
if (!effect2.active) return;
|
|
376
|
+
cleanupFn?.();
|
|
377
|
+
cleanupFn = void 0;
|
|
378
|
+
const newVal = effect2.run();
|
|
379
|
+
if (opts.deep || hasChanged(newVal, oldVal)) {
|
|
380
|
+
cb(newVal, oldVal, cleanup);
|
|
381
|
+
oldVal = newVal;
|
|
382
|
+
}
|
|
383
|
+
if (opts.once) effect2.stop();
|
|
384
|
+
};
|
|
385
|
+
const effect2 = new ReactiveEffect(getter, job, activeScope);
|
|
386
|
+
if (opts.immediate) {
|
|
387
|
+
cleanupFn?.();
|
|
388
|
+
cleanupFn = void 0;
|
|
389
|
+
const val = effect2.run();
|
|
390
|
+
cb(val, oldVal, cleanup);
|
|
391
|
+
oldVal = val;
|
|
392
|
+
} else {
|
|
393
|
+
oldVal = effect2.run();
|
|
394
|
+
}
|
|
395
|
+
return () => effect2.stop();
|
|
396
|
+
}
|
|
397
|
+
var __hooks = {
|
|
398
|
+
useValue: (r) => isRef(r) ? r.value : r(),
|
|
399
|
+
useLocalRef: (init) => ref(init),
|
|
400
|
+
useLocalReactive: (init) => reactive(init)
|
|
401
|
+
};
|
|
402
|
+
function use(source) {
|
|
403
|
+
return __hooks.useValue(source);
|
|
404
|
+
}
|
|
405
|
+
function useLocalRef(init) {
|
|
406
|
+
return __hooks.useLocalRef(init);
|
|
407
|
+
}
|
|
408
|
+
function useLocalReactive(init) {
|
|
409
|
+
return __hooks.useLocalReactive(init);
|
|
410
|
+
}
|
|
411
|
+
function definePage(def) {
|
|
412
|
+
return { __type: "page", ...def };
|
|
413
|
+
}
|
|
414
|
+
function defineGroup(def) {
|
|
415
|
+
return { __type: "group", ...def };
|
|
416
|
+
}
|
|
417
|
+
function defineLayout(Component) {
|
|
418
|
+
return { __type: "layout", Component };
|
|
419
|
+
}
|
|
420
|
+
function defineMiddleware(handler) {
|
|
421
|
+
return { __type: "middleware", handler };
|
|
422
|
+
}
|
|
423
|
+
function defineApiRoute(path, register) {
|
|
424
|
+
return { __type: "api", path, register };
|
|
425
|
+
}
|
|
426
|
+
function resolveRoutes(routes, options = {}) {
|
|
427
|
+
const pages = [];
|
|
428
|
+
const apis = [];
|
|
429
|
+
for (const route of routes) {
|
|
430
|
+
if (route.__type === "api") {
|
|
431
|
+
apis.push({ ...route, path: (options.prefix ?? "") + route.path });
|
|
432
|
+
} else if (route.__type === "group") {
|
|
433
|
+
const prefix = (options.prefix ?? "") + route.prefix;
|
|
434
|
+
const mw = [...options.middleware ?? [], ...route.middleware ?? []];
|
|
435
|
+
const layout = route.layout !== void 0 ? route.layout : options.layout;
|
|
436
|
+
const sub = resolveRoutes(route.routes, { prefix, middleware: mw, layout });
|
|
437
|
+
pages.push(...sub.pages);
|
|
438
|
+
apis.push(...sub.apis);
|
|
439
|
+
} else {
|
|
440
|
+
const fullPath = (options.prefix ?? "") + route.path;
|
|
441
|
+
const layout = route.layout !== void 0 ? route.layout : options.layout;
|
|
442
|
+
const middleware = [...options.middleware ?? [], ...route.middleware ?? []];
|
|
443
|
+
pages.push({ fullPath, page: route, layout, middleware });
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return { pages, apis };
|
|
447
|
+
}
|
|
448
|
+
var SPA_HEADER = "x-fnetro-spa";
|
|
449
|
+
var STATE_KEY = "__FNETRO_STATE__";
|
|
450
|
+
var PARAMS_KEY = "__FNETRO_PARAMS__";
|
|
451
|
+
function hasChanged(a, b) {
|
|
452
|
+
return !Object.is(a, b);
|
|
453
|
+
}
|
|
454
|
+
function hasOwn(obj, key) {
|
|
455
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// server.ts
|
|
459
|
+
function compilePath(path) {
|
|
460
|
+
const keys = [];
|
|
461
|
+
const src = path.replace(/\[\.\.\.([^\]]+)\]/g, (_, k) => {
|
|
462
|
+
keys.push(k);
|
|
463
|
+
return "(.*)";
|
|
464
|
+
}).replace(/\[([^\]]+)\]/g, (_, k) => {
|
|
465
|
+
keys.push(k);
|
|
466
|
+
return "([^/]+)";
|
|
467
|
+
}).replace(/\*/g, "(.*)");
|
|
468
|
+
return { re: new RegExp(`^${src}$`), keys, original: path };
|
|
469
|
+
}
|
|
470
|
+
function matchPath(compiled, pathname) {
|
|
471
|
+
const m = pathname.match(compiled.re);
|
|
472
|
+
if (!m) return null;
|
|
473
|
+
const params = {};
|
|
474
|
+
compiled.keys.forEach((k, i) => {
|
|
475
|
+
params[k] = decodeURIComponent(m[i + 1]);
|
|
476
|
+
});
|
|
477
|
+
return params;
|
|
478
|
+
}
|
|
479
|
+
function buildShell(opts) {
|
|
480
|
+
return `<!DOCTYPE html>
|
|
481
|
+
<html lang="en">
|
|
482
|
+
<head>
|
|
483
|
+
<meta charset="UTF-8">
|
|
484
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
485
|
+
<title>${escHtml(opts.title)}</title>
|
|
486
|
+
<link rel="stylesheet" href="/assets/style.css">
|
|
487
|
+
</head>
|
|
488
|
+
<body>
|
|
489
|
+
<div id="fnetro-app">${opts.pageHtml}</div>
|
|
490
|
+
<script>window.${STATE_KEY}=${opts.stateJson};window.${PARAMS_KEY}=${opts.paramsJson};</script>
|
|
491
|
+
<script type="module" src="/assets/client.js"></script>
|
|
492
|
+
</body>
|
|
493
|
+
</html>`;
|
|
494
|
+
}
|
|
495
|
+
function escHtml(s) {
|
|
496
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
497
|
+
}
|
|
498
|
+
async function renderInner(route, data, url, params, appLayout) {
|
|
499
|
+
const pageNode = jsx(route.page.Page, { ...data, url, params });
|
|
500
|
+
const layout = route.layout !== void 0 ? route.layout : appLayout;
|
|
501
|
+
const wrapped = layout ? jsx(layout.Component, { url, params, children: pageNode }) : pageNode;
|
|
502
|
+
return renderToString(wrapped);
|
|
503
|
+
}
|
|
504
|
+
async function renderFullPage(route, data, url, params, appLayout, title = "FNetro") {
|
|
505
|
+
const pageHtml = await renderInner(route, data, url, params, appLayout);
|
|
506
|
+
return buildShell({
|
|
507
|
+
title,
|
|
508
|
+
stateJson: JSON.stringify({ [url]: data }),
|
|
509
|
+
paramsJson: JSON.stringify(params),
|
|
510
|
+
pageHtml
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
function createFNetro(config) {
|
|
514
|
+
const app = new Hono();
|
|
515
|
+
app.use("/assets/*", async (c, next) => {
|
|
516
|
+
await next();
|
|
517
|
+
});
|
|
518
|
+
(config.middleware ?? []).forEach((mw) => app.use("*", mw));
|
|
519
|
+
const { pages, apis } = resolveRoutes(config.routes, {
|
|
520
|
+
layout: config.layout,
|
|
521
|
+
middleware: []
|
|
522
|
+
});
|
|
523
|
+
const compiled = pages.map((r) => ({
|
|
524
|
+
route: r,
|
|
525
|
+
compiled: compilePath(r.fullPath)
|
|
526
|
+
}));
|
|
527
|
+
apis.forEach((api) => {
|
|
528
|
+
const sub = new Hono();
|
|
529
|
+
api.register(sub, config.middleware ?? []);
|
|
530
|
+
app.route(api.path, sub);
|
|
531
|
+
});
|
|
532
|
+
app.all("*", async (c) => {
|
|
533
|
+
const url = new URL(c.req.url);
|
|
534
|
+
const pathname = url.pathname;
|
|
535
|
+
const isSPA = c.req.header(SPA_HEADER) === "1";
|
|
536
|
+
let matched = null;
|
|
537
|
+
for (const { route: route2, compiled: cp } of compiled) {
|
|
538
|
+
const params2 = matchPath(cp, pathname);
|
|
539
|
+
if (params2 !== null) {
|
|
540
|
+
matched = { route: route2, params: params2 };
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (!matched) {
|
|
545
|
+
if (config.notFound) {
|
|
546
|
+
const html = await renderToString(jsx(config.notFound, {}));
|
|
547
|
+
return c.html(`<!DOCTYPE html><html><body>${html}</body></html>`, 404);
|
|
548
|
+
}
|
|
549
|
+
return c.text("Not Found", 404);
|
|
550
|
+
}
|
|
551
|
+
const { route, params } = matched;
|
|
552
|
+
const origParam = c.req.param.bind(c.req);
|
|
553
|
+
c.req.param = (key) => key ? params[key] ?? origParam(key) : { ...params, ...origParam() };
|
|
554
|
+
let earlyResponse;
|
|
555
|
+
const handlers = [...route.middleware];
|
|
556
|
+
let idx = 0;
|
|
557
|
+
const runMiddleware = async () => {
|
|
558
|
+
const mw = handlers[idx++];
|
|
559
|
+
if (!mw) return;
|
|
560
|
+
const res = await mw(c, runMiddleware);
|
|
561
|
+
if (res instanceof Response && !earlyResponse) earlyResponse = res;
|
|
562
|
+
};
|
|
563
|
+
await runMiddleware();
|
|
564
|
+
if (earlyResponse) return earlyResponse;
|
|
565
|
+
const data = route.page.loader ? await route.page.loader(c) : {};
|
|
566
|
+
const safeData = data ?? {};
|
|
567
|
+
if (isSPA) {
|
|
568
|
+
const html = await renderInner(route, safeData, pathname, params, config.layout);
|
|
569
|
+
return c.json({
|
|
570
|
+
html,
|
|
571
|
+
state: safeData,
|
|
572
|
+
params,
|
|
573
|
+
url: pathname
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
const fullHtml = await renderFullPage(route, safeData, pathname, params, config.layout);
|
|
577
|
+
return c.html(fullHtml);
|
|
578
|
+
});
|
|
579
|
+
return { app, handler: app.fetch };
|
|
580
|
+
}
|
|
581
|
+
function detectRuntime() {
|
|
582
|
+
if (typeof globalThis.Bun !== "undefined") return "bun";
|
|
583
|
+
if (typeof globalThis.Deno !== "undefined") return "deno";
|
|
584
|
+
if (typeof process !== "undefined" && process.versions?.node) return "node";
|
|
585
|
+
return "edge";
|
|
586
|
+
}
|
|
587
|
+
async function serve(opts) {
|
|
588
|
+
const runtime = opts.runtime ?? detectRuntime();
|
|
589
|
+
const port = opts.port ?? Number(globalThis.process?.env?.PORT ?? 3e3);
|
|
590
|
+
const hostname = opts.hostname ?? "0.0.0.0";
|
|
591
|
+
const staticDir = opts.staticDir ?? "./dist";
|
|
592
|
+
const addr = `http://${hostname === "0.0.0.0" ? "localhost" : hostname}:${port}`;
|
|
593
|
+
const logReady = () => console.log(`
|
|
594
|
+
\u{1F525} FNetro [${runtime}] ready \u2192 ${addr}
|
|
595
|
+
`);
|
|
596
|
+
switch (runtime) {
|
|
597
|
+
case "node": {
|
|
598
|
+
const [{ serve: nodeServe }, { serveStatic }] = await Promise.all([
|
|
599
|
+
import("@hono/node-server"),
|
|
600
|
+
import("@hono/node-server/serve-static")
|
|
601
|
+
]);
|
|
602
|
+
opts.app.app.use("/assets/*", serveStatic({ root: staticDir }));
|
|
603
|
+
nodeServe({ fetch: opts.app.handler, port, hostname });
|
|
604
|
+
logReady();
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
607
|
+
case "bun": {
|
|
608
|
+
;
|
|
609
|
+
globalThis.Bun.serve({ fetch: opts.app.handler, port, hostname });
|
|
610
|
+
logReady();
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
case "deno": {
|
|
614
|
+
;
|
|
615
|
+
globalThis.Deno.serve({ port, hostname }, opts.app.handler);
|
|
616
|
+
logReady();
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
default:
|
|
620
|
+
console.warn("[fnetro] serve() is a no-op on edge runtimes. Export `app.handler` instead.");
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
var NODE_BUILTINS = /^node:|^(assert|buffer|child_process|cluster|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|perf_hooks|process|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|trace_events|tty|url|util|v8|vm|worker_threads|zlib)$/;
|
|
624
|
+
function fnetroVitePlugin(opts = {}) {
|
|
625
|
+
const {
|
|
626
|
+
serverEntry = "app/server.ts",
|
|
627
|
+
clientEntry = "app/client.ts",
|
|
628
|
+
serverOutDir = "dist/server",
|
|
629
|
+
clientOutDir = "dist/assets",
|
|
630
|
+
serverExternal = []
|
|
631
|
+
} = opts;
|
|
632
|
+
let isServerBuild = true;
|
|
633
|
+
const sharedEsbuild = {
|
|
634
|
+
jsx: "automatic",
|
|
635
|
+
jsxImportSource: "hono/jsx"
|
|
636
|
+
};
|
|
637
|
+
const jsxPlugin = {
|
|
638
|
+
name: "fnetro:jsx",
|
|
639
|
+
config: () => ({ esbuild: sharedEsbuild })
|
|
640
|
+
};
|
|
641
|
+
const serverPlugin = {
|
|
642
|
+
name: "fnetro:server",
|
|
643
|
+
apply: "build",
|
|
644
|
+
enforce: "pre",
|
|
645
|
+
config() {
|
|
646
|
+
return {
|
|
647
|
+
build: {
|
|
648
|
+
outDir: serverOutDir,
|
|
649
|
+
ssr: true,
|
|
650
|
+
target: "node18",
|
|
651
|
+
lib: {
|
|
652
|
+
entry: serverEntry,
|
|
653
|
+
formats: ["es"],
|
|
654
|
+
fileName: "server"
|
|
655
|
+
},
|
|
656
|
+
rollupOptions: {
|
|
657
|
+
external: (id) => NODE_BUILTINS.test(id) || id === "@hono/node-server" || serverExternal.includes(id)
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
esbuild: sharedEsbuild
|
|
661
|
+
};
|
|
662
|
+
},
|
|
663
|
+
async closeBundle() {
|
|
664
|
+
console.log("\n\u26A1 FNetro: building client bundle\u2026\n");
|
|
665
|
+
const { build } = await import("vite");
|
|
666
|
+
await build({
|
|
667
|
+
configFile: false,
|
|
668
|
+
esbuild: sharedEsbuild,
|
|
669
|
+
build: {
|
|
670
|
+
outDir: clientOutDir,
|
|
671
|
+
lib: {
|
|
672
|
+
entry: clientEntry,
|
|
673
|
+
formats: ["es"],
|
|
674
|
+
fileName: "client"
|
|
675
|
+
},
|
|
676
|
+
rollupOptions: {
|
|
677
|
+
output: { entryFileNames: "[name].js" }
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
console.log("\n\u2705 FNetro: both bundles ready\n");
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
return [jsxPlugin, serverPlugin];
|
|
685
|
+
}
|
|
686
|
+
export {
|
|
687
|
+
SPA_HEADER,
|
|
688
|
+
STATE_KEY,
|
|
689
|
+
computed,
|
|
690
|
+
createFNetro,
|
|
691
|
+
defineApiRoute,
|
|
692
|
+
defineGroup,
|
|
693
|
+
defineLayout,
|
|
694
|
+
defineMiddleware,
|
|
695
|
+
definePage,
|
|
696
|
+
detectRuntime,
|
|
697
|
+
effect,
|
|
698
|
+
effectScope,
|
|
699
|
+
fnetroVitePlugin,
|
|
700
|
+
isReactive,
|
|
701
|
+
isReadonly,
|
|
702
|
+
isRef,
|
|
703
|
+
markRaw,
|
|
704
|
+
reactive,
|
|
705
|
+
readonly,
|
|
706
|
+
ref,
|
|
707
|
+
serve,
|
|
708
|
+
shallowReactive,
|
|
709
|
+
shallowRef,
|
|
710
|
+
toRaw,
|
|
711
|
+
toRef,
|
|
712
|
+
toRefs,
|
|
713
|
+
triggerRef,
|
|
714
|
+
unref,
|
|
715
|
+
use,
|
|
716
|
+
useLocalReactive,
|
|
717
|
+
useLocalRef,
|
|
718
|
+
watch,
|
|
719
|
+
watchEffect
|
|
720
|
+
};
|