create-flow-os 0.0.1-dev.1771785969 → 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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/packages/client/client/root.tsx +1 -8
- package/packages/client/client/routes/index.tsx +8 -7
- package/profiles/client/bun.lock +30 -13
- package/profiles/client/client/root.tsx +1 -8
- package/profiles/client/client/routes/about.tsx +7 -0
- package/profiles/client/client/routes/index.tsx +8 -7
- package/profiles/client/flow.config.ts +4 -1
- package/profiles/client/package.json +3 -0
- package/profiles/client/packages/client/build/config.ts +67 -22
- package/profiles/client/packages/client/build/vite.ts +52 -8
- package/profiles/client/packages/client/features/attrs.ts +1 -1
- package/profiles/client/packages/client/index.ts +2 -1
- package/profiles/client/packages/client/package.json +3 -3
- package/profiles/client/packages/client/runtime/jsx-types.d.ts +2 -0
- package/profiles/client/packages/client/runtime/jsx.ts +4 -2
- package/profiles/client/packages/router/index.ts +14 -168
- package/profiles/client/packages/router/matcher.ts +105 -0
- package/profiles/client/packages/router/router.ts +185 -0
- package/profiles/client/packages/router/types.ts +34 -0
- package/profiles/client/packages/router/utils.ts +45 -0
- package/profiles/client/packages/style/index.ts +29 -15
- package/profiles/client/packages/style/package.json +6 -1
- package/profiles/client/packages/style/react.ts +68 -0
- package/profiles/client/packages/style/resolve.ts +44 -1
- package/profiles/client/packages/style/server.ts +299 -0
- package/profiles/client/packages/style/shorthand.ts +34 -0
- package/profiles/client/packages/style/style-builder/button.ts +41 -0
- package/profiles/client/packages/style/style-builder/constants.ts +16 -0
- package/profiles/client/packages/style/style-builder/dom.ts +18 -0
- package/profiles/client/packages/style/style-builder/index.ts +48 -0
- package/profiles/client/packages/style/style-builder/panel.ts +69 -0
- package/profiles/client/packages/style/style-builder/position.ts +25 -0
- package/profiles/client/packages/style/visual-builder.ts +822 -0
- package/profiles/client/packages/style/vite-plugin.ts +86 -0
- package/profiles/full/bun.lock +30 -13
- package/profiles/full/client/root.tsx +1 -8
- package/profiles/full/client/routes/about.tsx +7 -0
- package/profiles/full/client/routes/index.tsx +8 -7
- package/profiles/full/flow.config.ts +2 -4
- package/profiles/full/package.json +3 -0
- package/profiles/full/packages/client/build/config.ts +67 -22
- package/profiles/full/packages/client/build/vite.ts +52 -8
- package/profiles/full/packages/client/features/attrs.ts +1 -1
- package/profiles/full/packages/client/index.ts +2 -1
- package/profiles/full/packages/client/package.json +3 -3
- package/profiles/full/packages/client/runtime/jsx-types.d.ts +2 -0
- package/profiles/full/packages/client/runtime/jsx.ts +4 -2
- package/profiles/full/packages/router/index.ts +14 -168
- package/profiles/full/packages/router/matcher.ts +105 -0
- package/profiles/full/packages/router/router.ts +185 -0
- package/profiles/full/packages/router/types.ts +34 -0
- package/profiles/full/packages/router/utils.ts +45 -0
- package/profiles/full/packages/style/index.ts +29 -15
- package/profiles/full/packages/style/package.json +6 -1
- package/profiles/full/packages/style/react.ts +68 -0
- package/profiles/full/packages/style/resolve.ts +44 -1
- package/profiles/full/packages/style/server.ts +299 -0
- package/profiles/full/packages/style/shorthand.ts +34 -0
- package/profiles/full/packages/style/style-builder/button.ts +41 -0
- package/profiles/full/packages/style/style-builder/constants.ts +16 -0
- package/profiles/full/packages/style/style-builder/dom.ts +18 -0
- package/profiles/full/packages/style/style-builder/index.ts +48 -0
- package/profiles/full/packages/style/style-builder/panel.ts +69 -0
- package/profiles/full/packages/style/style-builder/position.ts +25 -0
- package/profiles/full/packages/style/visual-builder.ts +822 -0
- package/profiles/full/packages/style/vite-plugin.ts +86 -0
- package/profiles/server/bun.lock +30 -13
- package/profiles/server/flow.config.ts +4 -3
- package/profiles/server/package.json +3 -0
- package/profiles/server/packages/router/index.ts +14 -168
- package/profiles/server/packages/router/matcher.ts +105 -0
- package/profiles/server/packages/router/router.ts +185 -0
- package/profiles/server/packages/router/types.ts +34 -0
- package/profiles/server/packages/router/utils.ts +45 -0
- package/profiles/server/packages/style/index.ts +29 -15
- package/profiles/server/packages/style/package.json +6 -1
- package/profiles/server/packages/style/react.ts +68 -0
- package/profiles/server/packages/style/resolve.ts +44 -1
- package/profiles/server/packages/style/server.ts +299 -0
- package/profiles/server/packages/style/shorthand.ts +34 -0
- package/profiles/server/packages/style/style-builder/button.ts +41 -0
- package/profiles/server/packages/style/style-builder/constants.ts +16 -0
- package/profiles/server/packages/style/style-builder/dom.ts +18 -0
- package/profiles/server/packages/style/style-builder/index.ts +48 -0
- package/profiles/server/packages/style/style-builder/panel.ts +69 -0
- package/profiles/server/packages/style/style-builder/position.ts +25 -0
- package/profiles/server/packages/style/visual-builder.ts +822 -0
- package/profiles/server/packages/style/vite-plugin.ts +86 -0
- package/templates/flow.config.client.ts +4 -1
- package/templates/flow.config.full.ts +6 -0
- 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}`;
|
package/profiles/server/bun.lock
CHANGED
|
@@ -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.
|
|
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
|
|
37
|
-
"version": "0.0.
|
|
47
|
+
"name": "create-flow-os",
|
|
48
|
+
"version": "0.0.3",
|
|
38
49
|
"bin": {
|
|
39
|
-
"create-flow
|
|
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.
|
|
62
|
+
"version": "0.0.5",
|
|
56
63
|
},
|
|
57
64
|
"packages/server": {
|
|
58
65
|
"name": "@flow.os/server",
|
|
59
|
-
"version": "0.0.
|
|
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.
|
|
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/
|
|
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
|
|
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
|
-
*
|
|
4
|
-
* export default config({ server:
|
|
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
|
-
|
|
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
|
-
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
12
|
-
default: (() => Node) | ((params: Record<string, string>) => Node) | (() => string) | string;
|
|
13
|
-
};
|
|
11
|
+
path() => '/posts/123'
|
|
14
12
|
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
+
}
|