@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 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(/\\/g, "/");
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(/\/?\+page\.\w+$/, "");
53
- normalized = normalized.replace(/\/?\+(layout|error)\.\w+$/, "");
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 (/^\(.+\)$/.test(segment)) {
68
+ if (GROUP_SEGMENT_RE.test(segment)) {
58
69
  continue;
59
70
  }
60
- if (/^\[\.\.\..+\]$/.test(segment)) {
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 (/^\[.+\]$/.test(segment)) {
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(/[.*+?^${}()|[\]\\]/g, "\\$&");
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(/\\/g, "/");
131
- if (/\+page\.\w+$/.test(normalized)) {
142
+ const normalized = filePath.replace(BACKSLASH_RE, "/");
143
+ if (PAGE_FILE_TEST_RE.test(normalized)) {
132
144
  pages.set(normalized, importFn);
133
- } else if (/\+layout\.\w+$/.test(normalized)) {
145
+ } else if (LAYOUT_FILE_TEST_RE.test(normalized)) {
134
146
  layouts.set(normalized, importFn);
135
- } else if (/\+error\.\w+$/.test(normalized)) {
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(/\\/g, "/");
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(/\\/g, "/");
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 el = document.getElementById(fullUrl.hash.slice(1));
328
- if (el) {
329
- el.scrollIntoView();
330
- return;
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(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
597
+ return str.replace(AMPERSAND_RE, "&amp;").replace(LESS_THAN_RE, "&lt;").replace(GREATER_THAN_RE, "&gt;").replace(DOUBLE_QUOTE_RE, "&quot;").replace(SINGLE_QUOTE_RE, "&#x27;");
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(/\\/g, "/");
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(/\/?\+page\.\w+$/, "");
9
- normalized = normalized.replace(/\/?\+(layout|error)\.\w+$/, "");
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 (/^\(.+\)$/.test(segment)) {
24
+ if (GROUP_SEGMENT_RE.test(segment)) {
14
25
  continue;
15
26
  }
16
- if (/^\[\.\.\..+\]$/.test(segment)) {
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 (/^\[.+\]$/.test(segment)) {
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(/[.*+?^${}()|[\]\\]/g, "\\$&");
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(/\\/g, "/");
87
- if (/\+page\.\w+$/.test(normalized)) {
98
+ const normalized = filePath.replace(BACKSLASH_RE, "/");
99
+ if (PAGE_FILE_TEST_RE.test(normalized)) {
88
100
  pages.set(normalized, importFn);
89
- } else if (/\+layout\.\w+$/.test(normalized)) {
101
+ } else if (LAYOUT_FILE_TEST_RE.test(normalized)) {
90
102
  layouts.set(normalized, importFn);
91
- } else if (/\+error\.\w+$/.test(normalized)) {
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(/\\/g, "/");
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(/\\/g, "/");
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 el = document.getElementById(fullUrl.hash.slice(1));
284
- if (el) {
285
- el.scrollIntoView();
286
- return;
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(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
553
+ return str.replace(AMPERSAND_RE, "&amp;").replace(LESS_THAN_RE, "&lt;").replace(GREATER_THAN_RE, "&gt;").replace(DOUBLE_QUOTE_RE, "&quot;").replace(SINGLE_QUOTE_RE, "&#x27;");
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.4.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.4.0",
44
- "@matthesketh/utopia-runtime": "0.4.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",