@emberkit/core 0.2.8 → 0.2.9
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.
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scores a route path for specificity. Higher score = more specific.
|
|
3
|
+
* Static segments beat dynamic params; dynamic params beat catch-alls.
|
|
4
|
+
* Used to resolve conflicts when multiple routes match the same URL.
|
|
5
|
+
*/
|
|
6
|
+
export declare function scoreRoute(routePath: string): number;
|
|
7
|
+
/**
|
|
8
|
+
* Returns the best-matching route for the given pathname.
|
|
9
|
+
* When multiple routes match (e.g. /admin and /:language both match "/admin"),
|
|
10
|
+
* the route with the highest specificity score wins — static beats dynamic.
|
|
11
|
+
*/
|
|
12
|
+
export declare function matchRoute<T extends {
|
|
13
|
+
path: string;
|
|
14
|
+
}>(routes: T[], pathname: string): T | undefined;
|
|
15
|
+
//# sourceMappingURL=match.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"match.d.ts","sourceRoot":"","sources":["../../../src/runtime/helpers/match.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAmBpD;AAsBD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAkBnG"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scores a route path for specificity. Higher score = more specific.
|
|
3
|
+
* Static segments beat dynamic params; dynamic params beat catch-alls.
|
|
4
|
+
* Used to resolve conflicts when multiple routes match the same URL.
|
|
5
|
+
*/
|
|
6
|
+
export function scoreRoute(routePath) {
|
|
7
|
+
let score = 100;
|
|
8
|
+
const segments = routePath.split('/').filter(Boolean);
|
|
9
|
+
score -= segments.length * 20;
|
|
10
|
+
for (const segment of segments) {
|
|
11
|
+
if (segment.startsWith(':') || segment.startsWith('*') || segment.endsWith('*')) {
|
|
12
|
+
score -= 30;
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
score += 10;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (routePath.includes('*')) {
|
|
19
|
+
score -= 50;
|
|
20
|
+
}
|
|
21
|
+
return Math.max(0, score);
|
|
22
|
+
}
|
|
23
|
+
function routeMatchesPath(routePath, normalized) {
|
|
24
|
+
if (routePath === normalized)
|
|
25
|
+
return true;
|
|
26
|
+
if (routePath !== '/' && normalized.startsWith(routePath + '/'))
|
|
27
|
+
return true;
|
|
28
|
+
if (routePath !== '/' && routePath.includes(':')) {
|
|
29
|
+
const routeParts = routePath.split('/');
|
|
30
|
+
const pathParts = normalized.split('/');
|
|
31
|
+
if (routeParts.length === pathParts.length) {
|
|
32
|
+
for (let i = 0; i < routeParts.length; i++) {
|
|
33
|
+
if (routeParts[i].startsWith(':'))
|
|
34
|
+
continue;
|
|
35
|
+
if (routeParts[i] !== pathParts[i])
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Returns the best-matching route for the given pathname.
|
|
45
|
+
* When multiple routes match (e.g. /admin and /:language both match "/admin"),
|
|
46
|
+
* the route with the highest specificity score wins — static beats dynamic.
|
|
47
|
+
*/
|
|
48
|
+
export function matchRoute(routes, pathname) {
|
|
49
|
+
const normalized = pathname === '/' ? '/' : pathname.replace(/\/$/, '');
|
|
50
|
+
let bestMatch;
|
|
51
|
+
let bestScore = -1;
|
|
52
|
+
for (const route of routes) {
|
|
53
|
+
const routePath = route.path === '/' ? '/' : route.path.replace(/\/$/, '');
|
|
54
|
+
if (routeMatchesPath(routePath, normalized)) {
|
|
55
|
+
const score = scoreRoute(routePath);
|
|
56
|
+
if (score > bestScore) {
|
|
57
|
+
bestScore = score;
|
|
58
|
+
bestMatch = route;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return bestMatch;
|
|
63
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAKnF,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,eAAe,KAAK,OAAO,CAAC,EACpD,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EACtC,GAAG,QAAQ,EAAE,OAAO,EAAE,GACrB,UAAU,CAYZ;AA6ID,wBAAgB,MAAM,CACpB,OAAO,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,EACnF,SAAS,EAAE,OAAO,GAAG,MAAM,EAC3B,OAAO,CAAC,EAAE;IACR,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,KAAK,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,MAAM,OAAO,CAAC;YAAE,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAA;SAAE,CAAC,CAAC;KACpF,CAAC,CAAC;CACJ,GACA,IAAI,CAoFN;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,EAAE,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAE9F;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAE9C;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,UAAU,CAEjE;AAED,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,OAAO,EAAE,CAAC"}
|
package/dist/runtime/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { renderToString, getHandler, clearHandlers } from './helpers/render.js';
|
|
2
2
|
import { getSignalByIndex } from '../signals/helpers/core.js';
|
|
3
|
+
import { matchRoute } from './helpers/match.js';
|
|
3
4
|
export function createElement(type, props, ...children) {
|
|
4
5
|
const resolvedProps = props ?? {};
|
|
5
6
|
const flatChildren = children.flat().filter((child) => child != null && child !== false);
|
|
@@ -147,36 +148,8 @@ export function render(element, container, options) {
|
|
|
147
148
|
renderToTarget(layout, target);
|
|
148
149
|
return;
|
|
149
150
|
}
|
|
150
|
-
function matchRoute(pathname) {
|
|
151
|
-
const normalized = pathname === '/' ? '/' : pathname.replace(/\/$/, '');
|
|
152
|
-
for (const route of routes) {
|
|
153
|
-
const routePath = route.path === '/' ? '/' : route.path.replace(/\/$/, '');
|
|
154
|
-
if (routePath === normalized)
|
|
155
|
-
return route;
|
|
156
|
-
if (routePath !== '/' && normalized.startsWith(routePath + '/'))
|
|
157
|
-
return route;
|
|
158
|
-
if (routePath !== '/' && routePath.includes(':')) {
|
|
159
|
-
const routeParts = routePath.split('/');
|
|
160
|
-
const pathParts = normalized.split('/');
|
|
161
|
-
if (routeParts.length === pathParts.length) {
|
|
162
|
-
let match = true;
|
|
163
|
-
for (let i = 0; i < routeParts.length; i++) {
|
|
164
|
-
if (routeParts[i].startsWith(':'))
|
|
165
|
-
continue;
|
|
166
|
-
if (routeParts[i] !== pathParts[i]) {
|
|
167
|
-
match = false;
|
|
168
|
-
break;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
if (match)
|
|
172
|
-
return route;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
return undefined;
|
|
177
|
-
}
|
|
178
151
|
async function renderCurrentRoute() {
|
|
179
|
-
const matched = matchRoute(window.location.pathname);
|
|
152
|
+
const matched = matchRoute(routes, window.location.pathname);
|
|
180
153
|
if (matched) {
|
|
181
154
|
const mod = await matched.component();
|
|
182
155
|
const params = extractParamsFromPath(matched.path, window.location.pathname);
|
|
@@ -196,8 +169,20 @@ export function render(element, container, options) {
|
|
|
196
169
|
if (!link)
|
|
197
170
|
return;
|
|
198
171
|
const href = link.getAttribute('href');
|
|
199
|
-
if (!href || href.startsWith('http') ||
|
|
172
|
+
if (!href || href.startsWith('http') || link.target === '_blank')
|
|
200
173
|
return;
|
|
174
|
+
// Handle anchor links
|
|
175
|
+
if (href.startsWith('#'))
|
|
176
|
+
return; // Pure anchor link (e.g., #section)
|
|
177
|
+
// Check if link is to an anchor on the same page (e.g., /current-page#section)
|
|
178
|
+
if (href.includes('#')) {
|
|
179
|
+
const [linkPath] = href.split('#');
|
|
180
|
+
const currentPath = window.location.pathname;
|
|
181
|
+
// If the path portion matches current page, it's a same-page anchor
|
|
182
|
+
if (linkPath === currentPath || linkPath === '') {
|
|
183
|
+
return; // Allow default browser behavior to scroll to anchor
|
|
184
|
+
}
|
|
185
|
+
}
|
|
201
186
|
e.preventDefault();
|
|
202
187
|
history.pushState(null, '', href);
|
|
203
188
|
renderCurrentRoute();
|
|
@@ -1115,6 +1115,23 @@ function scanRouteFiles(dir) {
|
|
|
1115
1115
|
walk(dir);
|
|
1116
1116
|
return files;
|
|
1117
1117
|
}
|
|
1118
|
+
function scoreRoutePath(routePath) {
|
|
1119
|
+
let score = 100;
|
|
1120
|
+
const segments = routePath.split('/').filter(Boolean);
|
|
1121
|
+
score -= segments.length * 20;
|
|
1122
|
+
for (const segment of segments) {
|
|
1123
|
+
if (segment.startsWith(':') || segment.startsWith('*') || segment.endsWith('*')) {
|
|
1124
|
+
score -= 30;
|
|
1125
|
+
}
|
|
1126
|
+
else {
|
|
1127
|
+
score += 10;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
if (routePath.includes('*')) {
|
|
1131
|
+
score -= 50;
|
|
1132
|
+
}
|
|
1133
|
+
return Math.max(0, score);
|
|
1134
|
+
}
|
|
1118
1135
|
function generateRoutesCode(files, routeDir) {
|
|
1119
1136
|
const routeEntries = [];
|
|
1120
1137
|
for (const file of files) {
|
|
@@ -1143,12 +1160,13 @@ function generateRoutesCode(files, routeDir) {
|
|
|
1143
1160
|
routePath = '/' + routePath;
|
|
1144
1161
|
}
|
|
1145
1162
|
const importPath = file.replace(/\\/g, '/');
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
routeEntries.push(` { path: ${JSON.stringify(routePath)}, component: () => import(${JSON.stringify(importPath)}) }`);
|
|
1151
|
-
}
|
|
1163
|
+
const entry = isMarkdown
|
|
1164
|
+
? ` { path: ${JSON.stringify(routePath)}, component: () => import(${JSON.stringify(importPath)}), isMarkdown: true }`
|
|
1165
|
+
: ` { path: ${JSON.stringify(routePath)}, component: () => import(${JSON.stringify(importPath)}) }`;
|
|
1166
|
+
routeEntries.push({ path: routePath, entry });
|
|
1152
1167
|
}
|
|
1153
|
-
|
|
1168
|
+
// Sort static routes before dynamic routes so the emitted array already has
|
|
1169
|
+
// the correct priority order (defense-in-depth alongside runtime scoring).
|
|
1170
|
+
routeEntries.sort((a, b) => scoreRoutePath(b.path) - scoreRoutePath(a.path));
|
|
1171
|
+
return `export const routes = [\n${routeEntries.map((r) => r.entry).join(',\n')}\n];`;
|
|
1154
1172
|
}
|