@flow.os/router 0.0.1-dev.1771665310
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/index.ts +139 -0
- package/package.json +14 -0
package/index.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export const APP_ID = 'flow-app';
|
|
2
|
+
|
|
3
|
+
/** Contenitore dove il router monta la pagina corrente. In root: <App /> (da @flow.os/router). */
|
|
4
|
+
export function App(): HTMLDivElement {
|
|
5
|
+
const el = document.createElement('div');
|
|
6
|
+
el.id = APP_ID;
|
|
7
|
+
el.setAttribute('role', 'main');
|
|
8
|
+
return el;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type RouteModule = {
|
|
12
|
+
default: (() => Node) | ((params: Record<string, string>) => Node) | (() => string) | string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type RunOptions = {
|
|
16
|
+
/** Nodo (o factory) mostrato mentre la route lazy viene caricata. */
|
|
17
|
+
fallback?: Node | (() => Node);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function pathToPattern(key: string): string {
|
|
21
|
+
const afterRoutes = key.replace(/^.*[/\\]routes[/\\]?/i, '').replace(/\.tsx?$/i, '');
|
|
22
|
+
const s = afterRoutes.split(/[/\\]/).filter(Boolean).join('/');
|
|
23
|
+
if (s === 'index' || s === '') return '';
|
|
24
|
+
return s
|
|
25
|
+
.split('/')
|
|
26
|
+
.map((x) => (x.startsWith('[') && x.endsWith(']') ? `:${x.slice(1, -1)}` : x))
|
|
27
|
+
.join('/');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function match(pattern: string, path: string): Record<string, string> | null {
|
|
31
|
+
const pa = pattern ? pattern.split('/').filter(Boolean) : [];
|
|
32
|
+
const ph = path.replace(/^\//, '').split('/').filter(Boolean);
|
|
33
|
+
if (pa.length !== ph.length) return null;
|
|
34
|
+
const params: Record<string, string> = {};
|
|
35
|
+
for (let i = 0; i < pa.length; i++) {
|
|
36
|
+
if (pa[i]!.startsWith(':')) params[pa[i]!.slice(1)] = ph[i] ?? '';
|
|
37
|
+
else if (pa[i] !== ph[i]) return null;
|
|
38
|
+
}
|
|
39
|
+
return params;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function setContent(container: HTMLElement, content: Node | string): void {
|
|
43
|
+
container.innerHTML = '';
|
|
44
|
+
if (typeof content === 'string') container.textContent = content;
|
|
45
|
+
else container.appendChild(content);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function defaultNotFound(path: string): Node {
|
|
49
|
+
const wrap = document.createElement('div');
|
|
50
|
+
wrap.setAttribute('role', 'alert');
|
|
51
|
+
const h = document.createElement('h1');
|
|
52
|
+
h.textContent = '404';
|
|
53
|
+
const p = document.createElement('p');
|
|
54
|
+
p.textContent = `Pagina non trovata: ${path || '/'}`;
|
|
55
|
+
wrap.append(h, p);
|
|
56
|
+
return wrap;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createRouter(
|
|
60
|
+
modules: Record<string, () => Promise<RouteModule>>,
|
|
61
|
+
container: HTMLElement,
|
|
62
|
+
options: { fallback?: Node | (() => Node); notFound?: Node | ((path: string) => Node) } = {}
|
|
63
|
+
) {
|
|
64
|
+
const routes = Object.entries(modules).map(([key, loader]) => ({
|
|
65
|
+
pattern: pathToPattern(key),
|
|
66
|
+
loader,
|
|
67
|
+
}));
|
|
68
|
+
routes.sort(
|
|
69
|
+
(a, b) =>
|
|
70
|
+
b.pattern.split('/').filter(Boolean).length - a.pattern.split('/').filter(Boolean).length
|
|
71
|
+
);
|
|
72
|
+
const notFound = options.notFound ?? defaultNotFound;
|
|
73
|
+
|
|
74
|
+
async function render(pathname: string): Promise<boolean> {
|
|
75
|
+
const path = pathname === '/' ? '' : pathname.slice(1);
|
|
76
|
+
for (const { pattern, loader } of routes) {
|
|
77
|
+
const params = match(pattern, path);
|
|
78
|
+
if (params === null) continue;
|
|
79
|
+
const fallbackNode =
|
|
80
|
+
options.fallback != null
|
|
81
|
+
? typeof options.fallback === 'function'
|
|
82
|
+
? options.fallback()
|
|
83
|
+
: options.fallback
|
|
84
|
+
: null;
|
|
85
|
+
if (fallbackNode) setContent(container, fallbackNode);
|
|
86
|
+
const mod = await loader();
|
|
87
|
+
const def = mod.default;
|
|
88
|
+
if (typeof def === 'function') {
|
|
89
|
+
const out = def.length
|
|
90
|
+
? (def as (p: Record<string, string>) => Node)(params)
|
|
91
|
+
: (def as () => Node | string)();
|
|
92
|
+
if (out instanceof Node) setContent(container, out);
|
|
93
|
+
else if (typeof out === 'string') container.innerHTML = out;
|
|
94
|
+
else container.innerHTML = '';
|
|
95
|
+
} else setContent(container, typeof def === 'string' ? def : (def as unknown as Node));
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
const nfNode = typeof notFound === 'function' ? notFound(pathname) : notFound;
|
|
99
|
+
setContent(container, nfNode);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function go(path: string, replace = false): void {
|
|
104
|
+
const p = path.startsWith('/') ? path : `/${path}`;
|
|
105
|
+
render(p).then((ok) => {
|
|
106
|
+
if (ok) (replace ? history.replaceState : history.pushState).call(history, null, '', p);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function start(): void {
|
|
111
|
+
render(location.pathname || '/');
|
|
112
|
+
window.addEventListener('popstate', () => render(location.pathname || '/'));
|
|
113
|
+
document.addEventListener('click', (e) => {
|
|
114
|
+
const a = (e.target as Element).closest('a');
|
|
115
|
+
if (a?.getAttribute('href')?.startsWith('/') && !a.href.startsWith('//')) {
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
go(a.getAttribute('href')!);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { go, start };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Monta App e avvia il router. Da usare nell'entry virtuale. */
|
|
126
|
+
export function run(
|
|
127
|
+
App: () => Node,
|
|
128
|
+
modules: Record<string, () => Promise<RouteModule>>,
|
|
129
|
+
options?: RunOptions
|
|
130
|
+
): void {
|
|
131
|
+
const app = document.getElementById('app')!;
|
|
132
|
+
app.appendChild(App());
|
|
133
|
+
const appContainer = document.getElementById(APP_ID);
|
|
134
|
+
if (!appContainer)
|
|
135
|
+
throw new Error(
|
|
136
|
+
`#${APP_ID} not found: use <App /> from @flow.os/router in your root layout.`
|
|
137
|
+
);
|
|
138
|
+
createRouter(modules, appContainer as HTMLElement, options).start();
|
|
139
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flow.os/router",
|
|
3
|
+
"version": "0.0.1-dev.1771665310",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./index.ts",
|
|
6
|
+
"types": "./index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./index.ts",
|
|
10
|
+
"import": "./index.ts",
|
|
11
|
+
"default": "./index.ts"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|