create-flow-os 0.0.1-dev.1771614697 → 0.0.1-dev.1771615015
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/gen.ts +5 -2
- package/package.json +1 -1
- package/packages/core/client/routes/index.tsx +47 -0
- package/packages/router/client/root.tsx +16 -0
- package/profiles/client/client/routes/about.tsx +1 -1
- package/profiles/client/client/routes/index.tsx +1 -1
- package/profiles/client/package.json +8 -12
- package/profiles/client/packages/client/dom.ts +1 -1
- package/profiles/client/packages/client/features/attrs.ts +1 -1
- package/profiles/client/packages/client/features/class-flow.ts +1 -1
- package/profiles/client/packages/client/features/style-flow.ts +1 -1
- package/profiles/client/packages/client/features/style.ts +1 -1
- package/profiles/client/packages/client/features/viewport.ts +1 -1
- package/profiles/client/packages/client/jsx.ts +1 -1
- package/profiles/client/packages/client/package.json +1 -1
- package/profiles/client/packages/flow-os/dom.ts +43 -0
- package/profiles/client/packages/flow-os/for.ts +42 -0
- package/profiles/client/packages/flow-os/index.ts +5 -0
- package/profiles/client/packages/flow-os/lifecycle.ts +6 -0
- package/profiles/client/packages/flow-os/package.json +15 -0
- package/profiles/client/packages/flow-os/show-switch.ts +56 -0
- package/profiles/client/packages/flow-os/state.ts +56 -0
- package/profiles/client/packages/router/index.ts +139 -0
- package/profiles/client/packages/router/package.json +10 -0
- package/profiles/client/packages/style/breakpoints.ts +20 -0
- package/profiles/client/packages/style/extension/.vscodeignore +5 -0
- package/profiles/client/packages/style/extension/README.md +7 -0
- package/profiles/client/packages/style/extension/flow-style-colors-0.0.1.vsix +0 -0
- package/profiles/client/packages/style/extension/out/extension.js +130 -0
- package/profiles/client/packages/style/extension/package.json +24 -0
- package/profiles/client/packages/style/extension/src/extension.ts +146 -0
- package/profiles/client/packages/style/extension/tsconfig.json +12 -0
- package/profiles/client/packages/style/index.ts +16 -0
- package/profiles/client/packages/style/package.json +14 -0
- package/profiles/client/packages/style/resolve.ts +84 -0
- package/profiles/client/packages/style/shorthand.ts +62 -0
- package/profiles/client/tsconfig.json +1 -1
- package/profiles/full/.dockerignore +12 -0
- package/profiles/full/.oxfmtrc.json +7 -0
- package/profiles/full/.oxlintrc.json +13 -0
- package/profiles/full/.vscode/settings.json +12 -0
- package/profiles/full/Dockerfile +26 -0
- package/profiles/full/bun.lock +334 -0
- package/profiles/full/client/root.css +9 -0
- package/profiles/full/client/root.tsx +17 -0
- package/profiles/full/client/routes/about.tsx +22 -0
- package/profiles/full/client/routes/index.tsx +48 -0
- package/profiles/full/flow.config.ts +8 -0
- package/profiles/full/index.html +5 -0
- package/profiles/full/package.json +28 -0
- package/profiles/full/packages/client/config.ts +68 -0
- package/profiles/full/packages/client/dom.ts +5 -0
- package/profiles/full/packages/client/features/attrs.ts +32 -0
- package/profiles/full/packages/client/features/class-flow.ts +116 -0
- package/profiles/full/packages/client/features/index.ts +8 -0
- package/profiles/full/packages/client/features/pseudo-injector.ts +40 -0
- package/profiles/full/packages/client/features/style-flow.ts +106 -0
- package/profiles/full/packages/client/features/style.ts +27 -0
- package/profiles/full/packages/client/features/utils.ts +4 -0
- package/profiles/full/packages/client/features/viewport.ts +20 -0
- package/profiles/full/packages/client/index.ts +4 -0
- package/profiles/full/packages/client/jsx-dev-runtime.ts +1 -0
- package/profiles/full/packages/client/jsx-runtime.ts +1 -0
- package/profiles/full/packages/client/jsx-types.d.ts +64 -0
- package/profiles/full/packages/client/jsx.ts +99 -0
- package/profiles/full/packages/client/package.json +42 -0
- package/profiles/full/packages/client/vite.ts +42 -0
- package/profiles/full/packages/flow-os/dom.ts +43 -0
- package/profiles/full/packages/flow-os/for.ts +42 -0
- package/profiles/full/packages/flow-os/index.ts +5 -0
- package/profiles/full/packages/flow-os/lifecycle.ts +6 -0
- package/profiles/full/packages/flow-os/package.json +15 -0
- package/profiles/full/packages/flow-os/show-switch.ts +56 -0
- package/profiles/full/packages/flow-os/state.ts +56 -0
- package/profiles/full/packages/router/index.ts +139 -0
- package/profiles/full/packages/router/package.json +10 -0
- package/profiles/full/packages/server/core.ts +140 -0
- package/profiles/full/packages/server/index.ts +24 -0
- package/profiles/full/packages/server/package.json +29 -0
- package/profiles/full/packages/server/plugin.ts +79 -0
- package/profiles/full/packages/server/production.ts +75 -0
- package/profiles/full/packages/server/start.ts +15 -0
- package/profiles/full/packages/style/breakpoints.ts +20 -0
- package/profiles/full/packages/style/extension/.vscodeignore +5 -0
- package/profiles/full/packages/style/extension/README.md +7 -0
- package/profiles/full/packages/style/extension/flow-style-colors-0.0.1.vsix +0 -0
- package/profiles/full/packages/style/extension/out/extension.js +130 -0
- package/profiles/full/packages/style/extension/package.json +24 -0
- package/profiles/full/packages/style/extension/src/extension.ts +146 -0
- package/profiles/full/packages/style/extension/tsconfig.json +12 -0
- package/profiles/full/packages/style/index.ts +16 -0
- package/profiles/full/packages/style/package.json +14 -0
- package/profiles/full/packages/style/resolve.ts +84 -0
- package/profiles/full/packages/style/shorthand.ts +62 -0
- package/profiles/full/server/routes/hello.get.ts +3 -0
- package/profiles/full/tsconfig.json +30 -0
- package/profiles/server/.dockerignore +12 -0
- package/profiles/server/.oxfmtrc.json +7 -0
- package/profiles/server/.oxlintrc.json +13 -0
- package/profiles/server/.vscode/settings.json +12 -0
- package/profiles/server/Dockerfile +26 -0
- package/profiles/server/bun.lock +334 -0
- package/profiles/server/flow.config.ts +8 -0
- package/profiles/server/index.html +5 -0
- package/profiles/server/package.json +28 -0
- package/profiles/server/packages/flow-os/dom.ts +43 -0
- package/profiles/server/packages/flow-os/for.ts +42 -0
- package/profiles/server/packages/flow-os/index.ts +5 -0
- package/profiles/server/packages/flow-os/lifecycle.ts +6 -0
- package/profiles/server/packages/flow-os/package.json +15 -0
- package/profiles/server/packages/flow-os/show-switch.ts +56 -0
- package/profiles/server/packages/flow-os/state.ts +56 -0
- package/profiles/server/packages/router/index.ts +139 -0
- package/profiles/server/packages/router/package.json +10 -0
- package/profiles/server/packages/server/core.ts +140 -0
- package/profiles/server/packages/server/index.ts +24 -0
- package/profiles/server/packages/server/package.json +29 -0
- package/profiles/server/packages/server/plugin.ts +79 -0
- package/profiles/server/packages/server/production.ts +75 -0
- package/profiles/server/packages/server/start.ts +15 -0
- package/profiles/server/packages/style/breakpoints.ts +20 -0
- package/profiles/server/packages/style/extension/.vscodeignore +5 -0
- package/profiles/server/packages/style/extension/README.md +7 -0
- package/profiles/server/packages/style/extension/flow-style-colors-0.0.1.vsix +0 -0
- package/profiles/server/packages/style/extension/out/extension.js +130 -0
- package/profiles/server/packages/style/extension/package.json +24 -0
- package/profiles/server/packages/style/extension/src/extension.ts +146 -0
- package/profiles/server/packages/style/extension/tsconfig.json +12 -0
- package/profiles/server/packages/style/index.ts +16 -0
- package/profiles/server/packages/style/package.json +14 -0
- package/profiles/server/packages/style/resolve.ts +84 -0
- package/profiles/server/packages/style/shorthand.ts +62 -0
- package/profiles/server/server/routes/hello.get.ts +3 -0
- package/profiles/server/tsconfig.json +30 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flow-os/core",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./index.ts",
|
|
6
|
+
"types": "./index.ts",
|
|
7
|
+
"dependencies": {},
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.ts",
|
|
11
|
+
"import": "./index.ts",
|
|
12
|
+
"default": "./index.ts"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Show: rendering condizionale (when/fallback).
|
|
3
|
+
* Switch: primo branch con when() truthy, altrimenti fallback.
|
|
4
|
+
*/
|
|
5
|
+
import { effect } from './state.js';
|
|
6
|
+
import { setContent } from './dom.js';
|
|
7
|
+
|
|
8
|
+
function isGetter(fn: unknown): fn is () => unknown {
|
|
9
|
+
return typeof fn === 'function' && fn.length === 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Show: quando when() è truthy mostra children, altrimenti fallback. */
|
|
13
|
+
export function Show(props: {
|
|
14
|
+
when: (() => unknown) | unknown;
|
|
15
|
+
fallback?: unknown;
|
|
16
|
+
children?: unknown;
|
|
17
|
+
}): HTMLElement {
|
|
18
|
+
const wrapper = document.createElement('span');
|
|
19
|
+
wrapper.style.display = 'contents';
|
|
20
|
+
const getWhen = isGetter(props.when) ? props.when : () => props.when;
|
|
21
|
+
effect(() => {
|
|
22
|
+
setContent(wrapper, getWhen() ? props.children : props.fallback);
|
|
23
|
+
});
|
|
24
|
+
return wrapper;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type MatchBranch = { when: () => boolean; children: unknown };
|
|
28
|
+
|
|
29
|
+
/** Switch: primo branch con when() truthy, altrimenti fallback. branches = [{ when, children }, ...]. */
|
|
30
|
+
export function Switch(props: {
|
|
31
|
+
fallback?: unknown;
|
|
32
|
+
branches: MatchBranch[];
|
|
33
|
+
}): HTMLElement {
|
|
34
|
+
const wrapper = document.createElement('span');
|
|
35
|
+
wrapper.style.display = 'contents';
|
|
36
|
+
effect(() => {
|
|
37
|
+
const list = props.branches;
|
|
38
|
+
for (let i = 0; i < list.length; i++) {
|
|
39
|
+
const b = list[i];
|
|
40
|
+
if (b === undefined) continue;
|
|
41
|
+
const cond = isGetter(b.when) ? b.when() : b.when;
|
|
42
|
+
if (cond) {
|
|
43
|
+
setContent(wrapper, b.children);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
setContent(wrapper, props.fallback);
|
|
48
|
+
});
|
|
49
|
+
return wrapper;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Helper per costruire i branch di Switch: Match({ when: () => cond, children: <Node/> }) => branch. */
|
|
53
|
+
export function Match(props: { when: (() => boolean) | boolean; children?: unknown }): MatchBranch {
|
|
54
|
+
const whenFn = isGetter(props.when) ? props.when : () => Boolean(props.when);
|
|
55
|
+
return { when: whenFn, children: props.children };
|
|
56
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
type Subscriber = () => void;
|
|
2
|
+
type SubscriberSet = Set<Subscriber>;
|
|
3
|
+
|
|
4
|
+
interface EffectRecord {
|
|
5
|
+
run: () => void;
|
|
6
|
+
deps: Set<SubscriberSet>;
|
|
7
|
+
cleanups: Array<() => void>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let currentEffect: EffectRecord | null = null;
|
|
11
|
+
|
|
12
|
+
export function state<T>(initialValue: T): [() => T, (value: T | ((prev: T) => T)) => void] {
|
|
13
|
+
let value = initialValue;
|
|
14
|
+
const subscribers: SubscriberSet = new Set();
|
|
15
|
+
|
|
16
|
+
function get(): T {
|
|
17
|
+
if (currentEffect !== null) {
|
|
18
|
+
subscribers.add(currentEffect.run);
|
|
19
|
+
currentEffect.deps.add(subscribers);
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function set(next: T | ((prev: T) => T)): void {
|
|
25
|
+
const nextValue = typeof next === 'function' ? (next as (prev: T) => T)(value) : next;
|
|
26
|
+
if (Object.is(value, nextValue)) return;
|
|
27
|
+
value = nextValue;
|
|
28
|
+
const copy = Array.from(subscribers);
|
|
29
|
+
copy.forEach((fn) => fn());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return [get, set];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function effect(fn: Subscriber): void {
|
|
36
|
+
const deps = new Set<SubscriberSet>();
|
|
37
|
+
const cleanups: Array<() => void> = [];
|
|
38
|
+
const run: Subscriber = () => {
|
|
39
|
+
for (const fn of cleanups) fn();
|
|
40
|
+
cleanups.length = 0;
|
|
41
|
+
deps.forEach((subs) => subs.delete(run));
|
|
42
|
+
deps.clear();
|
|
43
|
+
const prev = currentEffect;
|
|
44
|
+
currentEffect = { run, deps, cleanups };
|
|
45
|
+
try {
|
|
46
|
+
fn();
|
|
47
|
+
} finally {
|
|
48
|
+
currentEffect = prev;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
run();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function onCleanup(fn: () => void): void {
|
|
55
|
+
if (currentEffect !== null) currentEffect.cleanups.push(fn);
|
|
56
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { readdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import type { FlowHandler, FlowRequest } from './index.js';
|
|
5
|
+
|
|
6
|
+
const METHOD_SUFFIXES = [
|
|
7
|
+
'.get',
|
|
8
|
+
'.post',
|
|
9
|
+
'.put',
|
|
10
|
+
'.delete',
|
|
11
|
+
'.patch',
|
|
12
|
+
'.head',
|
|
13
|
+
'.options',
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
export type RouteMeta = { path: string; method: string; filePath: string };
|
|
17
|
+
|
|
18
|
+
function fileSegmentToRoute(segment: string): string {
|
|
19
|
+
return segment.replace(/^\[([^\]]+)\]$/, ':$1');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseRoutePath(relativePath: string): { path: string; method: string } | null {
|
|
23
|
+
const lower = relativePath.toLowerCase();
|
|
24
|
+
for (const suffix of METHOD_SUFFIXES) {
|
|
25
|
+
const ext = lower.endsWith('.tsx') ? '.tsx' : '.ts';
|
|
26
|
+
if (lower.endsWith(suffix + ext)) {
|
|
27
|
+
const withoutExt = relativePath.slice(0, -(suffix.length + ext.length));
|
|
28
|
+
const segments = withoutExt.split(/[/\\]/).filter(Boolean);
|
|
29
|
+
const routeSegments = segments.map((s) => fileSegmentToRoute(s));
|
|
30
|
+
const path = '/' + routeSegments.join('/');
|
|
31
|
+
const pathNorm = path === '/index' ? '/' : path;
|
|
32
|
+
const method = suffix.slice(1).toUpperCase();
|
|
33
|
+
return { path: pathNorm, method };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function walkRoutes(dir: string, base = ''): RouteMeta[] {
|
|
40
|
+
let entries: Array<{ name: string; isDirectory(): boolean; isFile(): boolean }>;
|
|
41
|
+
try {
|
|
42
|
+
entries = readdirSync(dir, { withFileTypes: true }) as Array<{
|
|
43
|
+
name: string;
|
|
44
|
+
isDirectory(): boolean;
|
|
45
|
+
isFile(): boolean;
|
|
46
|
+
}>;
|
|
47
|
+
} catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
const routes: RouteMeta[] = [];
|
|
51
|
+
for (const e of entries) {
|
|
52
|
+
const name = e.name;
|
|
53
|
+
const rel = base ? `${base}/${name}` : name;
|
|
54
|
+
if (e.isDirectory()) {
|
|
55
|
+
routes.push(...walkRoutes(join(dir, name), rel));
|
|
56
|
+
} else if (e.isFile() && (name.endsWith('.ts') || name.endsWith('.tsx'))) {
|
|
57
|
+
const parsed = parseRoutePath(rel);
|
|
58
|
+
if (parsed) routes.push({ ...parsed, filePath: join(dir, name) });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return routes;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function routePatternToRegex(path: string): { re: RegExp; keys: string[] } {
|
|
65
|
+
const keys: string[] = [];
|
|
66
|
+
const pattern =
|
|
67
|
+
'^' +
|
|
68
|
+
path.replace(/:(\w+)/g, (_, key) => {
|
|
69
|
+
keys.push(key);
|
|
70
|
+
return '([^/]+)';
|
|
71
|
+
}) +
|
|
72
|
+
'$';
|
|
73
|
+
return { re: new RegExp(pattern), keys };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type LoadHandler = (filePath: string) => Promise<{ default: FlowHandler }>;
|
|
77
|
+
|
|
78
|
+
type RouteEntry = {
|
|
79
|
+
method: string;
|
|
80
|
+
re: RegExp;
|
|
81
|
+
keys: string[];
|
|
82
|
+
handler: FlowHandler;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export async function createFlowServer(
|
|
86
|
+
routesDir: string,
|
|
87
|
+
loadHandler: LoadHandler
|
|
88
|
+
): Promise<{
|
|
89
|
+
dispatch: (pathname: string, method: string, request: Request) => Promise<Response | null>;
|
|
90
|
+
matches: (pathname: string, method: string) => boolean;
|
|
91
|
+
}> {
|
|
92
|
+
const routeList = walkRoutes(routesDir);
|
|
93
|
+
const routes: RouteEntry[] = [];
|
|
94
|
+
|
|
95
|
+
for (const r of routeList) {
|
|
96
|
+
const mod = await loadHandler(r.filePath);
|
|
97
|
+
const handler = mod.default;
|
|
98
|
+
if (!handler) continue;
|
|
99
|
+
const { re, keys } = routePatternToRegex(r.path);
|
|
100
|
+
routes.push({ method: r.method, re, keys, handler });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getMatch(
|
|
104
|
+
pathname: string,
|
|
105
|
+
method: string
|
|
106
|
+
): { handler: FlowHandler; params: Record<string, string> } | null {
|
|
107
|
+
const norm = pathname.split('?')[0] || '/';
|
|
108
|
+
for (const route of routes) {
|
|
109
|
+
if (route.method !== method) continue;
|
|
110
|
+
const m = route.re.exec(norm);
|
|
111
|
+
if (!m) continue;
|
|
112
|
+
const params: Record<string, string> = {};
|
|
113
|
+
route.keys.forEach((k, i) => (params[k] = m[i + 1] ?? ''));
|
|
114
|
+
return { handler: route.handler, params };
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function dispatch(
|
|
120
|
+
pathname: string,
|
|
121
|
+
method: string,
|
|
122
|
+
request: Request
|
|
123
|
+
): Promise<Response | null> {
|
|
124
|
+
const match = getMatch(pathname, method);
|
|
125
|
+
if (!match) return null;
|
|
126
|
+
const flowReq = request as FlowRequest;
|
|
127
|
+
flowReq.params = match.params;
|
|
128
|
+
const out = await match.handler(flowReq);
|
|
129
|
+
if (out instanceof Response) return out;
|
|
130
|
+
return new Response(JSON.stringify(out), {
|
|
131
|
+
headers: { 'Content-Type': 'application/json' },
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function matches(pathname: string, method: string): boolean {
|
|
136
|
+
return getMatch(pathname, method) !== null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { dispatch, matches };
|
|
140
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API native: Request/Response (Bun/Web). Zero dipendenze.
|
|
3
|
+
* Per route dinamiche usa request.params (iniettato dal router).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { flowServer } from './plugin.js';
|
|
7
|
+
export type FlowRequest = Request & { params: Record<string, string> };
|
|
8
|
+
|
|
9
|
+
export type FlowHandler = (
|
|
10
|
+
request: FlowRequest
|
|
11
|
+
) => Response | Promise<Response> | object | Promise<object>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Wrapper per gli handler: ritorna oggetti come JSON, Response come sono.
|
|
15
|
+
*/
|
|
16
|
+
export function defineHandler(fn: FlowHandler): FlowHandler {
|
|
17
|
+
return async (request: FlowRequest) => {
|
|
18
|
+
const out = await fn(request);
|
|
19
|
+
if (out instanceof Response) return out;
|
|
20
|
+
return new Response(JSON.stringify(out), {
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flow-os/server",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./index.ts",
|
|
6
|
+
"types": "./index.ts",
|
|
7
|
+
"bin": "./start.ts",
|
|
8
|
+
"dependencies": {},
|
|
9
|
+
"peerDependencies": {
|
|
10
|
+
"vite": ">=5.0.0"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./index.ts",
|
|
15
|
+
"import": "./index.ts",
|
|
16
|
+
"default": "./index.ts"
|
|
17
|
+
},
|
|
18
|
+
"./vite": {
|
|
19
|
+
"types": "./plugin.ts",
|
|
20
|
+
"import": "./plugin.ts",
|
|
21
|
+
"default": "./plugin.ts"
|
|
22
|
+
},
|
|
23
|
+
"./production": {
|
|
24
|
+
"types": "./production.ts",
|
|
25
|
+
"import": "./production.ts",
|
|
26
|
+
"default": "./production.ts"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
import type { Plugin } from 'vite';
|
|
5
|
+
import { createFlowServer, type LoadHandler } from './core.js';
|
|
6
|
+
import type { FlowHandler } from './index.js';
|
|
7
|
+
|
|
8
|
+
type NodeReq = import('http').IncomingMessage;
|
|
9
|
+
type NodeRes = import('http').ServerResponse;
|
|
10
|
+
|
|
11
|
+
function nodeRequestToWebRequest(req: NodeReq, baseUrl: string): Request {
|
|
12
|
+
const url = (req.url ?? '/').startsWith('http') ? req.url! : baseUrl + (req.url ?? '/');
|
|
13
|
+
const opts: RequestInit = {
|
|
14
|
+
method: req.method ?? 'GET',
|
|
15
|
+
headers: req.headers as HeadersInit,
|
|
16
|
+
};
|
|
17
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
18
|
+
(opts as RequestInit & { duplex?: string }).body = req as unknown as BodyInit;
|
|
19
|
+
(opts as RequestInit & { duplex?: string }).duplex = 'half';
|
|
20
|
+
}
|
|
21
|
+
return new Request(url, opts);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function webResponseToNodeResponse(webRes: Response, res: NodeRes): Promise<void> {
|
|
25
|
+
res.writeHead(webRes.status, Object.fromEntries(webRes.headers));
|
|
26
|
+
if (webRes.body) {
|
|
27
|
+
const buf = Buffer.from(await webRes.arrayBuffer());
|
|
28
|
+
res.end(buf);
|
|
29
|
+
} else {
|
|
30
|
+
res.end();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function flowServerMiddleware(
|
|
35
|
+
dispatch: (pathname: string, method: string, request: Request) => Promise<Response | null>,
|
|
36
|
+
matches: (pathname: string, method: string) => boolean
|
|
37
|
+
): (req: NodeReq, res: NodeRes, next: () => void) => void {
|
|
38
|
+
return (req, res, next) => {
|
|
39
|
+
const pathname = req.url?.split('?')[0] ?? '/';
|
|
40
|
+
const method = req.method ?? 'GET';
|
|
41
|
+
if (!matches(pathname, method)) return next();
|
|
42
|
+
const baseUrl = `http://${req.headers.host ?? 'localhost'}`;
|
|
43
|
+
const request = nodeRequestToWebRequest(req, baseUrl);
|
|
44
|
+
dispatch(pathname, method, request)
|
|
45
|
+
.then((response) => {
|
|
46
|
+
if (response) return webResponseToNodeResponse(response, res);
|
|
47
|
+
next();
|
|
48
|
+
})
|
|
49
|
+
.catch((err) => {
|
|
50
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
51
|
+
res.end(String(err?.message ?? err));
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type FlowServerOptions = {
|
|
57
|
+
/** Cartella route API (file-based). Default: server/routes */
|
|
58
|
+
routesDir?: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type ConnectStack = Array<{ route: string; handle: (req: NodeReq, res: NodeRes, next: () => void) => void }>;
|
|
62
|
+
|
|
63
|
+
export function flowServer(options?: FlowServerOptions): Plugin {
|
|
64
|
+
const routesDir = options?.routesDir ?? resolve(process.cwd(), 'server', 'routes');
|
|
65
|
+
return {
|
|
66
|
+
name: 'flow-server',
|
|
67
|
+
enforce: 'pre',
|
|
68
|
+
configureServer: async (server) => {
|
|
69
|
+
const loadHandler: LoadHandler = (filePath) =>
|
|
70
|
+
server.ssrLoadModule(pathToFileURL(filePath).href) as Promise<{ default: FlowHandler }>;
|
|
71
|
+
const { dispatch, matches } = await createFlowServer(routesDir, loadHandler);
|
|
72
|
+
const middleware = flowServerMiddleware(dispatch, matches);
|
|
73
|
+
(server.middlewares as unknown as { stack: ConnectStack }).stack.unshift({
|
|
74
|
+
route: '',
|
|
75
|
+
handle: middleware,
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { join, resolve } from 'path';
|
|
2
|
+
import { pathToFileURL } from 'url';
|
|
3
|
+
import { createFlowServer } from './core.js';
|
|
4
|
+
|
|
5
|
+
declare const Bun: {
|
|
6
|
+
file(path: string): { exists(): Promise<boolean> };
|
|
7
|
+
serve(options: { port: number; hostname: string; fetch(req: Request): Promise<Response> }): { hostname: string; port: number };
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type ProductionServerOptions = {
|
|
11
|
+
/** Cartella route (es. server/routes). Default: server/routes */
|
|
12
|
+
routesDir?: string;
|
|
13
|
+
/** Cartella statici (es. dist). Default: dist */
|
|
14
|
+
staticDir?: string;
|
|
15
|
+
/** Porta. Default: 3000 */
|
|
16
|
+
port?: number;
|
|
17
|
+
/** Hostname. Default: 0.0.0.0 */
|
|
18
|
+
hostname?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Serve un file dalla cartella staticDir. pathname = / -> index.html.
|
|
23
|
+
* Se il file non esiste (SPA client-side routing) ritorna index.html.
|
|
24
|
+
*/
|
|
25
|
+
async function serveStatic(staticDir: string, pathname: string): Promise<Response> {
|
|
26
|
+
const path = pathname === '/' ? '/index.html' : pathname;
|
|
27
|
+
const filePath = join(staticDir, path.replace(/^\//, ''));
|
|
28
|
+
try {
|
|
29
|
+
const file = Bun.file(filePath);
|
|
30
|
+
if (await file.exists()) {
|
|
31
|
+
return new Response(file as unknown as BodyInit, { headers: { 'Cache-Control': 'public, max-age=0' } });
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// ignore
|
|
35
|
+
}
|
|
36
|
+
// SPA fallback: qualsiasi path non trovato → index.html
|
|
37
|
+
const indexFile = Bun.file(join(staticDir, 'index.html'));
|
|
38
|
+
if (await indexFile.exists()) return new Response(indexFile as unknown as BodyInit);
|
|
39
|
+
return new Response('Not Found', { status: 404 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Avvia il server di produzione: API da routesDir + statici da staticDir.
|
|
44
|
+
* Usa Bun.serve() (solo Bun).
|
|
45
|
+
*/
|
|
46
|
+
export async function runProductionServer(options: ProductionServerOptions = {}): Promise<void> {
|
|
47
|
+
const cwd = process.cwd();
|
|
48
|
+
const routesDir = resolve(cwd, options.routesDir ?? 'server/routes');
|
|
49
|
+
const staticDir = resolve(cwd, options.staticDir ?? 'dist');
|
|
50
|
+
const port = options.port ?? 3000;
|
|
51
|
+
const hostname = options.hostname ?? '0.0.0.0';
|
|
52
|
+
|
|
53
|
+
const loadHandler = async (filePath: string) => {
|
|
54
|
+
return import(pathToFileURL(filePath).href) as Promise<{ default: import('./index.js').FlowHandler }>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const { dispatch, matches } = await createFlowServer(routesDir, loadHandler);
|
|
58
|
+
|
|
59
|
+
const server = Bun.serve({
|
|
60
|
+
port,
|
|
61
|
+
hostname,
|
|
62
|
+
async fetch(req: Request) {
|
|
63
|
+
const url = new URL(req.url);
|
|
64
|
+
const pathname = url.pathname;
|
|
65
|
+
const method = req.method;
|
|
66
|
+
|
|
67
|
+
const apiRes = await dispatch(pathname, method, req);
|
|
68
|
+
if (apiRes) return apiRes;
|
|
69
|
+
|
|
70
|
+
return serveStatic(staticDir, pathname);
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
console.log(`Server: http://${server.hostname}:${server.port}`);
|
|
75
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Entry produzione: lancia il server (statici + API).
|
|
4
|
+
* Uso: bunx @flow-os/server oppure "start": "bunx @flow-os/server" in package.json.
|
|
5
|
+
*/
|
|
6
|
+
import { runProductionServer } from './production.js';
|
|
7
|
+
|
|
8
|
+
const port = process.env['PORT'] ? Number(process.env['PORT']) : 3000;
|
|
9
|
+
|
|
10
|
+
runProductionServer({
|
|
11
|
+
routesDir: 'server/routes',
|
|
12
|
+
staticDir: 'dist',
|
|
13
|
+
port,
|
|
14
|
+
hostname: '0.0.0.0',
|
|
15
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Chiavi viewport: ordine da piccolo a grande (merge sovrascrive). */
|
|
2
|
+
export const VIEWPORT_KEYS = ['mob', 'tab', 'des'] as const;
|
|
3
|
+
export type ViewportKey = (typeof VIEWPORT_KEYS)[number];
|
|
4
|
+
|
|
5
|
+
/** Chiavi pseudo-stato (hover, active, focus). */
|
|
6
|
+
export const PSEUDO_KEYS = ['hover', 'active', 'focus', 'focusVisible'] as const;
|
|
7
|
+
export type PseudoKey = (typeof PSEUDO_KEYS)[number];
|
|
8
|
+
|
|
9
|
+
/** Larghezze min (px) per viewport. mob < 640, tab 640–1023, des 1024+. */
|
|
10
|
+
export const VIEWPORT_WIDTHS: Record<ViewportKey, number> = {
|
|
11
|
+
mob: 0,
|
|
12
|
+
tab: 640,
|
|
13
|
+
des: 1024,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function getViewportKeyFromWidth(w: number): ViewportKey {
|
|
17
|
+
if (w >= 1024) return 'des';
|
|
18
|
+
if (w >= 640) return 'tab';
|
|
19
|
+
return 'mob';
|
|
20
|
+
}
|