@matthesketh/utopia-router 0.4.0 → 0.5.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/dist/index.cjs +40 -19
- package/dist/index.js +40 -19
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -43,26 +43,37 @@ __export(index_exports, {
|
|
|
43
43
|
module.exports = __toCommonJS(index_exports);
|
|
44
44
|
|
|
45
45
|
// src/matcher.ts
|
|
46
|
+
var BACKSLASH_RE = /\\/g;
|
|
47
|
+
var PAGE_FILE_RE = /\/?\+page\.\w+$/;
|
|
48
|
+
var LAYOUT_OR_ERROR_FILE_RE = /\/?\+(layout|error)\.\w+$/;
|
|
49
|
+
var GROUP_SEGMENT_RE = /^\(.+\)$/;
|
|
50
|
+
var REST_PARAM_RE = /^\[\.\.\..+\]$/;
|
|
51
|
+
var DYNAMIC_PARAM_RE = /^\[.+\]$/;
|
|
52
|
+
var ROOT_ROUTE_RE = /^\/$/;
|
|
53
|
+
var REGEX_SPECIAL_CHARS_RE = /[.*+?^${}()|[\]\\]/g;
|
|
54
|
+
var PAGE_FILE_TEST_RE = /\+page\.\w+$/;
|
|
55
|
+
var LAYOUT_FILE_TEST_RE = /\+layout\.\w+$/;
|
|
56
|
+
var ERROR_FILE_TEST_RE = /\+error\.\w+$/;
|
|
46
57
|
function filePathToRoute(filePath) {
|
|
47
|
-
let normalized = filePath.replace(
|
|
58
|
+
let normalized = filePath.replace(BACKSLASH_RE, "/");
|
|
48
59
|
const routesIdx = normalized.indexOf("routes/");
|
|
49
60
|
if (routesIdx !== -1) {
|
|
50
61
|
normalized = normalized.slice(routesIdx + "routes/".length);
|
|
51
62
|
}
|
|
52
|
-
normalized = normalized.replace(
|
|
53
|
-
normalized = normalized.replace(
|
|
63
|
+
normalized = normalized.replace(PAGE_FILE_RE, "");
|
|
64
|
+
normalized = normalized.replace(LAYOUT_OR_ERROR_FILE_RE, "");
|
|
54
65
|
const segments = normalized.split("/").filter(Boolean);
|
|
55
66
|
const routeSegments = [];
|
|
56
67
|
for (const segment of segments) {
|
|
57
|
-
if (
|
|
68
|
+
if (GROUP_SEGMENT_RE.test(segment)) {
|
|
58
69
|
continue;
|
|
59
70
|
}
|
|
60
|
-
if (
|
|
71
|
+
if (REST_PARAM_RE.test(segment)) {
|
|
61
72
|
const paramName = segment.slice(4, -1);
|
|
62
73
|
routeSegments.push(`*${paramName}`);
|
|
63
74
|
continue;
|
|
64
75
|
}
|
|
65
|
-
if (
|
|
76
|
+
if (DYNAMIC_PARAM_RE.test(segment)) {
|
|
66
77
|
const paramName = segment.slice(1, -1);
|
|
67
78
|
routeSegments.push(`:${paramName}`);
|
|
68
79
|
continue;
|
|
@@ -76,7 +87,7 @@ function compilePattern(pattern) {
|
|
|
76
87
|
const params = [];
|
|
77
88
|
if (pattern === "/") {
|
|
78
89
|
return {
|
|
79
|
-
regex:
|
|
90
|
+
regex: ROOT_ROUTE_RE,
|
|
80
91
|
params: []
|
|
81
92
|
};
|
|
82
93
|
}
|
|
@@ -98,12 +109,13 @@ function compilePattern(pattern) {
|
|
|
98
109
|
}
|
|
99
110
|
regexStr = "^" + regexStr + "/?$";
|
|
100
111
|
return {
|
|
112
|
+
// Dynamic regex — built from sanitized input (segments are escaped via escapeRegex).
|
|
101
113
|
regex: new RegExp(regexStr),
|
|
102
114
|
params
|
|
103
115
|
};
|
|
104
116
|
}
|
|
105
117
|
function escapeRegex(str) {
|
|
106
|
-
return str.replace(
|
|
118
|
+
return str.replace(REGEX_SPECIAL_CHARS_RE, "\\$&");
|
|
107
119
|
}
|
|
108
120
|
function matchRoute(url, routes2) {
|
|
109
121
|
let pathname = url.pathname;
|
|
@@ -127,12 +139,12 @@ function buildRouteTable(manifest) {
|
|
|
127
139
|
const layouts = /* @__PURE__ */ new Map();
|
|
128
140
|
const errors = /* @__PURE__ */ new Map();
|
|
129
141
|
for (const [filePath, importFn] of Object.entries(manifest)) {
|
|
130
|
-
const normalized = filePath.replace(
|
|
131
|
-
if (
|
|
142
|
+
const normalized = filePath.replace(BACKSLASH_RE, "/");
|
|
143
|
+
if (PAGE_FILE_TEST_RE.test(normalized)) {
|
|
132
144
|
pages.set(normalized, importFn);
|
|
133
|
-
} else if (
|
|
145
|
+
} else if (LAYOUT_FILE_TEST_RE.test(normalized)) {
|
|
134
146
|
layouts.set(normalized, importFn);
|
|
135
|
-
} else if (
|
|
147
|
+
} else if (ERROR_FILE_TEST_RE.test(normalized)) {
|
|
136
148
|
errors.set(normalized, importFn);
|
|
137
149
|
}
|
|
138
150
|
}
|
|
@@ -179,12 +191,12 @@ function routeSpecificity(path) {
|
|
|
179
191
|
return score;
|
|
180
192
|
}
|
|
181
193
|
function findNearestSpecialFile(pageFilePath, specialFiles) {
|
|
182
|
-
const normalized = pageFilePath.replace(
|
|
194
|
+
const normalized = pageFilePath.replace(BACKSLASH_RE, "/");
|
|
183
195
|
const lastSlash = normalized.lastIndexOf("/");
|
|
184
196
|
let dir = lastSlash !== -1 ? normalized.slice(0, lastSlash) : "";
|
|
185
197
|
while (dir) {
|
|
186
198
|
for (const [specialPath, importFn] of specialFiles) {
|
|
187
|
-
const specialNorm = specialPath.replace(
|
|
199
|
+
const specialNorm = specialPath.replace(BACKSLASH_RE, "/");
|
|
188
200
|
const specialLastSlash = specialNorm.lastIndexOf("/");
|
|
189
201
|
const specialDir = specialLastSlash !== -1 ? specialNorm.slice(0, specialLastSlash) : "";
|
|
190
202
|
if (specialDir === dir) {
|
|
@@ -202,6 +214,7 @@ function findNearestSpecialFile(pageFilePath, specialFiles) {
|
|
|
202
214
|
|
|
203
215
|
// src/router.ts
|
|
204
216
|
var import_utopia_core = require("@matthesketh/utopia-core");
|
|
217
|
+
var VALID_DOM_ID_RE = /^[A-Za-z0-9_-]+$/;
|
|
205
218
|
var currentRoute = (0, import_utopia_core.signal)(null);
|
|
206
219
|
var isNavigating = (0, import_utopia_core.signal)(false);
|
|
207
220
|
var routes = [];
|
|
@@ -324,10 +337,13 @@ async function navigate(url, options = {}) {
|
|
|
324
337
|
currentRoute.set(match);
|
|
325
338
|
requestAnimationFrame(() => {
|
|
326
339
|
if (fullUrl.hash) {
|
|
327
|
-
const
|
|
328
|
-
if (
|
|
329
|
-
el.
|
|
330
|
-
|
|
340
|
+
const hashId = fullUrl.hash.slice(1);
|
|
341
|
+
if (hashId && VALID_DOM_ID_RE.test(hashId)) {
|
|
342
|
+
const el = document.getElementById(hashId);
|
|
343
|
+
if (el) {
|
|
344
|
+
el.scrollIntoView();
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
331
347
|
}
|
|
332
348
|
}
|
|
333
349
|
window.scrollTo(0, 0);
|
|
@@ -572,8 +588,13 @@ function clearContainer(container) {
|
|
|
572
588
|
container.removeChild(container.firstChild);
|
|
573
589
|
}
|
|
574
590
|
}
|
|
591
|
+
var AMPERSAND_RE = /&/g;
|
|
592
|
+
var LESS_THAN_RE = /</g;
|
|
593
|
+
var GREATER_THAN_RE = />/g;
|
|
594
|
+
var DOUBLE_QUOTE_RE = /"/g;
|
|
595
|
+
var SINGLE_QUOTE_RE = /'/g;
|
|
575
596
|
function escapeHtml(str) {
|
|
576
|
-
return str.replace(
|
|
597
|
+
return str.replace(AMPERSAND_RE, "&").replace(LESS_THAN_RE, "<").replace(GREATER_THAN_RE, ">").replace(DOUBLE_QUOTE_RE, """).replace(SINGLE_QUOTE_RE, "'");
|
|
577
598
|
}
|
|
578
599
|
|
|
579
600
|
// src/query.ts
|
package/dist/index.js
CHANGED
|
@@ -1,24 +1,35 @@
|
|
|
1
1
|
// src/matcher.ts
|
|
2
|
+
var BACKSLASH_RE = /\\/g;
|
|
3
|
+
var PAGE_FILE_RE = /\/?\+page\.\w+$/;
|
|
4
|
+
var LAYOUT_OR_ERROR_FILE_RE = /\/?\+(layout|error)\.\w+$/;
|
|
5
|
+
var GROUP_SEGMENT_RE = /^\(.+\)$/;
|
|
6
|
+
var REST_PARAM_RE = /^\[\.\.\..+\]$/;
|
|
7
|
+
var DYNAMIC_PARAM_RE = /^\[.+\]$/;
|
|
8
|
+
var ROOT_ROUTE_RE = /^\/$/;
|
|
9
|
+
var REGEX_SPECIAL_CHARS_RE = /[.*+?^${}()|[\]\\]/g;
|
|
10
|
+
var PAGE_FILE_TEST_RE = /\+page\.\w+$/;
|
|
11
|
+
var LAYOUT_FILE_TEST_RE = /\+layout\.\w+$/;
|
|
12
|
+
var ERROR_FILE_TEST_RE = /\+error\.\w+$/;
|
|
2
13
|
function filePathToRoute(filePath) {
|
|
3
|
-
let normalized = filePath.replace(
|
|
14
|
+
let normalized = filePath.replace(BACKSLASH_RE, "/");
|
|
4
15
|
const routesIdx = normalized.indexOf("routes/");
|
|
5
16
|
if (routesIdx !== -1) {
|
|
6
17
|
normalized = normalized.slice(routesIdx + "routes/".length);
|
|
7
18
|
}
|
|
8
|
-
normalized = normalized.replace(
|
|
9
|
-
normalized = normalized.replace(
|
|
19
|
+
normalized = normalized.replace(PAGE_FILE_RE, "");
|
|
20
|
+
normalized = normalized.replace(LAYOUT_OR_ERROR_FILE_RE, "");
|
|
10
21
|
const segments = normalized.split("/").filter(Boolean);
|
|
11
22
|
const routeSegments = [];
|
|
12
23
|
for (const segment of segments) {
|
|
13
|
-
if (
|
|
24
|
+
if (GROUP_SEGMENT_RE.test(segment)) {
|
|
14
25
|
continue;
|
|
15
26
|
}
|
|
16
|
-
if (
|
|
27
|
+
if (REST_PARAM_RE.test(segment)) {
|
|
17
28
|
const paramName = segment.slice(4, -1);
|
|
18
29
|
routeSegments.push(`*${paramName}`);
|
|
19
30
|
continue;
|
|
20
31
|
}
|
|
21
|
-
if (
|
|
32
|
+
if (DYNAMIC_PARAM_RE.test(segment)) {
|
|
22
33
|
const paramName = segment.slice(1, -1);
|
|
23
34
|
routeSegments.push(`:${paramName}`);
|
|
24
35
|
continue;
|
|
@@ -32,7 +43,7 @@ function compilePattern(pattern) {
|
|
|
32
43
|
const params = [];
|
|
33
44
|
if (pattern === "/") {
|
|
34
45
|
return {
|
|
35
|
-
regex:
|
|
46
|
+
regex: ROOT_ROUTE_RE,
|
|
36
47
|
params: []
|
|
37
48
|
};
|
|
38
49
|
}
|
|
@@ -54,12 +65,13 @@ function compilePattern(pattern) {
|
|
|
54
65
|
}
|
|
55
66
|
regexStr = "^" + regexStr + "/?$";
|
|
56
67
|
return {
|
|
68
|
+
// Dynamic regex — built from sanitized input (segments are escaped via escapeRegex).
|
|
57
69
|
regex: new RegExp(regexStr),
|
|
58
70
|
params
|
|
59
71
|
};
|
|
60
72
|
}
|
|
61
73
|
function escapeRegex(str) {
|
|
62
|
-
return str.replace(
|
|
74
|
+
return str.replace(REGEX_SPECIAL_CHARS_RE, "\\$&");
|
|
63
75
|
}
|
|
64
76
|
function matchRoute(url, routes2) {
|
|
65
77
|
let pathname = url.pathname;
|
|
@@ -83,12 +95,12 @@ function buildRouteTable(manifest) {
|
|
|
83
95
|
const layouts = /* @__PURE__ */ new Map();
|
|
84
96
|
const errors = /* @__PURE__ */ new Map();
|
|
85
97
|
for (const [filePath, importFn] of Object.entries(manifest)) {
|
|
86
|
-
const normalized = filePath.replace(
|
|
87
|
-
if (
|
|
98
|
+
const normalized = filePath.replace(BACKSLASH_RE, "/");
|
|
99
|
+
if (PAGE_FILE_TEST_RE.test(normalized)) {
|
|
88
100
|
pages.set(normalized, importFn);
|
|
89
|
-
} else if (
|
|
101
|
+
} else if (LAYOUT_FILE_TEST_RE.test(normalized)) {
|
|
90
102
|
layouts.set(normalized, importFn);
|
|
91
|
-
} else if (
|
|
103
|
+
} else if (ERROR_FILE_TEST_RE.test(normalized)) {
|
|
92
104
|
errors.set(normalized, importFn);
|
|
93
105
|
}
|
|
94
106
|
}
|
|
@@ -135,12 +147,12 @@ function routeSpecificity(path) {
|
|
|
135
147
|
return score;
|
|
136
148
|
}
|
|
137
149
|
function findNearestSpecialFile(pageFilePath, specialFiles) {
|
|
138
|
-
const normalized = pageFilePath.replace(
|
|
150
|
+
const normalized = pageFilePath.replace(BACKSLASH_RE, "/");
|
|
139
151
|
const lastSlash = normalized.lastIndexOf("/");
|
|
140
152
|
let dir = lastSlash !== -1 ? normalized.slice(0, lastSlash) : "";
|
|
141
153
|
while (dir) {
|
|
142
154
|
for (const [specialPath, importFn] of specialFiles) {
|
|
143
|
-
const specialNorm = specialPath.replace(
|
|
155
|
+
const specialNorm = specialPath.replace(BACKSLASH_RE, "/");
|
|
144
156
|
const specialLastSlash = specialNorm.lastIndexOf("/");
|
|
145
157
|
const specialDir = specialLastSlash !== -1 ? specialNorm.slice(0, specialLastSlash) : "";
|
|
146
158
|
if (specialDir === dir) {
|
|
@@ -158,6 +170,7 @@ function findNearestSpecialFile(pageFilePath, specialFiles) {
|
|
|
158
170
|
|
|
159
171
|
// src/router.ts
|
|
160
172
|
import { signal } from "@matthesketh/utopia-core";
|
|
173
|
+
var VALID_DOM_ID_RE = /^[A-Za-z0-9_-]+$/;
|
|
161
174
|
var currentRoute = signal(null);
|
|
162
175
|
var isNavigating = signal(false);
|
|
163
176
|
var routes = [];
|
|
@@ -280,10 +293,13 @@ async function navigate(url, options = {}) {
|
|
|
280
293
|
currentRoute.set(match);
|
|
281
294
|
requestAnimationFrame(() => {
|
|
282
295
|
if (fullUrl.hash) {
|
|
283
|
-
const
|
|
284
|
-
if (
|
|
285
|
-
el.
|
|
286
|
-
|
|
296
|
+
const hashId = fullUrl.hash.slice(1);
|
|
297
|
+
if (hashId && VALID_DOM_ID_RE.test(hashId)) {
|
|
298
|
+
const el = document.getElementById(hashId);
|
|
299
|
+
if (el) {
|
|
300
|
+
el.scrollIntoView();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
287
303
|
}
|
|
288
304
|
}
|
|
289
305
|
window.scrollTo(0, 0);
|
|
@@ -528,8 +544,13 @@ function clearContainer(container) {
|
|
|
528
544
|
container.removeChild(container.firstChild);
|
|
529
545
|
}
|
|
530
546
|
}
|
|
547
|
+
var AMPERSAND_RE = /&/g;
|
|
548
|
+
var LESS_THAN_RE = /</g;
|
|
549
|
+
var GREATER_THAN_RE = />/g;
|
|
550
|
+
var DOUBLE_QUOTE_RE = /"/g;
|
|
551
|
+
var SINGLE_QUOTE_RE = /'/g;
|
|
531
552
|
function escapeHtml(str) {
|
|
532
|
-
return str.replace(
|
|
553
|
+
return str.replace(AMPERSAND_RE, "&").replace(LESS_THAN_RE, "<").replace(GREATER_THAN_RE, ">").replace(DOUBLE_QUOTE_RE, """).replace(SINGLE_QUOTE_RE, "'");
|
|
533
554
|
}
|
|
534
555
|
|
|
535
556
|
// src/query.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matthesketh/utopia-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "File-based routing for UtopiaJS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"dist"
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@matthesketh/utopia-core": "0.
|
|
44
|
-
"@matthesketh/utopia-runtime": "0.
|
|
43
|
+
"@matthesketh/utopia-core": "0.5.0",
|
|
44
|
+
"@matthesketh/utopia-runtime": "0.5.0"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build": "tsup src/index.ts --format esm,cjs --dts",
|