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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ziko",
3
- "version": "1.9.0",
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
- } from "../utils/index.js"
9
- export async function createSPAFileBasedRouter({
11
+ } from "../utils/index.js";
12
+
13
+ /**
14
+ * Environment-independent file-based router core
15
+ */
16
+ export async function createFileBasedRouter({
10
17
  pages = {},
11
- target = globalThis?.document?.body,
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
- if(!(target instanceof HTMLElement) && target?.element instanceof HTMLElement) target = target?.element;
17
- if (!(target instanceof HTMLElement)) {
18
- throw new Error("Invalid mount target: must be HTMLElement or UIElement");
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
- let path = decodeURIComponent(globalThis.location.pathname.replace(/\/$/, ''));
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
- pairs[normalize_path(route, root, extensions)] = modComponent;
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 [routePath, comp] of Object.entries(pairs)) {
35
- if (routes_matcher(routePath, `/${path}`)) {
65
+ for (const routePath of sortedRouteKeys) {
66
+ if (routes_matcher(routePath, currentPath)) {
36
67
  mask = routePath;
37
- component = comp;
68
+ component = pairs[routePath];
38
69
  break;
39
70
  }
40
71
  }
41
- if (mask === null) return; // no route matched
42
- const params = is_dynamic(mask) ? dynamic_routes_parser(mask, path) : undefined;
43
- renderer(target, component, params, wrapper)
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
- const routeSegment = routeSegments[j];
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 (routeSegment) {
35
- params[paramName] = routeSegment;
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] = routeSegment;
44
- }
45
- else if (maskSegment !== routeSegment) return {};
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
- const splitPaths = paths.map(path => path.split('/'));
4
- const minLength = Math.min(...splitPaths.map(parts => parts.length));
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 = splitPaths[0][i];
8
- if (splitPaths.every(parts => parts[i] === part || parts[i].startsWith('['))) {
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
- if(root.at(-1)==="/") root = root.slice(0, -1)
3
- const normalizedPath = inputPath.replace(/\\/g, '/')
4
- // .replace(/\[(\w+)\]/g, '$1/:$1');
5
- const parts = normalizedPath.split('/');
6
- const rootParts = root.split('/');
7
- const rootIndex = parts.indexOf(rootParts[rootParts.length - 1]);
8
- if (rootIndex !== -1) {
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
- return '';
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
- const routeSegment = routeSegments[j];
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 (routeSegment) j++;
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
- if (maskSegment !== routeSegment) return false;
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