@netrojs/fnetro 0.1.5 → 0.2.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/dist/server.js CHANGED
@@ -1,413 +1,9 @@
1
1
  // server.ts
2
2
  import { Hono } from "hono";
3
- import { jsx } from "hono/jsx";
4
- import { renderToString } from "hono/jsx/dom/server";
3
+ import { createComponent } from "solid-js";
4
+ import { renderToStringAsync, generateHydrationScript } from "solid-js/web";
5
5
 
6
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
7
  function definePage(def) {
412
8
  return { __type: "page", ...def };
413
9
  }
@@ -417,9 +13,6 @@ function defineGroup(def) {
417
13
  function defineLayout(Component) {
418
14
  return { __type: "layout", Component };
419
15
  }
420
- function defineMiddleware(handler) {
421
- return { __type: "middleware", handler };
422
- }
423
16
  function defineApiRoute(path, register) {
424
17
  return { __type: "api", path, register };
425
18
  }
@@ -437,25 +30,16 @@ function resolveRoutes(routes, options = {}) {
437
30
  pages.push(...sub.pages);
438
31
  apis.push(...sub.apis);
439
32
  } 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 });
33
+ pages.push({
34
+ fullPath: (options.prefix ?? "") + route.path,
35
+ page: route,
36
+ layout: route.layout !== void 0 ? route.layout : options.layout,
37
+ middleware: [...options.middleware ?? [], ...route.middleware ?? []]
38
+ });
444
39
  }
445
40
  }
446
41
  return { pages, apis };
447
42
  }
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
43
  function compilePath(path) {
460
44
  const keys = [];
461
45
  const src = path.replace(/\[\.\.\.([^\]]+)\]/g, (_, k) => {
@@ -465,76 +49,183 @@ function compilePath(path) {
465
49
  keys.push(k);
466
50
  return "([^/]+)";
467
51
  }).replace(/\*/g, "(.*)");
468
- return { re: new RegExp(`^${src}$`), keys, original: path };
52
+ return { re: new RegExp(`^${src}$`), keys };
469
53
  }
470
54
  function matchPath(compiled, pathname) {
471
55
  const m = pathname.match(compiled.re);
472
56
  if (!m) return null;
473
57
  const params = {};
474
58
  compiled.keys.forEach((k, i) => {
475
- params[k] = decodeURIComponent(m[i + 1]);
59
+ params[k] = decodeURIComponent(m[i + 1] ?? "");
476
60
  });
477
61
  return params;
478
62
  }
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) {
63
+ var SPA_HEADER = "x-fnetro-spa";
64
+ var STATE_KEY = "__FNETRO_STATE__";
65
+ var PARAMS_KEY = "__FNETRO_PARAMS__";
66
+ var SEO_KEY = "__FNETRO_SEO__";
67
+
68
+ // server.ts
69
+ function esc(s) {
496
70
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
497
71
  }
498
- async function renderInner(route, data, url, params, appLayout) {
499
- const pageNode = jsx(route.page.Page, { ...data, url, params });
72
+ function buildHeadMeta(seo, extraHead = "") {
73
+ const m = (n, v) => v ? `<meta name="${n}" content="${esc(v)}">` : "";
74
+ const p = (pr, v) => v ? `<meta property="${pr}" content="${esc(v)}">` : "";
75
+ const lk = (rel, href) => `<link rel="${rel}" href="${esc(href)}">`;
76
+ const parts = [];
77
+ if (seo.description) parts.push(m("description", seo.description));
78
+ if (seo.keywords) parts.push(m("keywords", seo.keywords));
79
+ if (seo.author) parts.push(m("author", seo.author));
80
+ if (seo.robots) parts.push(m("robots", seo.robots));
81
+ if (seo.themeColor) parts.push(m("theme-color", seo.themeColor));
82
+ if (seo.canonical) parts.push(lk("canonical", seo.canonical));
83
+ if (seo.ogTitle) parts.push(p("og:title", seo.ogTitle));
84
+ if (seo.ogDescription) parts.push(p("og:description", seo.ogDescription));
85
+ if (seo.ogImage) parts.push(p("og:image", seo.ogImage));
86
+ if (seo.ogImageAlt) parts.push(p("og:image:alt", seo.ogImageAlt));
87
+ if (seo.ogImageWidth) parts.push(p("og:image:width", seo.ogImageWidth));
88
+ if (seo.ogImageHeight) parts.push(p("og:image:height", seo.ogImageHeight));
89
+ if (seo.ogUrl) parts.push(p("og:url", seo.ogUrl));
90
+ if (seo.ogType) parts.push(p("og:type", seo.ogType));
91
+ if (seo.ogSiteName) parts.push(p("og:site_name", seo.ogSiteName));
92
+ if (seo.ogLocale) parts.push(p("og:locale", seo.ogLocale));
93
+ if (seo.twitterCard) parts.push(m("twitter:card", seo.twitterCard));
94
+ if (seo.twitterSite) parts.push(m("twitter:site", seo.twitterSite));
95
+ if (seo.twitterCreator) parts.push(m("twitter:creator", seo.twitterCreator));
96
+ if (seo.twitterTitle) parts.push(m("twitter:title", seo.twitterTitle));
97
+ if (seo.twitterDescription) parts.push(m("twitter:description", seo.twitterDescription));
98
+ if (seo.twitterImage) parts.push(m("twitter:image", seo.twitterImage));
99
+ if (seo.twitterImageAlt) parts.push(m("twitter:image:alt", seo.twitterImageAlt));
100
+ for (const tag of seo.extra ?? []) {
101
+ const attrs = [
102
+ tag.name ? `name="${esc(tag.name)}"` : "",
103
+ tag.property ? `property="${esc(tag.property)}"` : "",
104
+ tag.httpEquiv ? `http-equiv="${esc(tag.httpEquiv)}"` : "",
105
+ `content="${esc(tag.content)}"`
106
+ ].filter(Boolean).join(" ");
107
+ parts.push(`<meta ${attrs}>`);
108
+ }
109
+ const ld = seo.jsonLd;
110
+ if (ld) {
111
+ const schemas = Array.isArray(ld) ? ld : [ld];
112
+ for (const schema of schemas) {
113
+ parts.push(`<script type="application/ld+json">${JSON.stringify(schema)}</script>`);
114
+ }
115
+ }
116
+ if (extraHead) parts.push(extraHead);
117
+ return parts.join("\n");
118
+ }
119
+ function mergeSEO(base, override) {
120
+ return { ...base ?? {}, ...override ?? {} };
121
+ }
122
+ var _assets = null;
123
+ async function resolveAssets(cfg, defaultEntry) {
124
+ if (_assets) return _assets;
125
+ if (cfg.manifestDir) {
126
+ try {
127
+ const [{ readFileSync }, { join }] = await Promise.all([
128
+ import("fs"),
129
+ import("path")
130
+ ]);
131
+ const raw = readFileSync(join(cfg.manifestDir, "manifest.json"), "utf-8");
132
+ const manifest = JSON.parse(raw);
133
+ const entryKey = cfg.manifestEntry ?? Object.keys(manifest).find((k) => k.endsWith(defaultEntry)) ?? defaultEntry;
134
+ const entry = manifest[entryKey];
135
+ if (entry) {
136
+ _assets = {
137
+ scripts: [`/assets/${entry.file}`],
138
+ styles: (entry.css ?? []).map((f) => `/assets/${f}`)
139
+ };
140
+ return _assets;
141
+ }
142
+ } catch {
143
+ }
144
+ }
145
+ _assets = {
146
+ scripts: cfg.scripts ?? ["/assets/client.js"],
147
+ styles: cfg.styles ?? []
148
+ };
149
+ return _assets;
150
+ }
151
+ function buildShell(o) {
152
+ const htmlAttrStr = Object.entries(o.htmlAttrs ?? { lang: "en" }).map(([k, v]) => `${k}="${esc(v)}"`).join(" ");
153
+ const styleLinks = o.styles.map((href) => `<link rel="stylesheet" href="${esc(href)}">`).join("\n");
154
+ const scriptTags = o.scripts.map((src) => `<script type="module" src="${esc(src)}"></script>`).join("\n");
155
+ return [
156
+ "<!DOCTYPE html>",
157
+ `<html ${htmlAttrStr}>`,
158
+ "<head>",
159
+ '<meta charset="UTF-8">',
160
+ '<meta name="viewport" content="width=device-width,initial-scale=1">',
161
+ `<title>${esc(o.title)}</title>`,
162
+ o.metaHtml,
163
+ generateHydrationScript(),
164
+ styleLinks,
165
+ "</head>",
166
+ "<body>",
167
+ `<div id="fnetro-app">${o.bodyHtml}</div>`,
168
+ "<script>",
169
+ `window.${STATE_KEY}=${o.stateJson};`,
170
+ `window.${PARAMS_KEY}=${o.paramsJson};`,
171
+ `window.${SEO_KEY}=${o.seoJson};`,
172
+ "</script>",
173
+ scriptTags,
174
+ "</body>",
175
+ "</html>"
176
+ ].filter(Boolean).join("\n");
177
+ }
178
+ async function renderPage(route, data, url, params, appLayout) {
500
179
  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);
180
+ return renderToStringAsync(() => {
181
+ const pageEl = createComponent(route.page.Page, { ...data, url, params });
182
+ if (!layout) return pageEl;
183
+ return createComponent(layout.Component, {
184
+ url,
185
+ params,
186
+ get children() {
187
+ return pageEl;
188
+ }
189
+ });
190
+ });
503
191
  }
504
- async function renderFullPage(route, data, url, params, appLayout, title = "FNetro") {
505
- const pageHtml = await renderInner(route, data, url, params, appLayout);
192
+ async function renderFullPage(route, data, url, params, config, assets) {
193
+ const pageSEO = typeof route.page.seo === "function" ? route.page.seo(data, params) : route.page.seo;
194
+ const seo = mergeSEO(config.seo, pageSEO);
195
+ const title = seo.title ?? "FNetro";
196
+ const bodyHtml = await renderPage(route, data, url, params, config.layout);
506
197
  return buildShell({
507
198
  title,
199
+ metaHtml: buildHeadMeta(seo, config.head),
200
+ bodyHtml,
508
201
  stateJson: JSON.stringify({ [url]: data }),
509
202
  paramsJson: JSON.stringify(params),
510
- pageHtml
203
+ seoJson: JSON.stringify(seo),
204
+ scripts: assets.scripts,
205
+ styles: assets.styles,
206
+ htmlAttrs: config.htmlAttrs
511
207
  });
512
208
  }
513
209
  function createFNetro(config) {
514
210
  const app = new Hono();
515
- app.use("/assets/*", async (c, next) => {
516
- await next();
517
- });
518
- (config.middleware ?? []).forEach((mw) => app.use("*", mw));
211
+ for (const mw of config.middleware ?? []) app.use("*", mw);
519
212
  const { pages, apis } = resolveRoutes(config.routes, {
520
213
  layout: config.layout,
521
214
  middleware: []
522
215
  });
523
- const compiled = pages.map((r) => ({
524
- route: r,
525
- compiled: compilePath(r.fullPath)
526
- }));
527
- apis.forEach((api) => {
216
+ const compiled = pages.map((r) => ({ route: r, cp: compilePath(r.fullPath) }));
217
+ for (const api of apis) {
528
218
  const sub = new Hono();
529
219
  api.register(sub, config.middleware ?? []);
530
220
  app.route(api.path, sub);
531
- });
221
+ }
532
222
  app.all("*", async (c) => {
533
223
  const url = new URL(c.req.url);
534
224
  const pathname = url.pathname;
535
225
  const isSPA = c.req.header(SPA_HEADER) === "1";
226
+ const isDev = process.env["NODE_ENV"] !== "production";
536
227
  let matched = null;
537
- for (const { route: route2, compiled: cp } of compiled) {
228
+ for (const { route: route2, cp } of compiled) {
538
229
  const params2 = matchPath(cp, pathname);
539
230
  if (params2 !== null) {
540
231
  matched = { route: route2, params: params2 };
@@ -543,55 +234,64 @@ function createFNetro(config) {
543
234
  }
544
235
  if (!matched) {
545
236
  if (config.notFound) {
546
- const html = await renderToString(jsx(config.notFound, {}));
547
- return c.html(`<!DOCTYPE html><html><body>${html}</body></html>`, 404);
237
+ const html2 = await renderToStringAsync(
238
+ () => createComponent(config.notFound, {})
239
+ );
240
+ return c.html(
241
+ `<!DOCTYPE html><html lang="en"><body>${html2}</body></html>`,
242
+ 404
243
+ );
548
244
  }
549
245
  return c.text("Not Found", 404);
550
246
  }
551
247
  const { route, params } = matched;
552
248
  const origParam = c.req.param.bind(c.req);
553
- c.req.param = (key) => key ? params[key] ?? origParam(key) : { ...params, ...origParam() };
554
- let earlyResponse;
249
+ c.req["param"] = (key) => key != null ? params[key] ?? origParam(key) : { ...origParam(), ...params };
250
+ let early;
555
251
  const handlers = [...route.middleware];
556
252
  let idx = 0;
557
- const runMiddleware = async () => {
253
+ const runNext = async () => {
558
254
  const mw = handlers[idx++];
559
255
  if (!mw) return;
560
- const res = await mw(c, runMiddleware);
561
- if (res instanceof Response && !earlyResponse) earlyResponse = res;
256
+ const res = await mw(c, runNext);
257
+ if (res instanceof Response && !early) early = res;
562
258
  };
563
- await runMiddleware();
564
- if (earlyResponse) return earlyResponse;
565
- const data = route.page.loader ? await route.page.loader(c) : {};
566
- const safeData = data ?? {};
259
+ await runNext();
260
+ if (early) return early;
261
+ const rawData = route.page.loader ? await route.page.loader(c) : {};
262
+ const data = rawData ?? {};
567
263
  if (isSPA) {
568
- const html = await renderInner(route, safeData, pathname, params, config.layout);
264
+ const pageSEO = typeof route.page.seo === "function" ? route.page.seo(data, params) : route.page.seo;
569
265
  return c.json({
570
- html,
571
- state: safeData,
266
+ state: data,
572
267
  params,
573
- url: pathname
268
+ url: pathname,
269
+ seo: mergeSEO(config.seo, pageSEO)
574
270
  });
575
271
  }
576
- const fullHtml = await renderFullPage(route, safeData, pathname, params, config.layout);
577
- return c.html(fullHtml);
272
+ const assets = isDev ? { scripts: [], styles: [] } : await resolveAssets(
273
+ config.assets ?? {},
274
+ config.assets?.manifestEntry ?? "client.ts"
275
+ );
276
+ const html = await renderFullPage(route, data, pathname, params, config, assets);
277
+ return c.html(html);
578
278
  });
579
- return { app, handler: app.fetch };
279
+ return { app, handler: app.fetch.bind(app) };
580
280
  }
581
281
  function detectRuntime() {
582
- if (typeof globalThis.Bun !== "undefined") return "bun";
583
- if (typeof globalThis.Deno !== "undefined") return "deno";
282
+ if (typeof globalThis["Bun"] !== "undefined") return "bun";
283
+ if (typeof globalThis["Deno"] !== "undefined") return "deno";
584
284
  if (typeof process !== "undefined" && process.versions?.node) return "node";
585
285
  return "edge";
586
286
  }
587
287
  async function serve(opts) {
588
288
  const runtime = opts.runtime ?? detectRuntime();
589
- const port = opts.port ?? Number(globalThis.process?.env?.PORT ?? 3e3);
289
+ const port = opts.port ?? Number(process?.env?.["PORT"] ?? 3e3);
590
290
  const hostname = opts.hostname ?? "0.0.0.0";
591
291
  const staticDir = opts.staticDir ?? "./dist";
592
- const addr = `http://${hostname === "0.0.0.0" ? "localhost" : hostname}:${port}`;
292
+ const displayHost = hostname === "0.0.0.0" ? "localhost" : hostname;
593
293
  const logReady = () => console.log(`
594
- \u{1F525} FNetro [${runtime}] ready \u2192 ${addr}
294
+ \u{1F525} FNetro [${runtime}] ready \u2192 http://${displayHost}:${port}
595
295
  `);
596
296
  switch (runtime) {
597
297
  case "node": {
@@ -600,134 +300,159 @@ async function serve(opts) {
600
300
  import("@hono/node-server/serve-static")
601
301
  ]);
602
302
  opts.app.app.use("/assets/*", serveStatic({ root: staticDir }));
303
+ opts.app.app.use("/*", serveStatic({ root: "./public" }));
603
304
  nodeServe({ fetch: opts.app.handler, port, hostname });
604
305
  logReady();
605
306
  break;
606
307
  }
607
308
  case "bun": {
608
309
  ;
609
- globalThis.Bun.serve({ fetch: opts.app.handler, port, hostname });
310
+ globalThis["Bun"].serve({ fetch: opts.app.handler, port, hostname });
610
311
  logReady();
611
312
  break;
612
313
  }
613
314
  case "deno": {
614
315
  ;
615
- globalThis.Deno.serve({ port, hostname }, opts.app.handler);
316
+ globalThis["Deno"].serve({ port, hostname }, opts.app.handler);
616
317
  logReady();
617
318
  break;
618
319
  }
619
320
  default:
620
- console.warn("[fnetro] serve() is a no-op on edge runtimes. Export `app.handler` instead.");
321
+ console.warn(
322
+ "[fnetro] serve() is a no-op on edge runtimes \u2014 export `fnetro.handler` instead."
323
+ );
621
324
  }
622
325
  }
623
326
  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)$/;
327
+ async function loadSolid() {
328
+ try {
329
+ const mod = await import("vite-plugin-solid");
330
+ return mod.default ?? mod;
331
+ } catch {
332
+ throw new Error(
333
+ "[fnetro] vite-plugin-solid is required.\n Install it: npm i -D vite-plugin-solid"
334
+ );
335
+ }
336
+ }
337
+ function toPlugins(v) {
338
+ return Array.isArray(v) ? v : [v];
339
+ }
624
340
  function fnetroVitePlugin(opts = {}) {
625
341
  const {
626
- serverEntry = "app/server.ts",
627
- clientEntry = "app/client.ts",
342
+ serverEntry = "server.ts",
343
+ clientEntry = "client.ts",
628
344
  serverOutDir = "dist/server",
629
345
  clientOutDir = "dist/assets",
630
- serverExternal = []
346
+ serverExternal = [],
347
+ solidOptions = {}
631
348
  } = opts;
632
- let isServerBuild = true;
633
- const jsxTransform = {
634
- jsx: "automatic",
635
- jsxImportSource: "hono/jsx"
636
- };
349
+ let _solid = null;
350
+ let _solidPlugins = [];
637
351
  const jsxPlugin = {
638
352
  name: "fnetro:jsx",
639
- config: () => ({
640
- // Vite ≤7 (esbuild transform)
641
- esbuild: jsxTransform,
642
- // Vite 8+ (oxc / rolldown transform) — jsx property uses JsxOptions object shape
643
- oxc: {
644
- jsx: {
645
- runtime: "automatic",
646
- importSource: "hono/jsx"
353
+ enforce: "pre",
354
+ // Sync config hook — must return Omit<UserConfig, 'plugins'> | null
355
+ config(_cfg, _env) {
356
+ return {
357
+ esbuild: {
358
+ jsx: "automatic",
359
+ jsxImportSource: "solid-js"
647
360
  }
361
+ };
362
+ },
363
+ async buildStart() {
364
+ if (!_solid) {
365
+ _solid = await loadSolid();
366
+ _solidPlugins = toPlugins(_solid({ ssr: true, ...solidOptions }));
648
367
  }
649
- // `as any` because JsxOptions typing varies across Vite 8 patch releases
650
- })
368
+ }
369
+ };
370
+ const solidProxy = {
371
+ name: "fnetro:solid-proxy",
372
+ enforce: "pre",
373
+ async transform(code, id, options) {
374
+ if (!_solidPlugins[0]?.transform) return null;
375
+ const hook = _solidPlugins[0].transform;
376
+ const fn = typeof hook === "function" ? hook : hook.handler;
377
+ if (!fn) return null;
378
+ return fn.call(this, code, id, options);
379
+ },
380
+ async resolveId(id) {
381
+ if (!_solidPlugins[0]?.resolveId) return null;
382
+ const hook = _solidPlugins[0].resolveId;
383
+ const fn = typeof hook === "function" ? hook : hook.handler;
384
+ if (!fn) return null;
385
+ return fn.call(this, id, void 0, {});
386
+ },
387
+ async load(id) {
388
+ if (!_solidPlugins[0]?.load) return null;
389
+ const hook = _solidPlugins[0].load;
390
+ const fn = typeof hook === "function" ? hook : hook.handler;
391
+ if (!fn) return null;
392
+ return fn.call(this, id, {});
393
+ }
651
394
  };
652
- const serverPlugin = {
653
- name: "fnetro:server",
395
+ const buildPlugin = {
396
+ name: "fnetro:build",
654
397
  apply: "build",
655
398
  enforce: "pre",
656
- config() {
399
+ // Sync config hook — Omit<UserConfig, 'plugins'> satisfies the ObjectHook constraint
400
+ config(_cfg, _env) {
657
401
  return {
658
402
  build: {
403
+ ssr: serverEntry,
659
404
  outDir: serverOutDir,
660
- ssr: true,
661
- target: "node18",
662
- lib: {
663
- entry: serverEntry,
664
- formats: ["es"],
665
- fileName: "server"
666
- },
667
405
  rollupOptions: {
668
- external: (id) => NODE_BUILTINS.test(id) || id === "@hono/node-server" || serverExternal.includes(id)
406
+ input: serverEntry,
407
+ output: {
408
+ format: "es",
409
+ entryFileNames: "server.js"
410
+ },
411
+ external: (id) => NODE_BUILTINS.test(id) || id === "@hono/node-server" || id === "@hono/node-server/serve-static" || serverExternal.includes(id)
669
412
  }
670
- },
671
- // Vite ≤7 fallback (oxc is set by jsxPlugin for Vite 8+)
672
- esbuild: jsxTransform
413
+ }
673
414
  };
674
415
  },
675
416
  async closeBundle() {
676
417
  console.log("\n\u26A1 FNetro: building client bundle\u2026\n");
418
+ const solid = _solid ?? await loadSolid();
677
419
  const { build } = await import("vite");
678
420
  await build({
679
421
  configFile: false,
680
- // Vite ≤7 fallback (oxc is set by jsxPlugin for Vite 8+)
681
- esbuild: jsxTransform,
422
+ plugins: toPlugins(solid({ ...solidOptions })),
682
423
  build: {
683
424
  outDir: clientOutDir,
684
- lib: {
685
- entry: clientEntry,
686
- formats: ["es"],
687
- fileName: "client"
688
- },
425
+ manifest: true,
689
426
  rollupOptions: {
690
- output: { entryFileNames: "[name].js" }
427
+ input: clientEntry,
428
+ output: {
429
+ format: "es",
430
+ entryFileNames: "[name]-[hash].js",
431
+ chunkFileNames: "[name]-[hash].js",
432
+ assetFileNames: "[name]-[hash][extname]"
433
+ }
691
434
  }
692
435
  }
693
436
  });
694
- console.log("\n\u2705 FNetro: both bundles ready\n");
437
+ console.log("\u2705 FNetro: both bundles ready\n");
695
438
  }
696
439
  };
697
- return [jsxPlugin, serverPlugin];
440
+ return [jsxPlugin, solidProxy, buildPlugin];
698
441
  }
699
442
  export {
443
+ PARAMS_KEY,
444
+ SEO_KEY,
700
445
  SPA_HEADER,
701
446
  STATE_KEY,
702
- computed,
447
+ compilePath,
703
448
  createFNetro,
704
449
  defineApiRoute,
705
450
  defineGroup,
706
451
  defineLayout,
707
- defineMiddleware,
708
452
  definePage,
709
453
  detectRuntime,
710
- effect,
711
- effectScope,
712
454
  fnetroVitePlugin,
713
- isReactive,
714
- isReadonly,
715
- isRef,
716
- markRaw,
717
- reactive,
718
- readonly,
719
- ref,
720
- serve,
721
- shallowReactive,
722
- shallowRef,
723
- toRaw,
724
- toRef,
725
- toRefs,
726
- triggerRef,
727
- unref,
728
- use,
729
- useLocalReactive,
730
- useLocalRef,
731
- watch,
732
- watchEffect
455
+ matchPath,
456
+ resolveRoutes,
457
+ serve
733
458
  };