@tinyfx/runtime 0.1.9 → 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/__tests__/http-errors.test.d.ts +1 -0
- package/dist/__tests__/http-errors.test.js +65 -0
- package/dist/__tests__/router-merged.test.d.ts +1 -0
- package/dist/__tests__/router-merged.test.js +33 -0
- package/dist/__tests__/signals.test.d.ts +1 -0
- package/dist/__tests__/signals.test.js +93 -0
- package/dist/http/data.d.ts +32 -41
- package/dist/http/data.js +15 -59
- package/dist/http/http.js +6 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/init.js +3 -1
- package/dist/router/index.d.ts +21 -23
- package/dist/router/index.js +12 -36
- package/dist/router/lifecycle.d.ts +2 -2
- package/dist/router/lifecycle.js +2 -2
- package/dist/router/params.d.ts +3 -14
- package/dist/router/params.js +4 -32
- package/dist/signals.d.ts +0 -20
- package/dist/signals.js +14 -35
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { httpError, timeoutError, parseError } from "../http/data";
|
|
3
|
+
describe("httpError", () => {
|
|
4
|
+
it("creates http error with all fields", () => {
|
|
5
|
+
const err = httpError(404, "Not Found", "/api/test", "GET");
|
|
6
|
+
expect(err.kind).toBe("http");
|
|
7
|
+
if (err.kind === "http") {
|
|
8
|
+
expect(err.status).toBe(404);
|
|
9
|
+
expect(err.statusText).toBe("Not Found");
|
|
10
|
+
expect(err.url).toBe("/api/test");
|
|
11
|
+
expect(err.method).toBe("GET");
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
it("includes optional response", () => {
|
|
15
|
+
const response = new Response();
|
|
16
|
+
const err = httpError(500, "Server Error", "/api", "POST", response);
|
|
17
|
+
if (err.kind === "http") {
|
|
18
|
+
expect(err.response).toBe(response);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
describe("timeoutError", () => {
|
|
23
|
+
it("creates timeout error with url and timeout", () => {
|
|
24
|
+
const err = timeoutError("/api/slow", 5000);
|
|
25
|
+
expect(err.kind).toBe("timeout");
|
|
26
|
+
if (err.kind === "timeout") {
|
|
27
|
+
expect(err.url).toBe("/api/slow");
|
|
28
|
+
expect(err.timeout).toBe(5000);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe("parseError", () => {
|
|
33
|
+
it("creates parse error with message and url", () => {
|
|
34
|
+
const err = parseError("Unexpected token", "/api/json");
|
|
35
|
+
expect(err.kind).toBe("parse");
|
|
36
|
+
if (err.kind === "parse") {
|
|
37
|
+
expect(err.message).toBe("Unexpected token");
|
|
38
|
+
expect(err.url).toBe("/api/json");
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe("error discrimination", () => {
|
|
43
|
+
it("allows discriminating by kind", () => {
|
|
44
|
+
const errors = [
|
|
45
|
+
httpError(400, "Bad Request", "/a", "GET"),
|
|
46
|
+
timeoutError("/b", 1000),
|
|
47
|
+
parseError("Bad JSON", "/c"),
|
|
48
|
+
];
|
|
49
|
+
const httpErr = errors.find((e) => e.kind === "http");
|
|
50
|
+
expect(httpErr === null || httpErr === void 0 ? void 0 : httpErr.kind).toBe("http");
|
|
51
|
+
if ((httpErr === null || httpErr === void 0 ? void 0 : httpErr.kind) === "http") {
|
|
52
|
+
expect(httpErr.status).toBe(400);
|
|
53
|
+
}
|
|
54
|
+
const timeoutErr = errors.find((e) => e.kind === "timeout");
|
|
55
|
+
expect(timeoutErr === null || timeoutErr === void 0 ? void 0 : timeoutErr.kind).toBe("timeout");
|
|
56
|
+
if ((timeoutErr === null || timeoutErr === void 0 ? void 0 : timeoutErr.kind) === "timeout") {
|
|
57
|
+
expect(timeoutErr.timeout).toBe(1000);
|
|
58
|
+
}
|
|
59
|
+
const parseErr = errors.find((e) => e.kind === "parse");
|
|
60
|
+
expect(parseErr === null || parseErr === void 0 ? void 0 : parseErr.kind).toBe("parse");
|
|
61
|
+
if ((parseErr === null || parseErr === void 0 ? void 0 : parseErr.kind) === "parse") {
|
|
62
|
+
expect(parseErr.message).toBe("Bad JSON");
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { navigate, goBack } from "../router/index";
|
|
3
|
+
describe("navigate", () => {
|
|
4
|
+
it("is a function that accepts a path string", () => {
|
|
5
|
+
expect(typeof navigate).toBe("function");
|
|
6
|
+
expect(() => navigate("/test/path")).not.toThrow();
|
|
7
|
+
});
|
|
8
|
+
});
|
|
9
|
+
describe("goBack", () => {
|
|
10
|
+
it("calls history.go(-1)", () => {
|
|
11
|
+
const spy = vi.spyOn(window.history, "go");
|
|
12
|
+
goBack();
|
|
13
|
+
expect(spy).toHaveBeenCalledWith(-1);
|
|
14
|
+
spy.mockRestore();
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
describe("RouteMeta type", () => {
|
|
18
|
+
it("accepts valid route metadata", () => {
|
|
19
|
+
const meta = { page: "blog-post", path: "/blog/:slug" };
|
|
20
|
+
expect(meta.page).toBe("blog-post");
|
|
21
|
+
expect(meta.path).toBe("/blog/:slug");
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe("RouteMap type", () => {
|
|
25
|
+
it("accepts route map object", () => {
|
|
26
|
+
const routes = {
|
|
27
|
+
"/": { page: "index", path: "/" },
|
|
28
|
+
"/blog/:slug": { page: "blog-post", path: "/blog/:slug" },
|
|
29
|
+
};
|
|
30
|
+
expect(Object.keys(routes)).toHaveLength(2);
|
|
31
|
+
expect(routes["/blog/:slug"].page).toBe("blog-post");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { signal, effect } from "../signals";
|
|
3
|
+
describe("signal (inlined)", () => {
|
|
4
|
+
it("returns current value when called", () => {
|
|
5
|
+
const count = signal(0);
|
|
6
|
+
expect(count()).toBe(0);
|
|
7
|
+
});
|
|
8
|
+
it("updates value via set()", () => {
|
|
9
|
+
const count = signal(0);
|
|
10
|
+
count.set(5);
|
|
11
|
+
expect(count()).toBe(5);
|
|
12
|
+
});
|
|
13
|
+
it("does not notify subscribers when value is the same", () => {
|
|
14
|
+
const count = signal(0);
|
|
15
|
+
let runs = 0;
|
|
16
|
+
effect(() => {
|
|
17
|
+
count();
|
|
18
|
+
runs++;
|
|
19
|
+
});
|
|
20
|
+
expect(runs).toBe(1);
|
|
21
|
+
count.set(0);
|
|
22
|
+
expect(runs).toBe(1);
|
|
23
|
+
});
|
|
24
|
+
it("notifies subscribers on change", () => {
|
|
25
|
+
const count = signal(0);
|
|
26
|
+
let value = 0;
|
|
27
|
+
effect(() => {
|
|
28
|
+
value = count();
|
|
29
|
+
});
|
|
30
|
+
expect(value).toBe(0);
|
|
31
|
+
count.set(10);
|
|
32
|
+
expect(value).toBe(10);
|
|
33
|
+
});
|
|
34
|
+
it("supports multiple subscribers", () => {
|
|
35
|
+
const count = signal(0);
|
|
36
|
+
let a = 0;
|
|
37
|
+
let b = 0;
|
|
38
|
+
effect(() => { a = count(); });
|
|
39
|
+
effect(() => { b = count() * 2; });
|
|
40
|
+
count.set(3);
|
|
41
|
+
expect(a).toBe(3);
|
|
42
|
+
expect(b).toBe(6);
|
|
43
|
+
});
|
|
44
|
+
it("works with objects", () => {
|
|
45
|
+
const user = signal({ name: "Alice" });
|
|
46
|
+
expect(user().name).toBe("Alice");
|
|
47
|
+
user.set({ name: "Bob" });
|
|
48
|
+
expect(user().name).toBe("Bob");
|
|
49
|
+
});
|
|
50
|
+
it("does not re-run effect when unrelated signal changes", () => {
|
|
51
|
+
const a = signal(0);
|
|
52
|
+
const b = signal(0);
|
|
53
|
+
let runs = 0;
|
|
54
|
+
effect(() => {
|
|
55
|
+
a();
|
|
56
|
+
runs++;
|
|
57
|
+
});
|
|
58
|
+
expect(runs).toBe(1);
|
|
59
|
+
b.set(1);
|
|
60
|
+
expect(runs).toBe(1);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe("effect", () => {
|
|
64
|
+
it("runs immediately on creation", () => {
|
|
65
|
+
let ran = false;
|
|
66
|
+
effect(() => { ran = true; });
|
|
67
|
+
expect(ran).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
it("re-runs when tracked signal changes", () => {
|
|
70
|
+
const count = signal(0);
|
|
71
|
+
let runs = 0;
|
|
72
|
+
effect(() => {
|
|
73
|
+
count();
|
|
74
|
+
runs++;
|
|
75
|
+
});
|
|
76
|
+
expect(runs).toBe(1);
|
|
77
|
+
count.set(1);
|
|
78
|
+
expect(runs).toBe(2);
|
|
79
|
+
});
|
|
80
|
+
it("supports nested signal reads", () => {
|
|
81
|
+
const a = signal(1);
|
|
82
|
+
const b = signal(2);
|
|
83
|
+
let sum = 0;
|
|
84
|
+
effect(() => {
|
|
85
|
+
sum = a() + b();
|
|
86
|
+
});
|
|
87
|
+
expect(sum).toBe(3);
|
|
88
|
+
a.set(10);
|
|
89
|
+
expect(sum).toBe(12);
|
|
90
|
+
b.set(5);
|
|
91
|
+
expect(sum).toBe(15);
|
|
92
|
+
});
|
|
93
|
+
});
|
package/dist/http/data.d.ts
CHANGED
|
@@ -1,56 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Discriminated union type for HTTP errors.
|
|
3
3
|
*
|
|
4
4
|
* @example
|
|
5
5
|
* try {
|
|
6
6
|
* await http.get("/posts");
|
|
7
7
|
* } catch (error) {
|
|
8
|
-
* if (error
|
|
8
|
+
* if (error.kind === "http") {
|
|
9
9
|
* console.error(error.status, error.url);
|
|
10
|
-
* }
|
|
11
|
-
* }
|
|
12
|
-
*/
|
|
13
|
-
export declare class HttpError extends Error {
|
|
14
|
-
readonly status: number;
|
|
15
|
-
readonly statusText: string;
|
|
16
|
-
readonly url: string;
|
|
17
|
-
readonly method: string;
|
|
18
|
-
readonly response?: Response | undefined;
|
|
19
|
-
constructor(status: number, statusText: string, url: string, method: string, response?: Response | undefined);
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Error thrown when a request times out.
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* try {
|
|
26
|
-
* await http.get("/slow", { timeout: 100 });
|
|
27
|
-
* } catch (error) {
|
|
28
|
-
* if (error instanceof HttpTimeoutError) {
|
|
10
|
+
* } else if (error.kind === "timeout") {
|
|
29
11
|
* console.error(error.timeout);
|
|
30
|
-
* }
|
|
31
|
-
* }
|
|
32
|
-
*/
|
|
33
|
-
export declare class HttpTimeoutError extends Error {
|
|
34
|
-
readonly url: string;
|
|
35
|
-
readonly timeout: number;
|
|
36
|
-
constructor(url: string, timeout: number);
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Error thrown when a JSON response cannot be parsed.
|
|
40
|
-
*
|
|
41
|
-
* @example
|
|
42
|
-
* try {
|
|
43
|
-
* await http.get("/broken-json");
|
|
44
|
-
* } catch (error) {
|
|
45
|
-
* if (error instanceof HttpParseError) {
|
|
12
|
+
* } else if (error.kind === "parse") {
|
|
46
13
|
* console.error(error.url);
|
|
47
14
|
* }
|
|
48
15
|
* }
|
|
49
16
|
*/
|
|
50
|
-
export
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
17
|
+
export type HttpError = {
|
|
18
|
+
kind: "http";
|
|
19
|
+
status: number;
|
|
20
|
+
statusText: string;
|
|
21
|
+
url: string;
|
|
22
|
+
method: string;
|
|
23
|
+
response?: Response;
|
|
24
|
+
} | {
|
|
25
|
+
kind: "timeout";
|
|
26
|
+
url: string;
|
|
27
|
+
timeout: number;
|
|
28
|
+
} | {
|
|
29
|
+
kind: "parse";
|
|
30
|
+
message: string;
|
|
31
|
+
url: string;
|
|
32
|
+
};
|
|
33
|
+
export declare function httpError(status: number, statusText: string, url: string, method: string, response?: Response): HttpError;
|
|
34
|
+
export declare function timeoutError(url: string, timeout: number): HttpError;
|
|
35
|
+
export declare function parseError(message: string, url: string): HttpError;
|
|
36
|
+
export declare function isHttpError(err: unknown): err is Extract<HttpError, {
|
|
37
|
+
kind: "http";
|
|
38
|
+
}>;
|
|
39
|
+
export declare function isTimeoutError(err: unknown): err is Extract<HttpError, {
|
|
40
|
+
kind: "timeout";
|
|
41
|
+
}>;
|
|
42
|
+
export declare function isParseError(err: unknown): err is Extract<HttpError, {
|
|
43
|
+
kind: "parse";
|
|
44
|
+
}>;
|
|
54
45
|
/**
|
|
55
46
|
* Hook that can rewrite an outgoing request before `fetch` runs.
|
|
56
47
|
*
|
package/dist/http/data.js
CHANGED
|
@@ -1,62 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* @example
|
|
5
|
-
* try {
|
|
6
|
-
* await http.get("/posts");
|
|
7
|
-
* } catch (error) {
|
|
8
|
-
* if (error instanceof HttpError) {
|
|
9
|
-
* console.error(error.status, error.url);
|
|
10
|
-
* }
|
|
11
|
-
* }
|
|
12
|
-
*/
|
|
13
|
-
export class HttpError extends Error {
|
|
14
|
-
constructor(status, statusText, url, method, response) {
|
|
15
|
-
super(`HTTP ${status} ${statusText}: ${method} ${url}`);
|
|
16
|
-
this.status = status;
|
|
17
|
-
this.statusText = statusText;
|
|
18
|
-
this.url = url;
|
|
19
|
-
this.method = method;
|
|
20
|
-
this.response = response;
|
|
21
|
-
this.name = "HttpError";
|
|
22
|
-
}
|
|
1
|
+
export function httpError(status, statusText, url, method, response) {
|
|
2
|
+
return { kind: "http", status, statusText, url, method, response };
|
|
23
3
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* try {
|
|
29
|
-
* await http.get("/slow", { timeout: 100 });
|
|
30
|
-
* } catch (error) {
|
|
31
|
-
* if (error instanceof HttpTimeoutError) {
|
|
32
|
-
* console.error(error.timeout);
|
|
33
|
-
* }
|
|
34
|
-
* }
|
|
35
|
-
*/
|
|
36
|
-
export class HttpTimeoutError extends Error {
|
|
37
|
-
constructor(url, timeout) {
|
|
38
|
-
super(`Request timeout after ${timeout}ms: ${url}`);
|
|
39
|
-
this.url = url;
|
|
40
|
-
this.timeout = timeout;
|
|
41
|
-
this.name = "HttpTimeoutError";
|
|
42
|
-
}
|
|
4
|
+
export function timeoutError(url, timeout) {
|
|
5
|
+
return { kind: "timeout", url, timeout };
|
|
43
6
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
*/
|
|
56
|
-
export class HttpParseError extends Error {
|
|
57
|
-
constructor(message, url) {
|
|
58
|
-
super(message);
|
|
59
|
-
this.url = url;
|
|
60
|
-
this.name = "HttpParseError";
|
|
61
|
-
}
|
|
7
|
+
export function parseError(message, url) {
|
|
8
|
+
return { kind: "parse", message, url };
|
|
9
|
+
}
|
|
10
|
+
export function isHttpError(err) {
|
|
11
|
+
return typeof err === "object" && err !== null && "kind" in err && err.kind === "http";
|
|
12
|
+
}
|
|
13
|
+
export function isTimeoutError(err) {
|
|
14
|
+
return typeof err === "object" && err !== null && "kind" in err && err.kind === "timeout";
|
|
15
|
+
}
|
|
16
|
+
export function isParseError(err) {
|
|
17
|
+
return typeof err === "object" && err !== null && "kind" in err && err.kind === "parse";
|
|
62
18
|
}
|
package/dist/http/http.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @tinyfx/runtime — HTTP client
|
|
2
2
|
// Thin, typed wrapper over fetch.
|
|
3
|
-
import {
|
|
3
|
+
import { httpError, timeoutError, parseError, isHttpError } from "./data";
|
|
4
4
|
import { buildUrl, sleep } from "./helper";
|
|
5
5
|
/**
|
|
6
6
|
* Creates a typed HTTP client around `fetch`.
|
|
@@ -73,7 +73,7 @@ export function createHttp(config = {}) {
|
|
|
73
73
|
if ((_b = options.signal) === null || _b === void 0 ? void 0 : _b.aborted) {
|
|
74
74
|
throw err; // User cancelled
|
|
75
75
|
}
|
|
76
|
-
throw
|
|
76
|
+
throw timeoutError(requestUrl, timeoutMs);
|
|
77
77
|
}
|
|
78
78
|
throw err;
|
|
79
79
|
}
|
|
@@ -85,7 +85,7 @@ export function createHttp(config = {}) {
|
|
|
85
85
|
res = await interceptor(res);
|
|
86
86
|
}
|
|
87
87
|
if (!res.ok) {
|
|
88
|
-
throw
|
|
88
|
+
throw httpError(res.status, res.statusText, requestUrl, method, res);
|
|
89
89
|
}
|
|
90
90
|
// Parse response based on content type
|
|
91
91
|
const contentType = res.headers.get("content-type") || "";
|
|
@@ -98,7 +98,7 @@ export function createHttp(config = {}) {
|
|
|
98
98
|
return JSON.parse(text);
|
|
99
99
|
}
|
|
100
100
|
catch (err) {
|
|
101
|
-
throw
|
|
101
|
+
throw parseError(`Failed to parse JSON response: ${err instanceof Error ? err.message : "Unknown error"}`, requestUrl);
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
if (contentType.includes("text/")) {
|
|
@@ -115,8 +115,8 @@ export function createHttp(config = {}) {
|
|
|
115
115
|
}
|
|
116
116
|
catch (err) {
|
|
117
117
|
// Retry logic for network errors or 5xx errors
|
|
118
|
-
const
|
|
119
|
-
|
|
118
|
+
const is5xx = isHttpError(err) && err.status >= 500 && err.status < 600;
|
|
119
|
+
const shouldRetry = attempts < maxAttempts && (!isHttpError(err) || is5xx);
|
|
120
120
|
if (shouldRetry) {
|
|
121
121
|
await sleep(retryDelay * attempts);
|
|
122
122
|
continue;
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export { matchPath, splitPath } from "./router/path-matcher";
|
|
|
9
9
|
export type { RouteDef } from "./router/path-matcher";
|
|
10
10
|
export { init } from "./init";
|
|
11
11
|
export type { InitConfig } from "./init";
|
|
12
|
-
export {
|
|
12
|
+
export { getParam, getParams, navigate, goBack } from "./router/index";
|
|
13
13
|
export { onMount, onDestroy } from "./router/lifecycle";
|
|
14
14
|
export type { TinyFxContext } from "./context";
|
|
15
|
-
export type { RouteMap, RouteMeta } from "./router/
|
|
15
|
+
export type { RouteMap, RouteMeta } from "./router/index";
|
package/dist/index.js
CHANGED
|
@@ -10,5 +10,5 @@ export { markMounted, isMounted } from "./mount-state";
|
|
|
10
10
|
export { matchPath, splitPath } from "./router/path-matcher";
|
|
11
11
|
export { init } from "./init";
|
|
12
12
|
// Keep existing router exports
|
|
13
|
-
export {
|
|
13
|
+
export { getParam, getParams, navigate, goBack } from "./router/index";
|
|
14
14
|
export { onMount, onDestroy } from "./router/lifecycle";
|
package/dist/init.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import { matchPath, splitPath } from "./router/path-matcher";
|
|
4
4
|
import { initLifecycle } from "./router/lifecycle";
|
|
5
5
|
import { highlightActiveLinks } from "./router/active-links";
|
|
6
|
-
import { navigate } from "./router/
|
|
6
|
+
import { navigate } from "./router/index";
|
|
7
|
+
import { setParams } from "./router/params";
|
|
7
8
|
import { mountComponents } from "./registry";
|
|
8
9
|
import { runPageInit } from "./page-registry";
|
|
9
10
|
let initialized = false;
|
|
@@ -30,6 +31,7 @@ export function init(config) {
|
|
|
30
31
|
}
|
|
31
32
|
initLifecycle();
|
|
32
33
|
highlightActiveLinks(pathname);
|
|
34
|
+
setParams(params);
|
|
33
35
|
const ctx = { params, navigate };
|
|
34
36
|
const instances = mountComponents(ctx);
|
|
35
37
|
if (config.setupDirectives) {
|
package/dist/router/index.d.ts
CHANGED
|
@@ -1,25 +1,23 @@
|
|
|
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
1
|
/**
|
|
7
|
-
*
|
|
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
|
-
* @returns Nothing
|
|
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
|
-
* });
|
|
2
|
+
* Metadata for a single route produced by the compiler.
|
|
24
3
|
*/
|
|
25
|
-
export
|
|
4
|
+
export interface RouteMeta {
|
|
5
|
+
/** The page name, derived from the source filename. */
|
|
6
|
+
page: string;
|
|
7
|
+
/** The declared URL pattern, e.g. "/blog/:slug". */
|
|
8
|
+
path: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The full route map emitted by `tinyfx build` into `tinyfx.gen.ts`.
|
|
12
|
+
*/
|
|
13
|
+
export type RouteMap = Record<string, RouteMeta>;
|
|
14
|
+
/**
|
|
15
|
+
* Navigate to the given path.
|
|
16
|
+
* This is a full browser navigation — not a DOM swap.
|
|
17
|
+
*/
|
|
18
|
+
export declare function navigate(path: string): void;
|
|
19
|
+
/**
|
|
20
|
+
* Go back one step in the browser history.
|
|
21
|
+
*/
|
|
22
|
+
export declare function goBack(): void;
|
|
23
|
+
export { getParam, getParams } from "./params";
|
package/dist/router/index.js
CHANGED
|
@@ -1,39 +1,15 @@
|
|
|
1
|
-
// @tinyfx/runtime — router
|
|
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 };
|
|
1
|
+
// @tinyfx/runtime — router utilities
|
|
7
2
|
/**
|
|
8
|
-
*
|
|
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
|
-
* @returns Nothing
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* // tinyfx.gen.ts (auto-generated — do not edit)
|
|
20
|
-
* initRouter({
|
|
21
|
-
* "/": { page: "home", path: "/" },
|
|
22
|
-
* "/blog": { page: "blog", path: "/blog" },
|
|
23
|
-
* "/blog/:slug":{ page: "blog-post", path: "/blog/:slug" },
|
|
24
|
-
* });
|
|
3
|
+
* Navigate to the given path.
|
|
4
|
+
* This is a full browser navigation — not a DOM swap.
|
|
25
5
|
*/
|
|
26
|
-
export function
|
|
27
|
-
|
|
28
|
-
for (const pattern of Object.keys(routes)) {
|
|
29
|
-
if (resolveParams(pattern, pathname)) {
|
|
30
|
-
// Match found
|
|
31
|
-
initLifecycle();
|
|
32
|
-
highlightActiveLinks(pathname);
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
// No route matched — log a warning and do nothing else.
|
|
37
|
-
console.warn(`[tinyfx] No route matched for pathname: "${pathname}". ` +
|
|
38
|
-
"Check that a page file exists for this URL in src/pages/.");
|
|
6
|
+
export function navigate(path) {
|
|
7
|
+
window.location.href = path;
|
|
39
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Go back one step in the browser history.
|
|
11
|
+
*/
|
|
12
|
+
export function goBack() {
|
|
13
|
+
window.history.go(-1);
|
|
14
|
+
}
|
|
15
|
+
export { getParam, getParams } from "./params";
|
|
@@ -29,7 +29,7 @@ export declare function onMount(fn: LifecycleCallback): void;
|
|
|
29
29
|
export declare function onDestroy(fn: LifecycleCallback): void;
|
|
30
30
|
/**
|
|
31
31
|
* Trigger all registered `onMount` callbacks.
|
|
32
|
-
* Called by `
|
|
32
|
+
* Called by `init()` after matching the current route.
|
|
33
33
|
*
|
|
34
34
|
* @returns Nothing
|
|
35
35
|
*
|
|
@@ -39,7 +39,7 @@ export declare function onDestroy(fn: LifecycleCallback): void;
|
|
|
39
39
|
export declare function flushMount(): void;
|
|
40
40
|
/**
|
|
41
41
|
* Wire up registered `onDestroy` callbacks to the `pagehide` event.
|
|
42
|
-
* Called by `
|
|
42
|
+
* Called by `init()` after matching the current route.
|
|
43
43
|
*
|
|
44
44
|
* @returns Nothing
|
|
45
45
|
*
|
package/dist/router/lifecycle.js
CHANGED
|
@@ -39,7 +39,7 @@ export function onDestroy(fn) {
|
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
41
|
* Trigger all registered `onMount` callbacks.
|
|
42
|
-
* Called by `
|
|
42
|
+
* Called by `init()` after matching the current route.
|
|
43
43
|
*
|
|
44
44
|
* @returns Nothing
|
|
45
45
|
*
|
|
@@ -57,7 +57,7 @@ export function flushMount() {
|
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
59
|
* Wire up registered `onDestroy` callbacks to the `pagehide` event.
|
|
60
|
-
* Called by `
|
|
60
|
+
* Called by `init()` after matching the current route.
|
|
61
61
|
*
|
|
62
62
|
* @returns Nothing
|
|
63
63
|
*
|
package/dist/router/params.d.ts
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Set resolved route params (called internally by init()).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
-
* @param pattern - The route pattern produced by the compiler
|
|
11
|
-
* @param pathname - The current browser pathname
|
|
12
|
-
* @returns `true` when the pathname matches the pattern, otherwise `false`
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* resolveParams("/blog/:slug", "/blog/hello-world");
|
|
4
|
+
* @param params - The resolved URL parameters
|
|
16
5
|
*/
|
|
17
|
-
export declare function
|
|
6
|
+
export declare function setParams(params: Record<string, string>): void;
|
|
18
7
|
/**
|
|
19
8
|
* Retrieve a URL parameter resolved for the current page.
|
|
20
9
|
*
|
package/dist/router/params.js
CHANGED
|
@@ -2,40 +2,12 @@
|
|
|
2
2
|
/** Module-level store for the current page's resolved params. */
|
|
3
3
|
let currentParams = {};
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Set resolved route params (called internally by init()).
|
|
6
6
|
*
|
|
7
|
-
*
|
|
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
|
-
* @param pattern - The route pattern produced by the compiler
|
|
14
|
-
* @param pathname - The current browser pathname
|
|
15
|
-
* @returns `true` when the pathname matches the pattern, otherwise `false`
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* resolveParams("/blog/:slug", "/blog/hello-world");
|
|
7
|
+
* @param params - The resolved URL parameters
|
|
19
8
|
*/
|
|
20
|
-
export function
|
|
21
|
-
|
|
22
|
-
const pathParts = pathname.split("/").filter(Boolean);
|
|
23
|
-
if (patternParts.length !== pathParts.length)
|
|
24
|
-
return false;
|
|
25
|
-
const captured = {};
|
|
26
|
-
for (let i = 0; i < patternParts.length; i++) {
|
|
27
|
-
const seg = patternParts[i];
|
|
28
|
-
if (seg.startsWith(":")) {
|
|
29
|
-
// Dynamic segment — capture the value
|
|
30
|
-
captured[seg.slice(1)] = decodeURIComponent(pathParts[i]);
|
|
31
|
-
}
|
|
32
|
-
else if (seg !== pathParts[i]) {
|
|
33
|
-
// Static segment mismatch
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
currentParams = captured;
|
|
38
|
-
return true;
|
|
9
|
+
export function setParams(params) {
|
|
10
|
+
currentParams = params;
|
|
39
11
|
}
|
|
40
12
|
/**
|
|
41
13
|
* Retrieve a URL parameter resolved for the current page.
|
package/dist/signals.d.ts
CHANGED
|
@@ -1,23 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reactive value container used by TinyFX signals.
|
|
3
|
-
*
|
|
4
|
-
* Reading the value through {@link Signal.get} during an active effect records
|
|
5
|
-
* that effect as a subscriber. Writing through {@link Signal.set} re-runs the
|
|
6
|
-
* affected subscribers.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* const count = new Signal(0);
|
|
10
|
-
* console.log(count.get());
|
|
11
|
-
* count.set(1);
|
|
12
|
-
* console.log(count.get());
|
|
13
|
-
*/
|
|
14
|
-
export declare class Signal<T> {
|
|
15
|
-
private value;
|
|
16
|
-
private subs;
|
|
17
|
-
constructor(v: T);
|
|
18
|
-
get(): T;
|
|
19
|
-
set(v: T): void;
|
|
20
|
-
}
|
|
21
1
|
/**
|
|
22
2
|
* Creates a reactive signal function.
|
|
23
3
|
*
|
package/dist/signals.js
CHANGED
|
@@ -1,38 +1,6 @@
|
|
|
1
1
|
// @tinyfx/runtime — signals
|
|
2
2
|
// Minimal, explicit reactivity — no proxies, no magic.
|
|
3
3
|
const effectStack = [];
|
|
4
|
-
/**
|
|
5
|
-
* Reactive value container used by TinyFX signals.
|
|
6
|
-
*
|
|
7
|
-
* Reading the value through {@link Signal.get} during an active effect records
|
|
8
|
-
* that effect as a subscriber. Writing through {@link Signal.set} re-runs the
|
|
9
|
-
* affected subscribers.
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* const count = new Signal(0);
|
|
13
|
-
* console.log(count.get());
|
|
14
|
-
* count.set(1);
|
|
15
|
-
* console.log(count.get());
|
|
16
|
-
*/
|
|
17
|
-
export class Signal {
|
|
18
|
-
constructor(v) {
|
|
19
|
-
this.subs = new Set();
|
|
20
|
-
this.value = v;
|
|
21
|
-
}
|
|
22
|
-
get() {
|
|
23
|
-
const running = effectStack[effectStack.length - 1];
|
|
24
|
-
if (running) {
|
|
25
|
-
this.subs.add(running);
|
|
26
|
-
}
|
|
27
|
-
return this.value;
|
|
28
|
-
}
|
|
29
|
-
set(v) {
|
|
30
|
-
if (Object.is(v, this.value))
|
|
31
|
-
return;
|
|
32
|
-
this.value = v;
|
|
33
|
-
this.subs.forEach((fn) => fn());
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
4
|
/**
|
|
37
5
|
* Creates a reactive signal function.
|
|
38
6
|
*
|
|
@@ -46,9 +14,20 @@ export class Signal {
|
|
|
46
14
|
* count.set(1);
|
|
47
15
|
*/
|
|
48
16
|
export function signal(v) {
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
fn
|
|
17
|
+
let value = v;
|
|
18
|
+
const subs = new Set();
|
|
19
|
+
const fn = () => {
|
|
20
|
+
const running = effectStack[effectStack.length - 1];
|
|
21
|
+
if (running)
|
|
22
|
+
subs.add(running);
|
|
23
|
+
return value;
|
|
24
|
+
};
|
|
25
|
+
fn.set = (next) => {
|
|
26
|
+
if (Object.is(next, value))
|
|
27
|
+
return;
|
|
28
|
+
value = next;
|
|
29
|
+
subs.forEach((s) => s());
|
|
30
|
+
};
|
|
52
31
|
return fn;
|
|
53
32
|
}
|
|
54
33
|
/**
|