ziko 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/router/file-based-router/index.js +63 -14
- package/src/router/utils/dynamic-routes-parser.js +45 -11
- package/src/router/utils/get-root.js +13 -5
- package/src/router/utils/normalize-path.js +30 -19
- package/src/router/utils/routes-grouper.js +28 -1
- package/src/router/utils/routes-matcher.js +32 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ziko",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "A versatile JavaScript library offering a rich set of Hyperscript Based UI components, advanced mathematical utilities, interactivity ,animations, client side routing and more ...",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"front-end",
|
|
@@ -1,23 +1,50 @@
|
|
|
1
|
+
// file-based-router/index.js
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
get_root,
|
|
3
5
|
normalize_path,
|
|
4
6
|
routes_matcher,
|
|
5
7
|
is_dynamic,
|
|
6
8
|
dynamic_routes_parser,
|
|
9
|
+
sort_routes,
|
|
7
10
|
renderer as ziko_renderer
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
} from "../utils/index.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Environment-independent file-based router core
|
|
15
|
+
*/
|
|
16
|
+
export async function createFileBasedRouter({
|
|
10
17
|
pages = {},
|
|
11
|
-
|
|
18
|
+
url = typeof location !== 'undefined' ? location.pathname : '/',
|
|
19
|
+
target = typeof document !== 'undefined' ? document.body : null,
|
|
12
20
|
extensions = ['js', 'ts'],
|
|
13
21
|
renderer = ziko_renderer,
|
|
14
22
|
wrapper,
|
|
23
|
+
base = '/',
|
|
15
24
|
} = {}) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
25
|
+
// Normalize target element safely for UI frameworks/DOM wrapper objects
|
|
26
|
+
let mountTarget = target;
|
|
27
|
+
if (target && typeof target === 'object' && 'element' in target) {
|
|
28
|
+
mountTarget = target.element;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 1. Normalize base path
|
|
32
|
+
let cleanBase = base === '.' ? '' : base.replace(/\/$/, '');
|
|
33
|
+
if (cleanBase && !cleanBase.startsWith('/')) {
|
|
34
|
+
cleanBase = '/' + cleanBase;
|
|
19
35
|
}
|
|
20
|
-
|
|
36
|
+
|
|
37
|
+
// 2. Normalize and extract current URL path
|
|
38
|
+
let rawPath = decodeURIComponent(url.replace(/\/$/, '')) || '/';
|
|
39
|
+
|
|
40
|
+
// Strip base prefix if matched
|
|
41
|
+
if (cleanBase && rawPath.startsWith(cleanBase)) {
|
|
42
|
+
rawPath = rawPath.slice(cleanBase.length) || '/';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let currentPath = rawPath.startsWith('/') ? rawPath : '/' + rawPath;
|
|
46
|
+
|
|
47
|
+
// 3. Normalize route masks
|
|
21
48
|
const routes = Object.keys(pages);
|
|
22
49
|
const root = get_root(routes);
|
|
23
50
|
|
|
@@ -25,21 +52,43 @@ export async function createSPAFileBasedRouter({
|
|
|
25
52
|
for (const route of routes) {
|
|
26
53
|
const module = await pages[route]();
|
|
27
54
|
const modComponent = await module.default;
|
|
28
|
-
|
|
55
|
+
const normalizedKey = normalize_path(route, root, extensions);
|
|
56
|
+
pairs[normalizedKey] = modComponent;
|
|
29
57
|
}
|
|
30
58
|
|
|
59
|
+
// 4. Sort routes by precedence (Static -> Dynamic -> Catch-All -> Optional Catch-All)
|
|
60
|
+
const sortedRouteKeys = sort_routes(Object.keys(pairs));
|
|
61
|
+
|
|
31
62
|
let mask = null;
|
|
32
63
|
let component = null;
|
|
33
64
|
|
|
34
|
-
for (const
|
|
35
|
-
if (routes_matcher(routePath,
|
|
65
|
+
for (const routePath of sortedRouteKeys) {
|
|
66
|
+
if (routes_matcher(routePath, currentPath)) {
|
|
36
67
|
mask = routePath;
|
|
37
|
-
component =
|
|
68
|
+
component = pairs[routePath];
|
|
38
69
|
break;
|
|
39
70
|
}
|
|
40
71
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
72
|
+
|
|
73
|
+
if (mask === null) {
|
|
74
|
+
return { mask: null, component: null, params: {}, matched: false };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const params = is_dynamic(mask) ? dynamic_routes_parser(mask, currentPath) : {};
|
|
78
|
+
|
|
79
|
+
// Execute renderer if a valid mount target is available
|
|
80
|
+
if (mountTarget && typeof renderer === 'function') {
|
|
81
|
+
await renderer(mountTarget, component, params, wrapper);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Return router state for SSR/static build environments
|
|
85
|
+
return {
|
|
86
|
+
mask,
|
|
87
|
+
component,
|
|
88
|
+
params,
|
|
89
|
+
matched: true
|
|
90
|
+
};
|
|
44
91
|
}
|
|
45
92
|
|
|
93
|
+
// Backward-compatible alias for SPA usage
|
|
94
|
+
export const createSPAFileBasedRouter = createFileBasedRouter;
|
|
@@ -3,9 +3,33 @@ export function dynamic_routes_parser(mask, route) {
|
|
|
3
3
|
const routeSegments = route.split("/").filter(Boolean);
|
|
4
4
|
const params = {};
|
|
5
5
|
let i = 0, j = 0;
|
|
6
|
+
|
|
6
7
|
while (i < maskSegments.length && j < routeSegments.length) {
|
|
7
8
|
const maskSegment = maskSegments[i];
|
|
8
|
-
|
|
9
|
+
|
|
10
|
+
// Handle [[...slug]]
|
|
11
|
+
if (maskSegment.startsWith("[[...") && maskSegment.endsWith("]]")) {
|
|
12
|
+
const paramName = maskSegment.slice(5, -2);
|
|
13
|
+
const remainingMaskSegments = maskSegments.length - i - 1;
|
|
14
|
+
if (remainingMaskSegments === 0) {
|
|
15
|
+
params[paramName] = routeSegments.slice(j).join("/");
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
let requiredSegments = 0;
|
|
19
|
+
for (let k = i + 1; k < maskSegments.length; k++) {
|
|
20
|
+
if (!maskSegments[k].endsWith("]+")) requiredSegments++;
|
|
21
|
+
}
|
|
22
|
+
const remainingRouteSegments = routeSegments.length - j;
|
|
23
|
+
const segmentsToConsume = remainingRouteSegments - requiredSegments;
|
|
24
|
+
if (segmentsToConsume >= 0) {
|
|
25
|
+
params[paramName] = routeSegments.slice(j, j + segmentsToConsume).join("/");
|
|
26
|
+
j += segmentsToConsume;
|
|
27
|
+
} else return {};
|
|
28
|
+
i++;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Handle [...slug]
|
|
9
33
|
if (maskSegment.startsWith("[...") && maskSegment.endsWith("]")) {
|
|
10
34
|
const paramName = maskSegment.slice(4, -1);
|
|
11
35
|
const remainingMaskSegments = maskSegments.length - i - 1;
|
|
@@ -20,32 +44,42 @@ export function dynamic_routes_parser(mask, route) {
|
|
|
20
44
|
const remainingRouteSegments = routeSegments.length - j;
|
|
21
45
|
const segmentsToConsume = remainingRouteSegments - requiredSegments;
|
|
22
46
|
if (segmentsToConsume >= 1) {
|
|
23
|
-
params[paramName] = routeSegments
|
|
24
|
-
.slice(j, j + segmentsToConsume)
|
|
25
|
-
.join("/");
|
|
47
|
+
params[paramName] = routeSegments.slice(j, j + segmentsToConsume).join("/");
|
|
26
48
|
j += segmentsToConsume;
|
|
27
|
-
}
|
|
28
|
-
else return {};
|
|
49
|
+
} else return {};
|
|
29
50
|
i++;
|
|
30
51
|
continue;
|
|
31
52
|
}
|
|
53
|
+
|
|
32
54
|
if (maskSegment.startsWith("[") && maskSegment.endsWith("]+")) {
|
|
33
55
|
const paramName = maskSegment.slice(1, -2);
|
|
34
|
-
if (
|
|
35
|
-
params[paramName] =
|
|
56
|
+
if (routeSegments[j]) {
|
|
57
|
+
params[paramName] = routeSegments[j];
|
|
36
58
|
j++;
|
|
37
59
|
}
|
|
38
60
|
i++;
|
|
39
61
|
continue;
|
|
40
62
|
}
|
|
63
|
+
|
|
41
64
|
if (maskSegment.startsWith("[") && maskSegment.endsWith("]")) {
|
|
42
65
|
const paramName = maskSegment.slice(1, -1);
|
|
43
|
-
params[paramName] =
|
|
44
|
-
}
|
|
45
|
-
|
|
66
|
+
params[paramName] = routeSegments[j];
|
|
67
|
+
} else if (maskSegment !== routeSegments[j]) return {};
|
|
68
|
+
|
|
46
69
|
i++;
|
|
47
70
|
j++;
|
|
48
71
|
}
|
|
72
|
+
|
|
73
|
+
// Set default empty string for remaining uncaptured optional catch-alls
|
|
74
|
+
while (i < maskSegments.length) {
|
|
75
|
+
const maskSegment = maskSegments[i];
|
|
76
|
+
if (maskSegment.startsWith("[[...") && maskSegment.endsWith("]]")) {
|
|
77
|
+
const paramName = maskSegment.slice(5, -2);
|
|
78
|
+
if (!(paramName in params)) params[paramName] = "";
|
|
79
|
+
}
|
|
80
|
+
i++;
|
|
81
|
+
}
|
|
82
|
+
|
|
49
83
|
return params;
|
|
50
84
|
}
|
|
51
85
|
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
export function get_root(paths) {
|
|
2
2
|
if (paths.length === 0) return '';
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
|
|
4
|
+
// Strip trailing file names (e.g. /index.js or /about.js) to compare directory structures only
|
|
5
|
+
const dirPaths = paths.map(path => {
|
|
6
|
+
const parts = path.split('/');
|
|
7
|
+
parts.pop(); // Remove the filename
|
|
8
|
+
return parts;
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const minLength = Math.min(...dirPaths.map(parts => parts.length));
|
|
5
12
|
let commonParts = [];
|
|
13
|
+
|
|
6
14
|
for (let i = 0; i < minLength; i++) {
|
|
7
|
-
const part =
|
|
8
|
-
if (
|
|
15
|
+
const part = dirPaths[0][i];
|
|
16
|
+
if (dirPaths.every(parts => parts[i] === part || parts[i].startsWith('['))) {
|
|
9
17
|
commonParts.push(part);
|
|
10
18
|
}
|
|
11
19
|
else break;
|
|
12
|
-
|
|
13
20
|
}
|
|
21
|
+
|
|
14
22
|
const root = commonParts.join('/') + (commonParts.length ? '/' : '');
|
|
15
23
|
return root;
|
|
16
24
|
}
|
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
export function normalize_path(inputPath, root = './src/pages', extensions = ['js', 'ts', 'jsx', 'tsx']) {
|
|
2
|
-
|
|
3
|
-
const normalizedPath = inputPath.replace(/\\/g, '/')
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const subsequentParts = parts.slice(rootIndex + 1);
|
|
10
|
-
const lastPart = subsequentParts[subsequentParts.length - 1];
|
|
11
|
-
const isIndexFile = extensions.some(ext => lastPart === `index.${ext}`);
|
|
12
|
-
const hasValidExtension = lastPart && extensions.some(ext => lastPart === `.${ext}` || lastPart.endsWith(`.${ext}`));
|
|
13
|
-
if (isIndexFile) return '/' + (subsequentParts.length > 1 ? subsequentParts.slice(0, -1).join('/') : '');
|
|
14
|
-
// if (hasValidExtension) return '/' + subsequentParts.join('/').replace(/\.(js|ts)$/, '');
|
|
15
|
-
if (hasValidExtension) {
|
|
16
|
-
const regex = new RegExp(`\\.(${extensions.join('|')})$`);
|
|
17
|
-
return '/' + subsequentParts.join('/').replace(regex, '');
|
|
18
|
-
}
|
|
2
|
+
let cleanRoot = root.endsWith('/') ? root.slice(0, -1) : root;
|
|
3
|
+
const normalizedPath = inputPath.replace(/\\/g, '/');
|
|
4
|
+
|
|
5
|
+
// 1. Extract path relative to root
|
|
6
|
+
let relativePath = normalizedPath;
|
|
7
|
+
if (cleanRoot && normalizedPath.includes(cleanRoot)) {
|
|
8
|
+
relativePath = normalizedPath.split(cleanRoot).pop();
|
|
19
9
|
}
|
|
20
|
-
|
|
21
|
-
|
|
10
|
+
|
|
11
|
+
// 2. Split directory parts & strip route groups like (auth)
|
|
12
|
+
const rawParts = relativePath.split('/').filter(Boolean).filter(p => !/^\([^)]+\)$/.test(p));
|
|
13
|
+
if (rawParts.length === 0) return '/';
|
|
14
|
+
|
|
15
|
+
const lastPart = rawParts[rawParts.length - 1];
|
|
16
|
+
const extRegex = new RegExp(`\\.(${extensions.join('|')})$`);
|
|
17
|
+
|
|
18
|
+
const isIndexFile = extensions.some(ext => lastPart === `index.${ext}`);
|
|
19
|
+
const fileNameWithoutExt = lastPart.replace(extRegex, '');
|
|
20
|
+
|
|
21
|
+
// Remove original file name from directory segments
|
|
22
|
+
rawParts.pop();
|
|
23
|
+
|
|
24
|
+
// 3. Handle flat dot notation without breaking [..slug] or [[..slug]]
|
|
25
|
+
if (!isIndexFile) {
|
|
26
|
+
// Splits by '.' ONLY if the dot is outside of bracketed patterns like [...] or [[...]]
|
|
27
|
+
const dotSegments = fileNameWithoutExt.split(/\.(?![^\[]*\])/);
|
|
28
|
+
rawParts.push(...dotSegments);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return '/' + rawParts.join('/');
|
|
32
|
+
}
|
|
@@ -1,7 +1,34 @@
|
|
|
1
1
|
export function is_dynamic(path) {
|
|
2
|
-
const DynamicPattern = /(:\w+|\[\.\.\.\w+\]|\[\w+\]\+?)/;
|
|
2
|
+
const DynamicPattern = /(:\w+|\[\[\.\.\.\w+\]\]|\[\.\.\.\w+\]|\[\w+\]\+?)/;
|
|
3
3
|
return DynamicPattern.test(path);
|
|
4
4
|
}
|
|
5
|
+
export function sort_routes(routeKeys) {
|
|
6
|
+
return [...routeKeys].sort((a, b) => {
|
|
7
|
+
const aIsOptionalCatchAll = a.includes('[[...');
|
|
8
|
+
const bIsOptionalCatchAll = b.includes('[[...');
|
|
9
|
+
|
|
10
|
+
const aIsCatchAll = a.includes('[...') && !aIsOptionalCatchAll;
|
|
11
|
+
const bIsCatchAll = b.includes('[...') && !bIsOptionalCatchAll;
|
|
12
|
+
|
|
13
|
+
const aIsDynamic = is_dynamic(a);
|
|
14
|
+
const bIsDynamic = is_dynamic(b);
|
|
15
|
+
|
|
16
|
+
// 1. Optional catch-alls [[...slug]] ALWAYS go last
|
|
17
|
+
if (aIsOptionalCatchAll && !bIsOptionalCatchAll) return 1;
|
|
18
|
+
if (!aIsOptionalCatchAll && bIsOptionalCatchAll) return -1;
|
|
19
|
+
|
|
20
|
+
// 2. Catch-alls [...slug] go right before optional catch-alls
|
|
21
|
+
if (aIsCatchAll && !bIsCatchAll) return 1;
|
|
22
|
+
if (!aIsCatchAll && bIsCatchAll) return -1;
|
|
23
|
+
|
|
24
|
+
// 3. Static routes go before standard dynamic routes ([id])
|
|
25
|
+
if (!aIsDynamic && bIsDynamic) return -1;
|
|
26
|
+
if (aIsDynamic && !bIsDynamic) return 1;
|
|
27
|
+
|
|
28
|
+
// 4. Deeper/longer routes take precedence over shorter ones
|
|
29
|
+
return b.length - a.length;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
5
32
|
export function routes_grouper(routeMap) {
|
|
6
33
|
const grouped = {
|
|
7
34
|
static: {},
|
|
@@ -2,18 +2,33 @@ export function routes_matcher(mask, route) {
|
|
|
2
2
|
const maskSegments = mask.split("/").filter(Boolean);
|
|
3
3
|
const routeSegments = route.split("/").filter(Boolean);
|
|
4
4
|
let i = 0, j = 0;
|
|
5
|
+
|
|
5
6
|
while (i < maskSegments.length && j < routeSegments.length) {
|
|
6
7
|
const maskSegment = maskSegments[i];
|
|
7
|
-
|
|
8
|
+
|
|
9
|
+
// Optional Catch-all [[...slug]]
|
|
10
|
+
if (maskSegment.startsWith("[[...") && maskSegment.endsWith("]]")) {
|
|
11
|
+
const remainingMaskSegments = maskSegments.length - i - 1;
|
|
12
|
+
if (remainingMaskSegments === 0) return true;
|
|
13
|
+
let requiredSegments = 0;
|
|
14
|
+
for (let k = i + 1; k < maskSegments.length; k++) {
|
|
15
|
+
if (!maskSegments[k].endsWith("]+")) requiredSegments++;
|
|
16
|
+
}
|
|
17
|
+
const remainingRouteSegments = routeSegments.length - j;
|
|
18
|
+
const segmentsToConsume = remainingRouteSegments - requiredSegments;
|
|
19
|
+
if (segmentsToConsume < 0) return false;
|
|
20
|
+
j += segmentsToConsume;
|
|
21
|
+
i++;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Required Catch-all [...slug]
|
|
8
26
|
if (maskSegment.startsWith("[...") && maskSegment.endsWith("]")) {
|
|
9
27
|
const remainingMaskSegments = maskSegments.length - i - 1;
|
|
10
28
|
if (remainingMaskSegments === 0) return true;
|
|
11
|
-
// Calculate minimum required route segments for remaining mask
|
|
12
29
|
let requiredSegments = 0;
|
|
13
30
|
for (let k = i + 1; k < maskSegments.length; k++) {
|
|
14
|
-
if (!maskSegments[k].endsWith("]+"))
|
|
15
|
-
requiredSegments++;
|
|
16
|
-
}
|
|
31
|
+
if (!maskSegments[k].endsWith("]+")) requiredSegments++;
|
|
17
32
|
}
|
|
18
33
|
const remainingRouteSegments = routeSegments.length - j;
|
|
19
34
|
if (remainingRouteSegments < requiredSegments) return false;
|
|
@@ -23,32 +38,40 @@ export function routes_matcher(mask, route) {
|
|
|
23
38
|
i++;
|
|
24
39
|
continue;
|
|
25
40
|
}
|
|
41
|
+
|
|
26
42
|
if (maskSegment.startsWith("[") && maskSegment.endsWith("]+")) {
|
|
27
|
-
if (
|
|
43
|
+
if (routeSegments[j]) j++;
|
|
28
44
|
i++;
|
|
29
45
|
continue;
|
|
30
46
|
}
|
|
47
|
+
|
|
31
48
|
if (maskSegment.startsWith("[") && maskSegment.endsWith("]")) {
|
|
32
49
|
i++;
|
|
33
50
|
j++;
|
|
34
51
|
continue;
|
|
35
52
|
}
|
|
36
|
-
|
|
53
|
+
|
|
54
|
+
if (maskSegment !== routeSegments[j]) return false;
|
|
37
55
|
i++;
|
|
38
56
|
j++;
|
|
39
57
|
}
|
|
58
|
+
|
|
40
59
|
while (i < maskSegments.length) {
|
|
41
60
|
const seg = maskSegments[i];
|
|
61
|
+
// [[...slug]] and [param]+ can match empty segments at the end
|
|
62
|
+
if (seg.startsWith("[[...") && seg.endsWith("]]")) {
|
|
63
|
+
i++;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
42
66
|
if (seg.endsWith("]+")) {
|
|
43
67
|
i++;
|
|
44
68
|
continue;
|
|
45
69
|
}
|
|
46
70
|
return false;
|
|
47
71
|
}
|
|
72
|
+
|
|
48
73
|
return i === maskSegments.length && j === routeSegments.length;
|
|
49
74
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
75
|
// // DEMO
|
|
53
76
|
// console.log("=== EXISTING TESTS ===");
|
|
54
77
|
// console.log(routes_matcher("/user/[id]+", "/user")); // true
|