@tinyfx/runtime 0.1.4 → 0.1.8
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 +48 -121
- package/dist/context.d.ts +4 -0
- package/dist/context.js +3 -0
- package/dist/di.d.ts +1 -1
- package/dist/http.d.ts +2 -10
- package/dist/http.js +1 -38
- package/dist/index.d.ts +6 -5
- package/dist/index.js +4 -3
- package/dist/router/active-links.d.ts +13 -0
- package/dist/router/active-links.js +31 -0
- package/dist/router/index.d.ts +24 -0
- package/dist/router/index.js +38 -0
- package/dist/router/lifecycle.d.ts +24 -0
- package/dist/router/lifecycle.js +44 -0
- package/dist/router/navigate.d.ts +12 -0
- package/dist/router/navigate.js +18 -0
- package/dist/router/params.d.ts +19 -0
- package/dist/router/params.js +45 -0
- package/dist/router/types.d.ts +9 -0
- package/dist/router/types.js +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
# @tinyfx/runtime
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
A lightweight collection of helpers for reactivity, DOM manipulation, and typed HTTP communication. Designed to be used standalone or as part of the `tinyfx` build pipeline.
|
|
3
|
+
The TinyFX browser runtime: signals, DOM helpers, typed HTTP, and router lifecycle utilities.
|
|
6
4
|
|
|
7
5
|
## Features
|
|
8
6
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
7
|
+
- Signals (`signal`, `effect`)
|
|
8
|
+
- DOM binding helpers (`bindText`, `bindAttr`, `bindClass`)
|
|
9
|
+
- Typed HTTP client (`createHttp`)
|
|
10
|
+
- Router helpers (`initRouter`, `navigate`, `goBack`, `getParam`)
|
|
11
|
+
- Lifecycle hooks (`onMount`, `onDestroy`)
|
|
12
|
+
- Lightweight `TinyFxContext` (`params`, `navigate`)
|
|
14
13
|
|
|
15
14
|
## Installation
|
|
16
15
|
|
|
@@ -20,27 +19,21 @@ npm install @tinyfx/runtime
|
|
|
20
19
|
|
|
21
20
|
## Usage
|
|
22
21
|
|
|
23
|
-
### Signals
|
|
24
|
-
|
|
25
|
-
Signals provide a simple way to manage state that triggers effects when changed.
|
|
22
|
+
### Signals
|
|
26
23
|
|
|
27
24
|
```ts
|
|
28
25
|
import { signal, effect } from "@tinyfx/runtime";
|
|
29
26
|
|
|
30
27
|
const count = signal(0);
|
|
31
28
|
|
|
32
|
-
// Runs immediately, and re-runs whenever count() changes
|
|
33
29
|
effect(() => {
|
|
34
|
-
console.log("
|
|
30
|
+
console.log("count:", count());
|
|
35
31
|
});
|
|
36
32
|
|
|
37
|
-
count.set(
|
|
38
|
-
count.set(count() + 1); // logs: "The count is: 2"
|
|
33
|
+
count.set(count() + 1);
|
|
39
34
|
```
|
|
40
35
|
|
|
41
|
-
### DOM
|
|
42
|
-
|
|
43
|
-
Keep your JS decoupled from the DOM while maintaining reactive updates.
|
|
36
|
+
### DOM bindings
|
|
44
37
|
|
|
45
38
|
```ts
|
|
46
39
|
import { signal, bindText, bindClass, bindAttr } from "@tinyfx/runtime";
|
|
@@ -51,132 +44,66 @@ const isActive = signal(false);
|
|
|
51
44
|
const el = document.getElementById("counter");
|
|
52
45
|
const btn = document.querySelector("button");
|
|
53
46
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
### Typed HTTP Client
|
|
60
|
-
|
|
61
|
-
A thin wrapper over `fetch` that returns typed responses.
|
|
62
|
-
|
|
63
|
-
```ts
|
|
64
|
-
import { createHttp } from "@tinyfx/runtime";
|
|
65
|
-
|
|
66
|
-
interface User {
|
|
67
|
-
id: number;
|
|
68
|
-
name: string;
|
|
47
|
+
if (el && btn) {
|
|
48
|
+
bindText(el, () => `Count: ${count()}`);
|
|
49
|
+
bindClass(btn, "active", isActive);
|
|
50
|
+
bindAttr(btn, "disabled", () => count() > 10);
|
|
69
51
|
}
|
|
70
|
-
|
|
71
|
-
const api = createHttp({ baseUrl: "/api" });
|
|
72
|
-
|
|
73
|
-
// Fully typed response
|
|
74
|
-
const users = await api.get<User[]>("/users");
|
|
75
|
-
|
|
76
|
-
// POST with data
|
|
77
|
-
await api.post("/users", { name: "Alice" });
|
|
78
52
|
```
|
|
79
53
|
|
|
80
|
-
###
|
|
81
|
-
|
|
82
|
-
TinyFX includes a small dependency injection container to avoid importing singleton instances everywhere.
|
|
83
|
-
|
|
84
|
-
Import it from `@tinyfx/runtime/di`:
|
|
54
|
+
### HTTP client
|
|
85
55
|
|
|
86
56
|
```ts
|
|
87
|
-
import {
|
|
88
|
-
|
|
89
|
-
class Config {
|
|
90
|
-
constructor(public baseUrl: string) {}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const container = new Container();
|
|
94
|
-
container.provide(Config, new Config("/api"));
|
|
95
|
-
|
|
96
|
-
const cfg = container.inject(Config);
|
|
97
|
-
console.log(cfg.baseUrl);
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
#### Using DI with tinyfx components
|
|
101
|
-
|
|
102
|
-
When you use the tinyfx compiler, the generated glue calls your component behavior as:
|
|
57
|
+
import { createHttp } from "@tinyfx/runtime";
|
|
103
58
|
|
|
104
|
-
|
|
105
|
-
|
|
59
|
+
const http = createHttp({ baseUrl: "/api" });
|
|
60
|
+
const users = await http.get<{ id: number; name: string }[]>("/users");
|
|
106
61
|
```
|
|
107
62
|
|
|
108
|
-
|
|
63
|
+
### TinyFxContext
|
|
109
64
|
|
|
110
65
|
```ts
|
|
111
|
-
import type { TinyFxContext } from "@tinyfx/runtime
|
|
112
|
-
import { HttpService } from "../lib/http.service";
|
|
66
|
+
import type { TinyFxContext } from "@tinyfx/runtime";
|
|
113
67
|
|
|
114
68
|
export function init(el: HTMLElement, ctx: TinyFxContext) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
el.textContent = text;
|
|
118
|
-
});
|
|
69
|
+
console.log(ctx.params);
|
|
70
|
+
ctx.navigate("/about");
|
|
119
71
|
}
|
|
120
72
|
```
|
|
121
73
|
|
|
122
|
-
|
|
74
|
+
### Router + lifecycle hooks
|
|
123
75
|
|
|
124
76
|
```ts
|
|
125
|
-
|
|
126
|
-
import { createHttp } from "@tinyfx/runtime";
|
|
127
|
-
|
|
128
|
-
export class HttpService {
|
|
129
|
-
private readonly http;
|
|
77
|
+
import { initRouter, onMount, onDestroy } from "@tinyfx/runtime";
|
|
130
78
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
getWeather() {
|
|
136
|
-
// wttr.in can return plain text; your wrapper returns text when not JSON.
|
|
137
|
-
return this.http.get<string>("");
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
```ts
|
|
143
|
-
// src/main.ts
|
|
144
|
-
import { Container } from "@tinyfx/runtime/di";
|
|
145
|
-
import { setupComponents } from "./tinyfx.gen";
|
|
146
|
-
import { HttpService } from "./lib/http.service";
|
|
79
|
+
initRouter({
|
|
80
|
+
"/": { page: "index", path: "/" },
|
|
81
|
+
});
|
|
147
82
|
|
|
148
|
-
|
|
149
|
-
|
|
83
|
+
onMount(() => {
|
|
84
|
+
console.log("mounted");
|
|
85
|
+
});
|
|
150
86
|
|
|
151
|
-
|
|
152
|
-
|
|
87
|
+
onDestroy(() => {
|
|
88
|
+
console.log("destroyed");
|
|
153
89
|
});
|
|
154
90
|
```
|
|
155
91
|
|
|
156
|
-
## API
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
- `
|
|
160
|
-
- `
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
- `
|
|
164
|
-
- `
|
|
165
|
-
- `
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
- `
|
|
169
|
-
- `
|
|
170
|
-
- `
|
|
171
|
-
- `client.put<T>(url: string, data?: any): Promise<T>`
|
|
172
|
-
- `client.del<T>(url: string): Promise<T>`
|
|
173
|
-
|
|
174
|
-
### DI
|
|
175
|
-
- `new Container()`
|
|
176
|
-
- `container.provide(token, instance)`
|
|
177
|
-
- `container.inject(token)`
|
|
178
|
-
- `createToken<T>(description: string): symbol`
|
|
179
|
-
- `TinyFxContext` (passed to component init via tinyfx glue)
|
|
92
|
+
## API overview
|
|
93
|
+
|
|
94
|
+
- `signal<T>(value: T)`
|
|
95
|
+
- `effect(fn)`
|
|
96
|
+
- `bindText(el, source)`
|
|
97
|
+
- `bindAttr(el, attr, source)`
|
|
98
|
+
- `bindClass(el, className, source)`
|
|
99
|
+
- `createHttp(config?)`
|
|
100
|
+
- `initRouter(routes)`
|
|
101
|
+
- `getParam(name)`
|
|
102
|
+
- `navigate(path)`
|
|
103
|
+
- `goBack()`
|
|
104
|
+
- `onMount(fn)`
|
|
105
|
+
- `onDestroy(fn)`
|
|
106
|
+
- `TinyFxContext`
|
|
180
107
|
|
|
181
108
|
## License
|
|
182
109
|
|
package/dist/context.js
ADDED
package/dist/di.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export type Token<T> = (new (...args: any[]) => T) | symbol;
|
|
3
3
|
/** Create a unique symbol token with a debug description. */
|
|
4
4
|
export declare function createToken<T>(description: string): Token<T>;
|
|
5
|
-
/** Context passed to every component's
|
|
5
|
+
/** Context passed to every component's mount() factory function. */
|
|
6
6
|
export interface TinyFxContext {
|
|
7
7
|
container: Container;
|
|
8
8
|
}
|
package/dist/http.d.ts
CHANGED
|
@@ -1,10 +1,2 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
headers?: Record<string, string>;
|
|
4
|
-
}
|
|
5
|
-
export declare function createHttp(config?: HttpConfig): {
|
|
6
|
-
get: <T>(url: string) => Promise<T>;
|
|
7
|
-
post: <T>(url: string, body?: unknown) => Promise<T>;
|
|
8
|
-
put: <T>(url: string, body?: unknown) => Promise<T>;
|
|
9
|
-
del: <T>(url: string) => Promise<T>;
|
|
10
|
-
};
|
|
1
|
+
export { createHttp } from "./http/http";
|
|
2
|
+
export type { HttpConfig } from "./http/data";
|
package/dist/http.js
CHANGED
|
@@ -1,38 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// Thin, typed wrapper over fetch.
|
|
3
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
4
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
5
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
6
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
7
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
8
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
9
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
10
|
-
});
|
|
11
|
-
};
|
|
12
|
-
export function createHttp(config = {}) {
|
|
13
|
-
var _a, _b;
|
|
14
|
-
const base = (_a = config.baseUrl) !== null && _a !== void 0 ? _a : "";
|
|
15
|
-
const defaultHeaders = (_b = config.headers) !== null && _b !== void 0 ? _b : {};
|
|
16
|
-
function request(method, url, body) {
|
|
17
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
18
|
-
const headers = Object.assign({}, defaultHeaders);
|
|
19
|
-
if (body !== undefined) {
|
|
20
|
-
headers["Content-Type"] = "application/json";
|
|
21
|
-
}
|
|
22
|
-
const res = yield fetch(base + url, {
|
|
23
|
-
method,
|
|
24
|
-
headers,
|
|
25
|
-
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
26
|
-
});
|
|
27
|
-
if (!res.ok)
|
|
28
|
-
throw new Error(`${method} ${url}: ${res.status} ${res.statusText}`);
|
|
29
|
-
return res.json();
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
return {
|
|
33
|
-
get: (url) => request("GET", url),
|
|
34
|
-
post: (url, body) => request("POST", url, body),
|
|
35
|
-
put: (url, body) => request("PUT", url, body),
|
|
36
|
-
del: (url) => request("DELETE", url),
|
|
37
|
-
};
|
|
38
|
-
}
|
|
1
|
+
export { createHttp } from "./http/http";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export { signal, effect
|
|
1
|
+
export { signal, effect } from "./signals";
|
|
2
2
|
export { bindText, bindAttr, bindClass } from "./dom";
|
|
3
|
-
export { createHttp } from "./http
|
|
4
|
-
export
|
|
5
|
-
export {
|
|
6
|
-
export type {
|
|
3
|
+
export { createHttp } from "./http";
|
|
4
|
+
export { initRouter, getParam, navigate, goBack } from "./router/index";
|
|
5
|
+
export { onMount, onDestroy } from "./router/lifecycle";
|
|
6
|
+
export type { TinyFxContext } from "./context";
|
|
7
|
+
export type { RouteMap, RouteMeta } from "./router/types";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @tinyfx/runtime — barrel export
|
|
2
|
-
export { signal, effect
|
|
2
|
+
export { signal, effect } from "./signals";
|
|
3
3
|
export { bindText, bindAttr, bindClass } from "./dom";
|
|
4
|
-
export { createHttp } from "./http
|
|
5
|
-
export {
|
|
4
|
+
export { createHttp } from "./http";
|
|
5
|
+
export { initRouter, getParam, navigate, goBack } from "./router/index";
|
|
6
|
+
export { onMount, onDestroy } from "./router/lifecycle";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mark anchor tags whose `href` pathname matches `pathname` with
|
|
3
|
+
* `data-active="true"`, removing the attribute from all others.
|
|
4
|
+
*
|
|
5
|
+
* Only the pathname portion of each href is compared (query strings and
|
|
6
|
+
* fragments are ignored).
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // In a nav: <a href="/blog">Blog</a>
|
|
10
|
+
* // On the /blog page this element will get data-active="true"
|
|
11
|
+
* highlightActiveLinks("/blog");
|
|
12
|
+
*/
|
|
13
|
+
export declare function highlightActiveLinks(pathname: string): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// @tinyfx/runtime — active link highlighting
|
|
2
|
+
/**
|
|
3
|
+
* Mark anchor tags whose `href` pathname matches `pathname` with
|
|
4
|
+
* `data-active="true"`, removing the attribute from all others.
|
|
5
|
+
*
|
|
6
|
+
* Only the pathname portion of each href is compared (query strings and
|
|
7
|
+
* fragments are ignored).
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // In a nav: <a href="/blog">Blog</a>
|
|
11
|
+
* // On the /blog page this element will get data-active="true"
|
|
12
|
+
* highlightActiveLinks("/blog");
|
|
13
|
+
*/
|
|
14
|
+
export function highlightActiveLinks(pathname) {
|
|
15
|
+
const links = document.querySelectorAll("a[href]");
|
|
16
|
+
links.forEach((link) => {
|
|
17
|
+
try {
|
|
18
|
+
// Resolve href against the current origin to normalise relative URLs
|
|
19
|
+
const url = new URL(link.getAttribute("href"), window.location.origin);
|
|
20
|
+
if (url.pathname === pathname) {
|
|
21
|
+
link.setAttribute("data-active", "true");
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
link.removeAttribute("data-active");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (_a) {
|
|
28
|
+
// Ignore malformed / external hrefs
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { RouteMap } from "./types";
|
|
2
|
+
import { navigate, goBack } from "./navigate";
|
|
3
|
+
import { getParam } from "./params";
|
|
4
|
+
export type { RouteMap };
|
|
5
|
+
export { navigate, goBack, getParam };
|
|
6
|
+
/**
|
|
7
|
+
* Initialise the TinyFX router for the current page.
|
|
8
|
+
*
|
|
9
|
+
* Called automatically from the compiler-generated `tinyfx.gen.ts` on every
|
|
10
|
+
* page load. Matches `window.location.pathname` against the compiled route
|
|
11
|
+
* map, resolves URL params, starts lifecycle hooks, and highlights active
|
|
12
|
+
* navigation links.
|
|
13
|
+
*
|
|
14
|
+
* @param routes - The route map emitted by `tinyfx build`.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // tinyfx.gen.ts (auto-generated — do not edit)
|
|
18
|
+
* initRouter({
|
|
19
|
+
* "/": { page: "home", path: "/" },
|
|
20
|
+
* "/blog": { page: "blog", path: "/blog" },
|
|
21
|
+
* "/blog/:slug":{ page: "blog-post", path: "/blog/:slug" },
|
|
22
|
+
* });
|
|
23
|
+
*/
|
|
24
|
+
export declare function initRouter(routes: RouteMap): void;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// @tinyfx/runtime — router entry point
|
|
2
|
+
import { navigate, goBack } from "./navigate";
|
|
3
|
+
import { getParam, resolveParams } from "./params";
|
|
4
|
+
import { initLifecycle } from "./lifecycle";
|
|
5
|
+
import { highlightActiveLinks } from "./active-links";
|
|
6
|
+
export { navigate, goBack, getParam };
|
|
7
|
+
/**
|
|
8
|
+
* Initialise the TinyFX router for the current page.
|
|
9
|
+
*
|
|
10
|
+
* Called automatically from the compiler-generated `tinyfx.gen.ts` on every
|
|
11
|
+
* page load. Matches `window.location.pathname` against the compiled route
|
|
12
|
+
* map, resolves URL params, starts lifecycle hooks, and highlights active
|
|
13
|
+
* navigation links.
|
|
14
|
+
*
|
|
15
|
+
* @param routes - The route map emitted by `tinyfx build`.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // tinyfx.gen.ts (auto-generated — do not edit)
|
|
19
|
+
* initRouter({
|
|
20
|
+
* "/": { page: "home", path: "/" },
|
|
21
|
+
* "/blog": { page: "blog", path: "/blog" },
|
|
22
|
+
* "/blog/:slug":{ page: "blog-post", path: "/blog/:slug" },
|
|
23
|
+
* });
|
|
24
|
+
*/
|
|
25
|
+
export function initRouter(routes) {
|
|
26
|
+
const pathname = window.location.pathname;
|
|
27
|
+
for (const pattern of Object.keys(routes)) {
|
|
28
|
+
if (resolveParams(pattern, pathname)) {
|
|
29
|
+
// Match found
|
|
30
|
+
initLifecycle();
|
|
31
|
+
highlightActiveLinks(pathname);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// No route matched — log a warning and do nothing else.
|
|
36
|
+
console.warn(`[tinyfx] No route matched for pathname: "${pathname}". ` +
|
|
37
|
+
"Check that a page file exists for this URL in src/pages/.");
|
|
38
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
type LifecycleCallback = () => void;
|
|
2
|
+
/**
|
|
3
|
+
* Register a callback to run when the page has mounted (DOM ready).
|
|
4
|
+
*
|
|
5
|
+
* - If the document is still loading, the callback runs after DOMContentLoaded.
|
|
6
|
+
* - If the document is already loaded, the callback runs immediately.
|
|
7
|
+
*/
|
|
8
|
+
export declare function onMount(fn: LifecycleCallback): void;
|
|
9
|
+
/**
|
|
10
|
+
* Register a callback to run when the user navigates away from this page.
|
|
11
|
+
* Fires on the browser's `pagehide` event.
|
|
12
|
+
*/
|
|
13
|
+
export declare function onDestroy(fn: LifecycleCallback): void;
|
|
14
|
+
/**
|
|
15
|
+
* Trigger all registered `onMount` callbacks.
|
|
16
|
+
* Called by `initRouter` after matching the current route.
|
|
17
|
+
*/
|
|
18
|
+
export declare function flushMount(): void;
|
|
19
|
+
/**
|
|
20
|
+
* Wire up registered `onDestroy` callbacks to the `pagehide` event.
|
|
21
|
+
* Called by `initRouter` after matching the current route.
|
|
22
|
+
*/
|
|
23
|
+
export declare function initLifecycle(): void;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @tinyfx/runtime — page lifecycle hooks
|
|
2
|
+
const mountCallbacks = [];
|
|
3
|
+
const destroyCallbacks = [];
|
|
4
|
+
/**
|
|
5
|
+
* Register a callback to run when the page has mounted (DOM ready).
|
|
6
|
+
*
|
|
7
|
+
* - If the document is still loading, the callback runs after DOMContentLoaded.
|
|
8
|
+
* - If the document is already loaded, the callback runs immediately.
|
|
9
|
+
*/
|
|
10
|
+
export function onMount(fn) {
|
|
11
|
+
if (document.readyState === "loading") {
|
|
12
|
+
mountCallbacks.push(fn);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
fn();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Register a callback to run when the user navigates away from this page.
|
|
19
|
+
* Fires on the browser's `pagehide` event.
|
|
20
|
+
*/
|
|
21
|
+
export function onDestroy(fn) {
|
|
22
|
+
destroyCallbacks.push(fn);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Trigger all registered `onMount` callbacks.
|
|
26
|
+
* Called by `initRouter` after matching the current route.
|
|
27
|
+
*/
|
|
28
|
+
export function flushMount() {
|
|
29
|
+
const run = () => mountCallbacks.forEach((fn) => fn());
|
|
30
|
+
if (document.readyState === "loading") {
|
|
31
|
+
document.addEventListener("DOMContentLoaded", run, { once: true });
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
run();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Wire up registered `onDestroy` callbacks to the `pagehide` event.
|
|
39
|
+
* Called by `initRouter` after matching the current route.
|
|
40
|
+
*/
|
|
41
|
+
export function initLifecycle() {
|
|
42
|
+
flushMount();
|
|
43
|
+
window.addEventListener("pagehide", () => destroyCallbacks.forEach((fn) => fn()), { once: true });
|
|
44
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigate to the given path.
|
|
3
|
+
* This is a full browser navigation — not a DOM swap.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* navigate("/blog/hello-world");
|
|
7
|
+
*/
|
|
8
|
+
export declare function navigate(path: string): void;
|
|
9
|
+
/**
|
|
10
|
+
* Go back one step in the browser history.
|
|
11
|
+
*/
|
|
12
|
+
export declare function goBack(): void;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// @tinyfx/runtime — navigation helpers
|
|
2
|
+
// Hard router: navigation is always a real browser page load.
|
|
3
|
+
/**
|
|
4
|
+
* Navigate to the given path.
|
|
5
|
+
* This is a full browser navigation — not a DOM swap.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* navigate("/blog/hello-world");
|
|
9
|
+
*/
|
|
10
|
+
export function navigate(path) {
|
|
11
|
+
window.location.href = path;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Go back one step in the browser history.
|
|
15
|
+
*/
|
|
16
|
+
export function goBack() {
|
|
17
|
+
window.history.go(-1);
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Match `pattern` (e.g. "/blog/:slug") against `pathname` (e.g. "/blog/hello").
|
|
3
|
+
*
|
|
4
|
+
* Returns true if they match, and stores any `:param` captures in the
|
|
5
|
+
* module-level store so `getParam()` can retrieve them.
|
|
6
|
+
*
|
|
7
|
+
* Static segments must match exactly; `:param` segments match any non-empty
|
|
8
|
+
* path segment.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveParams(pattern: string, pathname: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Retrieve a URL parameter resolved for the current page.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // On the page with route "/blog/:slug" and URL "/blog/hello-world"
|
|
16
|
+
* getParam("slug"); // → "hello-world"
|
|
17
|
+
*/
|
|
18
|
+
export declare function getParam(key: string): string | undefined;
|
|
19
|
+
export declare function getParams(): Record<string, string>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// @tinyfx/runtime — route param resolution
|
|
2
|
+
/** Module-level store for the current page's resolved params. */
|
|
3
|
+
let currentParams = {};
|
|
4
|
+
/**
|
|
5
|
+
* Match `pattern` (e.g. "/blog/:slug") against `pathname` (e.g. "/blog/hello").
|
|
6
|
+
*
|
|
7
|
+
* Returns true if they match, and stores any `:param` captures in the
|
|
8
|
+
* module-level store so `getParam()` can retrieve them.
|
|
9
|
+
*
|
|
10
|
+
* Static segments must match exactly; `:param` segments match any non-empty
|
|
11
|
+
* path segment.
|
|
12
|
+
*/
|
|
13
|
+
export function resolveParams(pattern, pathname) {
|
|
14
|
+
const patternParts = pattern.split("/").filter(Boolean);
|
|
15
|
+
const pathParts = pathname.split("/").filter(Boolean);
|
|
16
|
+
if (patternParts.length !== pathParts.length)
|
|
17
|
+
return false;
|
|
18
|
+
const captured = {};
|
|
19
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
20
|
+
const seg = patternParts[i];
|
|
21
|
+
if (seg.startsWith(":")) {
|
|
22
|
+
// Dynamic segment — capture the value
|
|
23
|
+
captured[seg.slice(1)] = decodeURIComponent(pathParts[i]);
|
|
24
|
+
}
|
|
25
|
+
else if (seg !== pathParts[i]) {
|
|
26
|
+
// Static segment mismatch
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
currentParams = captured;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Retrieve a URL parameter resolved for the current page.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* // On the page with route "/blog/:slug" and URL "/blog/hello-world"
|
|
38
|
+
* getParam("slug"); // → "hello-world"
|
|
39
|
+
*/
|
|
40
|
+
export function getParam(key) {
|
|
41
|
+
return currentParams[key];
|
|
42
|
+
}
|
|
43
|
+
export function getParams() {
|
|
44
|
+
return Object.assign({}, currentParams);
|
|
45
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Metadata for a single route, produced by the compiler. */
|
|
2
|
+
export interface RouteMeta {
|
|
3
|
+
/** The page name, derived from the source filename. */
|
|
4
|
+
page: string;
|
|
5
|
+
/** The declared URL pattern, e.g. "/blog/:slug". */
|
|
6
|
+
path: string;
|
|
7
|
+
}
|
|
8
|
+
/** The full route map emitted by `tinyfx build` into tinyfx.gen.ts. */
|
|
9
|
+
export type RouteMap = Record<string, RouteMeta>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tinyfx/runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Minimal frontend runtime — signals, DOM helpers, typed HTTP, DTO mapping",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"./signals": "./dist/signals.js",
|
|
11
11
|
"./dom": "./dist/dom.js",
|
|
12
12
|
"./http": "./dist/http.js",
|
|
13
|
-
"./
|
|
13
|
+
"./router": "./dist/router/index.js"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"dist"
|