stx-router 0.2.415 → 0.2.416
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/file-requests.d.ts +59 -0
- package/dist/file-requests.js +99 -0
- package/dist/file-router.js +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/matcher.d.ts +21 -18
- package/dist/matcher.js +9 -6
- package/package.json +1 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does this request path ask for a file? True when its last segment ends in
|
|
3
|
+
* one of {@link FILE_REQUEST_EXTENSIONS} (all-lowercase or all-uppercase).
|
|
4
|
+
* Query strings and fragments are ignored; `/` and extensionless paths are
|
|
5
|
+
* never files.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isFileRequestPath(pathname: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Which request paths ask for a FILE rather than a page.
|
|
10
|
+
*
|
|
11
|
+
* An app whose routes start at the root has a dynamic page answering every
|
|
12
|
+
* one-segment path: `resources/views/[username].stx` renders a profile at
|
|
13
|
+
* `/chris`. Without a rule, it also answers `/favicon.ico`,
|
|
14
|
+
* `/apple-touch-icon.png`, `/robots.txt` on a site without one, and every
|
|
15
|
+
* `/wp-login.php` a scanner tries - each a full page render (often one that
|
|
16
|
+
* queries the database for a user called `favicon.ico`) shipped back as 80KB
|
|
17
|
+
* of HTML a browser expected to be an image.
|
|
18
|
+
*
|
|
19
|
+
* The rule: when the LAST segment of a request path ends in a known file
|
|
20
|
+
* extension, a dynamic page segment (`[param]`, `[[param]]`) never captures
|
|
21
|
+
* it. The request falls through to `publicDir`, and when nothing is there it
|
|
22
|
+
* gets a plain `404 Not Found` instead of a page.
|
|
23
|
+
*
|
|
24
|
+
* Why a list of known extensions rather than "anything after a dot":
|
|
25
|
+
*
|
|
26
|
+
* - Identifiers carry dots. A username `john.doe`, a repository `three.js`
|
|
27
|
+
* or `user.github.io`, a version `/docs/v1.2` or `/releases/1.0.0` are
|
|
28
|
+
* all pages. Only an extension that names a file format nobody would use
|
|
29
|
+
* as a name counts, and TLD-shaped ones (`.io`, `.dev`, `.sh`, `.me`,
|
|
30
|
+
* `.app`, `.ai`) are deliberately absent.
|
|
31
|
+
* - Page extensions stay routable: `.html`/`.htm` (the explicit spelling of
|
|
32
|
+
* a page), and `.md`/`.stx` (a docs page addressed by its source name).
|
|
33
|
+
* - Extensions are matched all-lowercase or all-uppercase (`.png`, `.PNG`),
|
|
34
|
+
* which is how files are actually named, and keeps the guard expressible
|
|
35
|
+
* in a case-sensitive route regex.
|
|
36
|
+
*
|
|
37
|
+
* Catch-alls (`[...path]`) are exempt on purpose. A catch-all is how an app
|
|
38
|
+
* says "the rest of the URL is mine, dots and all": a code browser serving
|
|
39
|
+
* `/owner/repo/tree/main/src/index.ts`, a docs site addressing `guide.md`.
|
|
40
|
+
* They still never shadow a file that really exists under `publicDir` (the
|
|
41
|
+
* serve path checks the disk first). That exemption is also the escape hatch:
|
|
42
|
+
* a segment that must accept file-shaped names (`[owner]/[repository]` for a
|
|
43
|
+
* repository called `three.js`) can be written as a catch-all.
|
|
44
|
+
*
|
|
45
|
+
* @module file-requests
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* File extensions that mark a request as a file, never a page. Lowercase; see
|
|
49
|
+
* {@link isFileRequestPath} for how case is handled.
|
|
50
|
+
*/
|
|
51
|
+
export declare const FILE_REQUEST_EXTENSIONS: readonly string[];
|
|
52
|
+
/**
|
|
53
|
+
* Regex source for a lookahead that refuses a final path segment naming a
|
|
54
|
+
* file. Placed in front of a dynamic segment's capture group, so the route
|
|
55
|
+
* regex itself - the one the server matches with and ships to the client
|
|
56
|
+
* router as an owned route - carries the rule. Non-capturing: group indices
|
|
57
|
+
* are unchanged.
|
|
58
|
+
*/
|
|
59
|
+
export declare const FILE_SEGMENT_GUARD: `(?![^/]*\\.(?:${alternation})$)`;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export const FILE_REQUEST_EXTENSIONS = Object.freeze([
|
|
2
|
+
"ico",
|
|
3
|
+
"png",
|
|
4
|
+
"jpg",
|
|
5
|
+
"jpeg",
|
|
6
|
+
"gif",
|
|
7
|
+
"webp",
|
|
8
|
+
"avif",
|
|
9
|
+
"svg",
|
|
10
|
+
"bmp",
|
|
11
|
+
"tif",
|
|
12
|
+
"tiff",
|
|
13
|
+
"heic",
|
|
14
|
+
"css",
|
|
15
|
+
"js",
|
|
16
|
+
"mjs",
|
|
17
|
+
"cjs",
|
|
18
|
+
"map",
|
|
19
|
+
"wasm",
|
|
20
|
+
"webmanifest",
|
|
21
|
+
"json",
|
|
22
|
+
"xml",
|
|
23
|
+
"txt",
|
|
24
|
+
"rss",
|
|
25
|
+
"atom",
|
|
26
|
+
"woff",
|
|
27
|
+
"woff2",
|
|
28
|
+
"ttf",
|
|
29
|
+
"otf",
|
|
30
|
+
"eot",
|
|
31
|
+
"mp3",
|
|
32
|
+
"mp4",
|
|
33
|
+
"m4a",
|
|
34
|
+
"m4v",
|
|
35
|
+
"mov",
|
|
36
|
+
"webm",
|
|
37
|
+
"ogg",
|
|
38
|
+
"ogv",
|
|
39
|
+
"oga",
|
|
40
|
+
"wav",
|
|
41
|
+
"flac",
|
|
42
|
+
"avi",
|
|
43
|
+
"mkv",
|
|
44
|
+
"pdf",
|
|
45
|
+
"csv",
|
|
46
|
+
"doc",
|
|
47
|
+
"docx",
|
|
48
|
+
"xls",
|
|
49
|
+
"xlsx",
|
|
50
|
+
"ppt",
|
|
51
|
+
"pptx",
|
|
52
|
+
"rtf",
|
|
53
|
+
"odt",
|
|
54
|
+
"zip",
|
|
55
|
+
"gz",
|
|
56
|
+
"tgz",
|
|
57
|
+
"tar",
|
|
58
|
+
"rar",
|
|
59
|
+
"7z",
|
|
60
|
+
"bz2",
|
|
61
|
+
"xz",
|
|
62
|
+
"php",
|
|
63
|
+
"phtml",
|
|
64
|
+
"asp",
|
|
65
|
+
"aspx",
|
|
66
|
+
"jsp",
|
|
67
|
+
"cgi",
|
|
68
|
+
"env",
|
|
69
|
+
"ini",
|
|
70
|
+
"yml",
|
|
71
|
+
"yaml",
|
|
72
|
+
"toml",
|
|
73
|
+
"sql",
|
|
74
|
+
"bak",
|
|
75
|
+
"old",
|
|
76
|
+
"swp",
|
|
77
|
+
"log",
|
|
78
|
+
"conf",
|
|
79
|
+
"cfg",
|
|
80
|
+
"exe",
|
|
81
|
+
"dmg",
|
|
82
|
+
"pkg",
|
|
83
|
+
"apk",
|
|
84
|
+
"ipa",
|
|
85
|
+
"msi",
|
|
86
|
+
"iso",
|
|
87
|
+
"bin",
|
|
88
|
+
"dll"
|
|
89
|
+
]);
|
|
90
|
+
const alternation = FILE_REQUEST_EXTENSIONS.flatMap((ext) => ext === ext.toUpperCase() ? [ext] : [ext, ext.toUpperCase()]).join("|"), FILE_SEGMENT = new RegExp(`\\.(?:${alternation})$`);
|
|
91
|
+
export const FILE_SEGMENT_GUARD = `(?![^/]*\\.(?:${alternation})$)`;
|
|
92
|
+
export function isFileRequestPath(pathname) {
|
|
93
|
+
let path = pathname;
|
|
94
|
+
const cut = path.search(/[?#]/);
|
|
95
|
+
if (cut !== -1)
|
|
96
|
+
path = path.slice(0, cut);
|
|
97
|
+
const last = path.slice(path.lastIndexOf("/") + 1);
|
|
98
|
+
return last.length > 0 && FILE_SEGMENT.test(last);
|
|
99
|
+
}
|
package/dist/file-router.js
CHANGED
|
@@ -65,7 +65,7 @@ export class Router {
|
|
|
65
65
|
if (seenPatterns.has(identity))
|
|
66
66
|
continue;
|
|
67
67
|
seenPatterns.add(identity);
|
|
68
|
-
const { regex, params } = patternToRegex(pattern), layouts = config.layouts !== !1 ? resolveLayoutChain(filePath, dir) : [];
|
|
68
|
+
const { regex, params } = patternToRegex(pattern, { page: !0 }), layouts = config.layouts !== !1 ? resolveLayoutChain(filePath, dir) : [];
|
|
69
69
|
this.routes.push({
|
|
70
70
|
pattern,
|
|
71
71
|
regex,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export type { PatternToRegexOptions } from './matcher';
|
|
1
2
|
export type { LayoutMetadata } from './layout-metadata';
|
|
2
3
|
// Types
|
|
3
4
|
export type {
|
|
@@ -24,6 +25,8 @@ export type {
|
|
|
24
25
|
export { createRouter, findErrorPage, formatRoutes, Router } from './file-router';
|
|
25
26
|
// Route pattern matching
|
|
26
27
|
export { bracketPathToRegex, filePathToPattern, matchRoute, patternToRegex } from './matcher';
|
|
28
|
+
// Which request paths ask for a file, never a page
|
|
29
|
+
export { FILE_REQUEST_EXTENSIONS, FILE_SEGMENT_GUARD, isFileRequestPath } from './file-requests';
|
|
27
30
|
// Named routes
|
|
28
31
|
export { defineRoute, defineRoutes, resetRoutes, route, setAppUrl } from './named-routes';
|
|
29
32
|
// Middleware system
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { createRouter, findErrorPage, formatRoutes, Router } from "./file-router.js";
|
|
2
2
|
export { bracketPathToRegex, filePathToPattern, matchRoute, patternToRegex } from "./matcher.js";
|
|
3
|
+
export { FILE_REQUEST_EXTENSIONS, FILE_SEGMENT_GUARD, isFileRequestPath } from "./file-requests.js";
|
|
3
4
|
export { defineRoute, defineRoutes, resetRoutes, route, setAppUrl } from "./named-routes.js";
|
|
4
5
|
export {
|
|
5
6
|
abortNavigation,
|
package/dist/matcher.d.ts
CHANGED
|
@@ -1,21 +1,5 @@
|
|
|
1
1
|
export declare function filePathToPattern(filePath: string, pagesDir: string): string;
|
|
2
|
-
|
|
3
|
-
* Compile a route pattern into a regex and the parameter names its capture
|
|
4
|
-
* groups correspond to.
|
|
5
|
-
*
|
|
6
|
-
* Single left-to-right pass, which is the whole point. Doing it as three
|
|
7
|
-
* successive `.replace()` sweeps - optional, then catch-all, then required -
|
|
8
|
-
* collects the names in *sweep* order while the capture groups end up in
|
|
9
|
-
* *pattern* order, and the two disagree the moment one pattern mixes kinds.
|
|
10
|
-
*
|
|
11
|
-
* `/:owner/:repository/tree/:ref/:path*` collected as
|
|
12
|
-
* `[path, owner, repository, ref]` against groups `(owner)(repository)(ref)(path)`,
|
|
13
|
-
* so every value was bound to the wrong name: a repository browser given a URL
|
|
14
|
-
* showed the root of a repository named after the branch. Nothing errored -
|
|
15
|
-
* the regex matched and the page rendered - which is why it reads as a routing
|
|
16
|
-
* mystery rather than an off-by-one.
|
|
17
|
-
*/
|
|
18
|
-
export declare function patternToRegex(pattern: string): { regex: RegExp, params: string[] };
|
|
2
|
+
export declare function patternToRegex(pattern: string, options?: PatternToRegexOptions): { regex: RegExp, params: string[] };
|
|
19
3
|
export declare function matchRoute(pathname: string, routes: { pattern: string, regex: RegExp, params: string[] }[]): { route: typeof routes[0], params: Record<string, string> } | null;
|
|
20
4
|
/**
|
|
21
5
|
* Compile a *file-style* route path - the one with `[param]` brackets - into a
|
|
@@ -35,4 +19,23 @@ export declare function matchRoute(pathname: string, routes: { pattern: string,
|
|
|
35
19
|
*
|
|
36
20
|
* Exported so nothing has to write those two lines again.
|
|
37
21
|
*/
|
|
38
|
-
export declare function bracketPathToRegex(routePath: string): { regex: RegExp, params: string[] };
|
|
22
|
+
export declare function bracketPathToRegex(routePath: string, options?: PatternToRegexOptions): { regex: RegExp, params: string[] };
|
|
23
|
+
/**
|
|
24
|
+
* Compile a route pattern into a regex and the parameter names its capture
|
|
25
|
+
* groups correspond to.
|
|
26
|
+
*
|
|
27
|
+
* Single left-to-right pass, which is the whole point. Doing it as three
|
|
28
|
+
* successive `.replace()` sweeps - optional, then catch-all, then required -
|
|
29
|
+
* collects the names in *sweep* order while the capture groups end up in
|
|
30
|
+
* *pattern* order, and the two disagree the moment one pattern mixes kinds.
|
|
31
|
+
*
|
|
32
|
+
* `/:owner/:repository/tree/:ref/:path*` collected as
|
|
33
|
+
* `[path, owner, repository, ref]` against groups `(owner)(repository)(ref)(path)`,
|
|
34
|
+
* so every value was bound to the wrong name: a repository browser given a URL
|
|
35
|
+
* showed the root of a repository named after the branch. Nothing errored -
|
|
36
|
+
* the regex matched and the page rendered - which is why it reads as a routing
|
|
37
|
+
* mystery rather than an off-by-one.
|
|
38
|
+
*/
|
|
39
|
+
export declare interface PatternToRegexOptions {
|
|
40
|
+
page?: boolean
|
|
41
|
+
}
|
package/dist/matcher.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { FILE_SEGMENT_GUARD } from "./file-requests.js";
|
|
2
3
|
export function filePathToPattern(filePath, pagesDir) {
|
|
3
4
|
let route = path.relative(pagesDir, filePath).replace(/\.(stx|md)$/, "");
|
|
4
5
|
route = route.split(path.sep).join("/");
|
|
@@ -11,14 +12,15 @@ export function filePathToPattern(filePath, pagesDir) {
|
|
|
11
12
|
route = route.replace(/:\.\.\.([^/]+)/g, ":$1*");
|
|
12
13
|
return `/${route}`;
|
|
13
14
|
}
|
|
14
|
-
export function patternToRegex(pattern) {
|
|
15
|
-
const params = [];
|
|
15
|
+
export function patternToRegex(pattern, options = {}) {
|
|
16
|
+
const guard = options.page ? FILE_SEGMENT_GUARD : "", params = [];
|
|
16
17
|
let regexStr = "", i = 0;
|
|
17
18
|
while (i < pattern.length) {
|
|
18
19
|
const rest = pattern.slice(i), optional = /^\/:([^/?*]+)\?/.exec(rest);
|
|
19
20
|
if (optional) {
|
|
20
21
|
params.push(optional[1]);
|
|
21
|
-
|
|
22
|
+
const last = i + optional[0].length === pattern.length;
|
|
23
|
+
regexStr += `(?:/${last ? guard : ""}([^/]+))?`;
|
|
22
24
|
i += optional[0].length;
|
|
23
25
|
continue;
|
|
24
26
|
}
|
|
@@ -32,7 +34,8 @@ export function patternToRegex(pattern) {
|
|
|
32
34
|
const required = /^:([^/?*]+)/.exec(rest);
|
|
33
35
|
if (required) {
|
|
34
36
|
params.push(required[1]);
|
|
35
|
-
|
|
37
|
+
const last = i + required[0].length === pattern.length;
|
|
38
|
+
regexStr += `${last ? guard : ""}([^/]+)`;
|
|
36
39
|
i += required[0].length;
|
|
37
40
|
continue;
|
|
38
41
|
}
|
|
@@ -55,7 +58,7 @@ export function matchRoute(pathname, routes) {
|
|
|
55
58
|
}
|
|
56
59
|
return null;
|
|
57
60
|
}
|
|
58
|
-
export function bracketPathToRegex(routePath) {
|
|
61
|
+
export function bracketPathToRegex(routePath, options = {}) {
|
|
59
62
|
const pattern = routePath.replace(/\[\[([^\]]+)\]\]/g, ":$1?").replace(/\[([^\]]+)\]/g, ":$1").replace(/:\.\.\.([^/]+)/g, ":$1*");
|
|
60
|
-
return patternToRegex(pattern);
|
|
63
|
+
return patternToRegex(pattern, options);
|
|
61
64
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stx-router",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.416",
|
|
5
5
|
"description": "File-based router for STX — .stx template discovery, nested layouts, typed route params, middleware, and client-side SPA navigation.",
|
|
6
6
|
"author": "Chris Breuer <chris@stacksjs.org>",
|
|
7
7
|
"license": "MIT",
|