create-flow-os 0.0.1-dev.1771783145 → 0.0.1-dev.1771840262

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.
Files changed (93) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/packages/client/client/root.tsx +1 -8
  4. package/packages/client/client/routes/index.tsx +8 -7
  5. package/profiles/client/bun.lock +30 -13
  6. package/profiles/client/client/root.tsx +1 -8
  7. package/profiles/client/client/routes/about.tsx +7 -0
  8. package/profiles/client/client/routes/index.tsx +8 -7
  9. package/profiles/client/flow.config.ts +4 -1
  10. package/profiles/client/package.json +3 -0
  11. package/profiles/client/packages/client/build/config.ts +67 -22
  12. package/profiles/client/packages/client/build/vite.ts +52 -8
  13. package/profiles/client/packages/client/features/attrs.ts +1 -1
  14. package/profiles/client/packages/client/index.ts +2 -1
  15. package/profiles/client/packages/client/package.json +3 -3
  16. package/profiles/client/packages/client/runtime/jsx-types.d.ts +2 -0
  17. package/profiles/client/packages/client/runtime/jsx.ts +4 -2
  18. package/profiles/client/packages/router/index.ts +14 -168
  19. package/profiles/client/packages/router/matcher.ts +105 -0
  20. package/profiles/client/packages/router/router.ts +185 -0
  21. package/profiles/client/packages/router/types.ts +34 -0
  22. package/profiles/client/packages/router/utils.ts +45 -0
  23. package/profiles/client/packages/style/index.ts +29 -15
  24. package/profiles/client/packages/style/package.json +6 -1
  25. package/profiles/client/packages/style/react.ts +68 -0
  26. package/profiles/client/packages/style/resolve.ts +44 -1
  27. package/profiles/client/packages/style/server.ts +299 -0
  28. package/profiles/client/packages/style/shorthand.ts +34 -0
  29. package/profiles/client/packages/style/style-builder/button.ts +41 -0
  30. package/profiles/client/packages/style/style-builder/constants.ts +16 -0
  31. package/profiles/client/packages/style/style-builder/dom.ts +18 -0
  32. package/profiles/client/packages/style/style-builder/index.ts +48 -0
  33. package/profiles/client/packages/style/style-builder/panel.ts +69 -0
  34. package/profiles/client/packages/style/style-builder/position.ts +25 -0
  35. package/profiles/client/packages/style/visual-builder.ts +822 -0
  36. package/profiles/client/packages/style/vite-plugin.ts +86 -0
  37. package/profiles/full/bun.lock +30 -13
  38. package/profiles/full/client/root.tsx +1 -8
  39. package/profiles/full/client/routes/about.tsx +7 -0
  40. package/profiles/full/client/routes/index.tsx +8 -7
  41. package/profiles/full/flow.config.ts +2 -4
  42. package/profiles/full/package.json +3 -0
  43. package/profiles/full/packages/client/build/config.ts +67 -22
  44. package/profiles/full/packages/client/build/vite.ts +52 -8
  45. package/profiles/full/packages/client/features/attrs.ts +1 -1
  46. package/profiles/full/packages/client/index.ts +2 -1
  47. package/profiles/full/packages/client/package.json +3 -3
  48. package/profiles/full/packages/client/runtime/jsx-types.d.ts +2 -0
  49. package/profiles/full/packages/client/runtime/jsx.ts +4 -2
  50. package/profiles/full/packages/router/index.ts +14 -168
  51. package/profiles/full/packages/router/matcher.ts +105 -0
  52. package/profiles/full/packages/router/router.ts +185 -0
  53. package/profiles/full/packages/router/types.ts +34 -0
  54. package/profiles/full/packages/router/utils.ts +45 -0
  55. package/profiles/full/packages/style/index.ts +29 -15
  56. package/profiles/full/packages/style/package.json +6 -1
  57. package/profiles/full/packages/style/react.ts +68 -0
  58. package/profiles/full/packages/style/resolve.ts +44 -1
  59. package/profiles/full/packages/style/server.ts +299 -0
  60. package/profiles/full/packages/style/shorthand.ts +34 -0
  61. package/profiles/full/packages/style/style-builder/button.ts +41 -0
  62. package/profiles/full/packages/style/style-builder/constants.ts +16 -0
  63. package/profiles/full/packages/style/style-builder/dom.ts +18 -0
  64. package/profiles/full/packages/style/style-builder/index.ts +48 -0
  65. package/profiles/full/packages/style/style-builder/panel.ts +69 -0
  66. package/profiles/full/packages/style/style-builder/position.ts +25 -0
  67. package/profiles/full/packages/style/visual-builder.ts +822 -0
  68. package/profiles/full/packages/style/vite-plugin.ts +86 -0
  69. package/profiles/server/bun.lock +30 -13
  70. package/profiles/server/flow.config.ts +4 -3
  71. package/profiles/server/package.json +3 -0
  72. package/profiles/server/packages/router/index.ts +14 -168
  73. package/profiles/server/packages/router/matcher.ts +105 -0
  74. package/profiles/server/packages/router/router.ts +185 -0
  75. package/profiles/server/packages/router/types.ts +34 -0
  76. package/profiles/server/packages/router/utils.ts +45 -0
  77. package/profiles/server/packages/style/index.ts +29 -15
  78. package/profiles/server/packages/style/package.json +6 -1
  79. package/profiles/server/packages/style/react.ts +68 -0
  80. package/profiles/server/packages/style/resolve.ts +44 -1
  81. package/profiles/server/packages/style/server.ts +299 -0
  82. package/profiles/server/packages/style/shorthand.ts +34 -0
  83. package/profiles/server/packages/style/style-builder/button.ts +41 -0
  84. package/profiles/server/packages/style/style-builder/constants.ts +16 -0
  85. package/profiles/server/packages/style/style-builder/dom.ts +18 -0
  86. package/profiles/server/packages/style/style-builder/index.ts +48 -0
  87. package/profiles/server/packages/style/style-builder/panel.ts +69 -0
  88. package/profiles/server/packages/style/style-builder/position.ts +25 -0
  89. package/profiles/server/packages/style/visual-builder.ts +822 -0
  90. package/profiles/server/packages/style/vite-plugin.ts +86 -0
  91. package/templates/flow.config.client.ts +4 -1
  92. package/templates/flow.config.full.ts +6 -0
  93. package/templates/flow.config.server.ts +4 -3
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Plugin Vite: avvia il server di patch, pagina builder (iframe), transform per data-flow-style-line.
3
+ */
4
+ import type { Plugin } from 'vite';
5
+ import { writeFileSync } from 'node:fs';
6
+ import path from 'node:path';
7
+ import { createStyleFlowServer } from './server.js';
8
+ import type { StyleFlowServerOptions } from './server.js';
9
+
10
+ const PATCH_PORT = 3757;
11
+ let pluginRoot = '';
12
+ const BUILDER_ENTRY_ID = '\0flowStyleBuilder-entry';
13
+
14
+ const BUILDER_HTML = `<!DOCTYPE html>
15
+ <html lang="it">
16
+ <head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><title>Style Builder</title>
17
+ <style>html,body{margin:0;padding:0;height:100%;overflow:hidden}</style></head>
18
+ <body><script type="module" src="/__flowStyleBuilder-entry"></script></body>
19
+ </html>`;
20
+
21
+ const BUILDER_ENTRY_CODE = `import { initFlowStyleBuilder } from '@flow.os/style';
22
+ const iframe = document.createElement('iframe');
23
+ iframe.src = '/';
24
+ iframe.setAttribute('data-flow-preview', '1');
25
+ iframe.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100vh;border:0;z-index:1';
26
+ document.body.appendChild(iframe);
27
+ initFlowStyleBuilder({ previewIframe: iframe });
28
+ `;
29
+
30
+ export type StyleFlowServerPluginOptions = StyleFlowServerOptions;
31
+
32
+ export function styleFlowServerPlugin(options: StyleFlowServerPluginOptions = {}): Plugin {
33
+ return {
34
+ name: 'flowStyleBuilder-server',
35
+ apply: 'serve',
36
+ configResolved(config) {
37
+ pluginRoot = config.root ?? process.cwd();
38
+ writeFileSync(path.resolve(pluginRoot, 'builder.html'), BUILDER_HTML);
39
+ },
40
+ resolveId(id) {
41
+ if (id === '/__flowStyleBuilder-entry') return BUILDER_ENTRY_ID;
42
+ return null;
43
+ },
44
+ load(id) {
45
+ if (id === BUILDER_ENTRY_ID) return BUILDER_ENTRY_CODE;
46
+ return null;
47
+ },
48
+ transform(code, id) {
49
+ if (!id.endsWith('.tsx') && !id.endsWith('.jsx')) return null;
50
+ const relativeFile = path.relative(pluginRoot || process.cwd(), id).replace(/\\/g, '/');
51
+ const out = code.replace(/\s(s|styleFlow)\s*=\s*\{/g, (match, _p1, offset) => {
52
+ const line = code.slice(0, offset).split(/\r?\n/).length;
53
+ return ` data-flow-style-line="${line}" data-flow-style-file="${relativeFile}" ${match.trim()}`;
54
+ });
55
+ return out === code ? null : { code: out, map: null };
56
+ },
57
+ configureServer(server) {
58
+ const root = server.config.root ?? process.cwd();
59
+ (server as { __flowStyleServerUrl?: string }).__flowStyleServerUrl = `http://127.0.0.1:${PATCH_PORT}`;
60
+ const { server: patchServer, port } = createStyleFlowServer({ ...options, root, port: PATCH_PORT });
61
+ patchServer.listen(port, '127.0.0.1');
62
+ // Servi /builder.html per primo (prima del SPA fallback di Vite)
63
+ const handleBuilder = (
64
+ req: import('node:http').IncomingMessage,
65
+ res: import('node:http').ServerResponse,
66
+ next: () => void
67
+ ) => {
68
+ const url = req.url ?? '';
69
+ if (url === '/builder.html' || url.startsWith('/builder.html?')) {
70
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
71
+ res.end(BUILDER_HTML);
72
+ return;
73
+ }
74
+ next();
75
+ };
76
+ const app = server.middlewares as { stack?: { route: string; handle: (req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse, next: () => void) => void }[] };
77
+ if (Array.isArray(app.stack)) {
78
+ app.stack.unshift({ route: '', handle: handleBuilder });
79
+ } else {
80
+ server.middlewares.use(handleBuilder);
81
+ }
82
+ },
83
+ };
84
+ }
85
+
86
+ export const STYLE_PATCH_SERVER_URL = `http://127.0.0.1:${PATCH_PORT}`;
@@ -15,6 +15,8 @@
15
15
  "bun-types": "~1.2.0",
16
16
  "oxfmt": "^0.33.0",
17
17
  "oxlint": "^0.15.0",
18
+ "react": "^19.2.4",
19
+ "react-dom": "^19.2.4",
18
20
  "typescript": "~5.7.0",
19
21
  "vite": "^7.3.1",
20
22
  "vite-plugin-oxlint": "^1.6.0",
@@ -22,7 +24,12 @@
22
24
  },
23
25
  "packages/client": {
24
26
  "name": "@flow.os/client",
25
- "version": "0.0.1",
27
+ "version": "0.0.5",
28
+ "bin": {
29
+ "flow-os-dev": "./scripts/start-dev.ts",
30
+ "flow-os-build": "./scripts/build.ts",
31
+ "flow-os-preview": "./scripts/preview.ts",
32
+ },
26
33
  "dependencies": {
27
34
  "@flow.os/core": "workspace:*",
28
35
  "@flow.os/router": "workspace:*",
@@ -32,11 +39,15 @@
32
39
  "vite": ">=5.0.0",
33
40
  },
34
41
  },
42
+ "packages/core": {
43
+ "name": "@flow.os/core",
44
+ "version": "0.0.5",
45
+ },
35
46
  "packages/create-flow": {
36
- "name": "create-flow.os",
37
- "version": "0.0.1",
47
+ "name": "create-flow-os",
48
+ "version": "0.0.3",
38
49
  "bin": {
39
- "create-flow.os": "./dist/index.js",
50
+ "create-flow-os": "bin/index.js",
40
51
  },
41
52
  "dependencies": {
42
53
  "@clack/prompts": "^0.8.2",
@@ -46,17 +57,13 @@
46
57
  "bun-types": "~1.2.0",
47
58
  },
48
59
  },
49
- "packages/flow.os": {
50
- "name": "@flow.os/core",
51
- "version": "0.0.1",
52
- },
53
60
  "packages/router": {
54
61
  "name": "@flow.os/router",
55
- "version": "0.0.1",
62
+ "version": "0.0.5",
56
63
  },
57
64
  "packages/server": {
58
65
  "name": "@flow.os/server",
59
- "version": "0.0.1",
66
+ "version": "0.0.4",
60
67
  "bin": "./start.ts",
61
68
  "peerDependencies": {
62
69
  "vite": ">=5.0.0",
@@ -64,7 +71,11 @@
64
71
  },
65
72
  "packages/style": {
66
73
  "name": "@flow.os/style",
67
- "version": "0.0.1",
74
+ "version": "0.0.4",
75
+ "peerDependencies": {
76
+ "react": ">=18.0.0",
77
+ "react-dom": ">=18.0.0",
78
+ },
68
79
  },
69
80
  },
70
81
  "packages": {
@@ -126,7 +137,7 @@
126
137
 
127
138
  "@flow.os/client": ["@flow.os/client@workspace:packages/client"],
128
139
 
129
- "@flow.os/core": ["@flow.os/core@workspace:packages/flow.os"],
140
+ "@flow.os/core": ["@flow.os/core@workspace:packages/core"],
130
141
 
131
142
  "@flow.os/router": ["@flow.os/router@workspace:packages/router"],
132
143
 
@@ -248,7 +259,7 @@
248
259
 
249
260
  "bun-types": ["bun-types@1.2.23", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-R9f0hKAZXgFU3mlrA0YpE/fiDvwV0FT9rORApt2aQVWSuJDzZOyB5QLc0N/4HF57CS8IXJ6+L5E4W1bW6NS2Aw=="],
250
261
 
251
- "create-flow.os": ["create-flow.os@workspace:packages/create-flow"],
262
+ "create-flow-os": ["create-flow-os@workspace:packages/create-flow"],
252
263
 
253
264
  "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
254
265
 
@@ -306,8 +317,14 @@
306
317
 
307
318
  "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
308
319
 
320
+ "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
321
+
322
+ "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
323
+
309
324
  "rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="],
310
325
 
326
+ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
327
+
311
328
  "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
312
329
 
313
330
  "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Server-only: no Vite client. When you add a frontend (e.g. React), add flow.config.ts
3
- * with: import { config } from '@flow.os/client'; import { flowServer } from '@flow.os/server';
4
- * export default config({ server: flowServer() });
2
+ * Server-only: no Vite client. When you add a frontend (e.g. React), add flow.config.ts:
3
+ * import { config } from '@flow.os/client';
4
+ * export default config({ server: true });
5
+ * (Or server: flowServer() from '@flow.os/server' for Vite plugins.)
5
6
  */
6
7
  export default {};
@@ -5,6 +5,7 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "server",
8
+ "dev:start": "bun packages/client/scripts/start-dev.ts",
8
9
  "start": "server",
9
10
  "lint": "oxlint .",
10
11
  "fmt": "oxfmt",
@@ -20,6 +21,8 @@
20
21
  },
21
22
  "devDependencies": {
22
23
  "bun-types": "~1.2.0",
24
+ "react": "^19.2.4",
25
+ "react-dom": "^19.2.4",
23
26
  "typescript": "~5.7.0",
24
27
  "vite": "^7.3.1"
25
28
  },
@@ -1,174 +1,20 @@
1
- export const APP_ID = 'flow-app';
1
+ // ========== Core ==========
2
+ // App = root component (one <App /> in layout). run = call once in entry: run(App, import.meta.glob('./routes/**/*.tsx'))
3
+ export { App, run } from './router.js';
2
4
 
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
- }
5
+ // ========== Utilities ==========
6
+ export { go, params } from './router.js';
7
+ export { base, path, href, query, setQuery } from './utils.js';
8
+ /*
9
+ base() => 'https://localhost:5173'
10
10
 
11
- export type RouteModule = {
12
- default: (() => Node) | ((params: Record<string, string>) => Node) | (() => string) | string;
13
- };
11
+ path() => '/posts/123'
14
12
 
15
- export type RunOptions = {
16
- /** Nodo (o factory) mostrato mentre la route lazy viene caricata. */
17
- fallback?: Node | (() => Node);
18
- /** Pagina 404: se fornita, viene mostrata full-page (sostituisce tutto #app, senza layout comune). */
19
- notFound?: Node | ((path: string) => Node);
20
- };
13
+ href() => 'https://localhost:5173/posts/123'
21
14
 
22
- function pathToPattern(key: string): string {
23
- const afterRoutes = key.replace(/^.*[/\\]routes[/\\]?/i, '').replace(/\.tsx?$/i, '');
24
- const s = afterRoutes.split(/[/\\]/).filter(Boolean).join('/');
25
- if (s === 'index' || s === '') return '';
26
- return s
27
- .split('/')
28
- .map((x) => (x.startsWith('[') && x.endsWith(']') ? `:${x.slice(1, -1)}` : x))
29
- .join('/');
30
- }
15
+ params() => ['posts', '123']
31
16
 
32
- function match(pattern: string, path: string): Record<string, string> | null {
33
- const pa = pattern ? pattern.split('/').filter(Boolean) : [];
34
- const ph = path.replace(/^\//, '').split('/').filter(Boolean);
35
- if (pa.length !== ph.length) return null;
36
- const params: Record<string, string> = {};
37
- for (let i = 0; i < pa.length; i++) {
38
- if (pa[i]!.startsWith(':')) params[pa[i]!.slice(1)] = ph[i] ?? '';
39
- else if (pa[i] !== ph[i]) return null;
40
- }
41
- return params;
42
- }
17
+ query() => URLSearchParams { page: '2' }
43
18
 
44
- function setContent(container: HTMLElement, content: Node | string): void {
45
- container.innerHTML = '';
46
- if (typeof content === 'string') container.textContent = content;
47
- else container.appendChild(content);
48
- }
49
-
50
- function defaultNotFound(path: string): Node {
51
- const wrap = document.createElement('div');
52
- wrap.setAttribute('role', 'alert');
53
- const h = document.createElement('h1');
54
- h.textContent = '404';
55
- const p = document.createElement('p');
56
- p.textContent = `Pagina non trovata: ${path || '/'}`;
57
- wrap.append(h, p);
58
- return wrap;
59
- }
60
-
61
- type RouterOptions = {
62
- fallback?: Node | (() => Node);
63
- notFound?: Node | ((path: string) => Node);
64
- /** Per 404 full-page: elemento #app e factory del layout da ripristinare quando si naviga via. */
65
- appRoot?: HTMLElement;
66
- getLayout?: () => Node;
67
- };
68
-
69
- export function createRouter(
70
- modules: Record<string, () => Promise<RouteModule>>,
71
- container: HTMLElement,
72
- options: RouterOptions = {}
73
- ) {
74
- const routes = Object.entries(modules).map(([key, loader]) => ({
75
- pattern: pathToPattern(key),
76
- loader,
77
- }));
78
- routes.sort(
79
- (a, b) =>
80
- b.pattern.split('/').filter(Boolean).length - a.pattern.split('/').filter(Boolean).length
81
- );
82
- const notFound = options.notFound ?? defaultNotFound;
83
- const fullPageNotFound = Boolean(options.appRoot && options.getLayout && options.notFound);
84
-
85
- function getContainer(): HTMLElement | null {
86
- return document.getElementById(APP_ID);
87
- }
88
-
89
- async function render(pathname: string): Promise<boolean> {
90
- const path = pathname === '/' ? '' : pathname.slice(1);
91
- for (const { pattern, loader } of routes) {
92
- const params = match(pattern, path);
93
- if (params === null) continue;
94
- let target = getContainer();
95
- if (fullPageNotFound && !target) {
96
- options.appRoot!.innerHTML = '';
97
- options.appRoot!.appendChild(options.getLayout!());
98
- target = getContainer();
99
- }
100
- if (!target) continue;
101
- const fallbackNode =
102
- options.fallback != null
103
- ? typeof options.fallback === 'function'
104
- ? options.fallback()
105
- : options.fallback
106
- : null;
107
- if (fallbackNode) setContent(target, fallbackNode);
108
- const mod = await loader();
109
- const def = mod.default;
110
- if (typeof def === 'function') {
111
- const out = def.length
112
- ? (def as (p: Record<string, string>) => Node)(params)
113
- : (def as () => Node | string)();
114
- if (out instanceof Node) setContent(target, out);
115
- else if (typeof out === 'string') target.innerHTML = out;
116
- else target.innerHTML = '';
117
- } else setContent(target, typeof def === 'string' ? def : (def as unknown as Node));
118
- return true;
119
- }
120
- const nfNode = typeof notFound === 'function' ? notFound(pathname) : notFound;
121
- if (fullPageNotFound) {
122
- options.appRoot!.innerHTML = '';
123
- options.appRoot!.appendChild(nfNode);
124
- } else {
125
- setContent(container, nfNode);
126
- }
127
- return false;
128
- }
129
-
130
- function go(path: string, replace = false): void {
131
- const p = path.startsWith('/') ? path : `/${path}`;
132
- render(p).then((ok) => {
133
- if (ok) (replace ? history.replaceState : history.pushState).call(history, null, '', p);
134
- });
135
- }
136
-
137
- function start(): void {
138
- render(location.pathname || '/');
139
- window.addEventListener('popstate', () => render(location.pathname || '/'));
140
- document.addEventListener('click', (e) => {
141
- const a = (e.target as Element).closest('a');
142
- if (a?.getAttribute('href')?.startsWith('/') && !a.href.startsWith('//')) {
143
- e.preventDefault();
144
- go(a.getAttribute('href')!);
145
- }
146
- });
147
- }
148
-
149
- return { go, start };
150
- }
151
-
152
- /** Monta App e avvia il router. Da usare nell'entry virtuale. */
153
- export function run(
154
- App: () => Node,
155
- modules: Record<string, () => Promise<RouteModule>>,
156
- options?: RunOptions
157
- ): void {
158
- const app = document.getElementById('app')!;
159
- app.appendChild(App());
160
- const appContainer = document.getElementById(APP_ID);
161
- if (!appContainer)
162
- throw new Error(
163
- `#${APP_ID} not found: use <App /> from @flow.os/router in your root layout.`
164
- );
165
- const routerOptions: RouterOptions = {
166
- fallback: options?.fallback,
167
- notFound: options?.notFound,
168
- };
169
- if (options?.notFound != null) {
170
- routerOptions.appRoot = app;
171
- routerOptions.getLayout = () => App();
172
- }
173
- createRouter(modules, appContainer as HTMLElement, routerOptions).start();
174
- }
19
+ setQuery({ page: '2' }) => 'https://localhost:5173/posts/123?page=2'
20
+ */
@@ -0,0 +1,105 @@
1
+ import type { RouteEntry } from './types.js';
2
+
3
+ const CATCH_ALL_PREFIX = ':*';
4
+
5
+ /**
6
+ * Converts a file-path key (e.g. from import.meta.glob) to a route pattern.
7
+ * - `routes/index.ts` → ''
8
+ * - `routes/about.ts` → 'about'
9
+ * - `routes/posts/[id].ts` → 'posts/:id'
10
+ * - `routes/docs/[...slug].ts` → 'docs/:*slug'
11
+ */
12
+ export function pathToPattern(key: string): string {
13
+ const afterRoutes = key.replace(/^.*[/\\]routes[/\\]?/i, '').replace(/\.tsx?$/i, '');
14
+ const segments = afterRoutes.split(/[/\\]/).filter(Boolean);
15
+ if (segments.length === 0 || segments[0] === 'index') return '';
16
+
17
+ const patternParts = segments.map((seg) => {
18
+ if (seg.startsWith('[...') && seg.endsWith(']')) {
19
+ const name = seg.slice(4, -1);
20
+ return name ? `${CATCH_ALL_PREFIX}${name}` : `${CATCH_ALL_PREFIX}rest`;
21
+ }
22
+ if (seg.startsWith('[') && seg.endsWith(']')) {
23
+ return ':' + seg.slice(1, -1);
24
+ }
25
+ return seg;
26
+ });
27
+
28
+ return patternParts.join('/');
29
+ }
30
+
31
+ /**
32
+ * Returns true if the pattern string contains a catch-all segment.
33
+ */
34
+ export function isCatchAllPattern(pattern: string): boolean {
35
+ return pattern.includes(CATCH_ALL_PREFIX);
36
+ }
37
+
38
+ /**
39
+ * Counts static (non-param) segments for route sort order.
40
+ */
41
+ export function countStaticSegments(pattern: string): number {
42
+ if (!pattern) return 0;
43
+ return pattern.split('/').filter((s) => s && !s.startsWith(':') && !s.startsWith(CATCH_ALL_PREFIX)).length;
44
+ }
45
+
46
+ /**
47
+ * Matches a path against a pattern. Supports :param and :*catchAll.
48
+ * - :param matches one segment.
49
+ * - :*name matches the rest of the path (one param with path segments joined by '/').
50
+ * Returns params object or null if no match.
51
+ */
52
+ export function match(pattern: string, path: string): Record<string, string> | null {
53
+ const patternSegs = pattern ? pattern.split('/').filter(Boolean) : [];
54
+ const pathSegs = path.replace(/^\//, '').split('/').filter(Boolean);
55
+
56
+ const params: Record<string, string> = {};
57
+ let pi = 0;
58
+
59
+ for (let i = 0; i < patternSegs.length; i++) {
60
+ const seg = patternSegs[i]!;
61
+ if (seg.startsWith(CATCH_ALL_PREFIX)) {
62
+ const name = seg.slice(CATCH_ALL_PREFIX.length) || 'rest';
63
+ params[name] = pathSegs.slice(pi).join('/');
64
+ return params;
65
+ }
66
+ if (pi >= pathSegs.length) return null;
67
+ if (seg.startsWith(':')) {
68
+ params[seg.slice(1)] = pathSegs[pi] ?? '';
69
+ pi++;
70
+ continue;
71
+ }
72
+ if (seg !== pathSegs[pi]) return null;
73
+ pi++;
74
+ }
75
+
76
+ return pi === pathSegs.length ? params : null;
77
+ }
78
+
79
+ /**
80
+ * Builds sorted route entries: more specific first (static > dynamic > catch-all),
81
+ * then by segment count descending.
82
+ */
83
+ export function buildRouteEntries(
84
+ modules: Record<string, () => Promise<import('./types.js').RouteModule>>
85
+ ): RouteEntry[] {
86
+ const entries: RouteEntry[] = Object.entries(modules).map(([key, loader]) => {
87
+ const pattern = pathToPattern(key);
88
+ return {
89
+ pattern,
90
+ loader,
91
+ staticSegments: countStaticSegments(pattern),
92
+ catchAll: isCatchAllPattern(pattern),
93
+ };
94
+ });
95
+
96
+ entries.sort((a, b) => {
97
+ if (a.catchAll !== b.catchAll) return a.catchAll ? 1 : -1;
98
+ if (a.staticSegments !== b.staticSegments) return b.staticSegments - a.staticSegments;
99
+ const lenA = a.pattern.split('/').filter(Boolean).length;
100
+ const lenB = b.pattern.split('/').filter(Boolean).length;
101
+ return lenB - lenA;
102
+ });
103
+
104
+ return entries;
105
+ }