react-bun-ssr 0.1.0 → 0.2.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/README.md +116 -132
- package/framework/cli/commands.ts +59 -223
- package/framework/cli/dev-client-watch.ts +281 -0
- package/framework/cli/dev-route-table.ts +71 -0
- package/framework/cli/dev-runtime.ts +382 -0
- package/framework/cli/internal.ts +138 -0
- package/framework/cli/main.ts +27 -31
- package/framework/runtime/build-tools.ts +280 -57
- package/framework/runtime/bun-route-adapter.ts +20 -7
- package/framework/runtime/client-runtime.tsx +1218 -15
- package/framework/runtime/client-transition-core.ts +159 -0
- package/framework/runtime/config.ts +4 -1
- package/framework/runtime/index.ts +6 -0
- package/framework/runtime/io.ts +1 -1
- package/framework/runtime/link.tsx +205 -0
- package/framework/runtime/markdown-headings.ts +54 -0
- package/framework/runtime/markdown-routes.ts +9 -40
- package/framework/runtime/matcher.ts +11 -11
- package/framework/runtime/module-loader.ts +215 -52
- package/framework/runtime/navigation-api.ts +223 -0
- package/framework/runtime/render.tsx +105 -105
- package/framework/runtime/route-api.ts +6 -0
- package/framework/runtime/route-errors.ts +166 -0
- package/framework/runtime/router.ts +80 -0
- package/framework/runtime/runtime-constants.ts +4 -0
- package/framework/runtime/server.ts +713 -145
- package/framework/runtime/tree.tsx +171 -3
- package/framework/runtime/types.ts +81 -3
- package/framework/runtime/utils.ts +9 -5
- package/package.json +19 -5
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { matchRouteBySegments } from "./matcher";
|
|
2
|
+
import type {
|
|
3
|
+
ClientRouteSnapshot,
|
|
4
|
+
Params,
|
|
5
|
+
TransitionChunk,
|
|
6
|
+
TransitionDeferredChunk,
|
|
7
|
+
TransitionDocumentChunk,
|
|
8
|
+
TransitionInitialChunk,
|
|
9
|
+
TransitionRedirectChunk,
|
|
10
|
+
} from "./types";
|
|
11
|
+
|
|
12
|
+
export const PREFETCH_TTL_MS = 30_000;
|
|
13
|
+
export const MAX_REDIRECT_DEPTH = 8;
|
|
14
|
+
|
|
15
|
+
export interface TransitionChunkParserState {
|
|
16
|
+
buffer: string;
|
|
17
|
+
initialChunk: TransitionInitialChunk | TransitionRedirectChunk | TransitionDocumentChunk | null;
|
|
18
|
+
deferredChunks: TransitionDeferredChunk[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TransitionNavigationOptions {
|
|
22
|
+
historyManagedByNavigationApi?: boolean;
|
|
23
|
+
isPopState?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createTransitionChunkParserState(): TransitionChunkParserState {
|
|
27
|
+
return {
|
|
28
|
+
buffer: "",
|
|
29
|
+
initialChunk: null,
|
|
30
|
+
deferredChunks: [],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function matchClientPageRoute(
|
|
35
|
+
routes: ClientRouteSnapshot[],
|
|
36
|
+
pathname: string,
|
|
37
|
+
): { route: ClientRouteSnapshot; params: Params } | null {
|
|
38
|
+
return matchRouteBySegments(routes, pathname);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function applyParsedTransitionChunk(
|
|
42
|
+
state: TransitionChunkParserState,
|
|
43
|
+
chunk: TransitionChunk,
|
|
44
|
+
): TransitionChunkParserState {
|
|
45
|
+
if (chunk.type === "initial" || chunk.type === "redirect" || chunk.type === "document") {
|
|
46
|
+
if (state.initialChunk) {
|
|
47
|
+
return state;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
...state,
|
|
52
|
+
initialChunk: chunk,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
...state,
|
|
58
|
+
deferredChunks: [...state.deferredChunks, chunk],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function consumeTransitionChunkText(
|
|
63
|
+
state: TransitionChunkParserState,
|
|
64
|
+
text: string,
|
|
65
|
+
): TransitionChunkParserState {
|
|
66
|
+
let buffer = state.buffer + text;
|
|
67
|
+
let nextState = {
|
|
68
|
+
...state,
|
|
69
|
+
buffer: "",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
let start = 0;
|
|
73
|
+
for (let index = 0; index < buffer.length; index += 1) {
|
|
74
|
+
if (buffer[index] !== "\n") {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const line = buffer.slice(start, index).trim();
|
|
79
|
+
if (line.length > 0) {
|
|
80
|
+
nextState = applyParsedTransitionChunk(
|
|
81
|
+
nextState,
|
|
82
|
+
JSON.parse(line) as TransitionChunk,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
start = index + 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
buffer = buffer.slice(start);
|
|
89
|
+
return {
|
|
90
|
+
...nextState,
|
|
91
|
+
buffer,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function flushTransitionChunkText(
|
|
96
|
+
state: TransitionChunkParserState,
|
|
97
|
+
): TransitionChunkParserState {
|
|
98
|
+
const trailing = state.buffer.trim();
|
|
99
|
+
if (trailing.length === 0) {
|
|
100
|
+
return {
|
|
101
|
+
...state,
|
|
102
|
+
buffer: "",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
...applyParsedTransitionChunk(
|
|
108
|
+
{
|
|
109
|
+
...state,
|
|
110
|
+
buffer: "",
|
|
111
|
+
},
|
|
112
|
+
JSON.parse(trailing) as TransitionChunk,
|
|
113
|
+
),
|
|
114
|
+
buffer: "",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function sanitizePrefetchCache<T extends { createdAt: number }>(
|
|
119
|
+
cache: Map<string, T>,
|
|
120
|
+
options: {
|
|
121
|
+
now?: number;
|
|
122
|
+
ttlMs?: number;
|
|
123
|
+
} = {},
|
|
124
|
+
): void {
|
|
125
|
+
const now = options.now ?? Date.now();
|
|
126
|
+
const ttlMs = options.ttlMs ?? PREFETCH_TTL_MS;
|
|
127
|
+
|
|
128
|
+
for (const [key, entry] of cache.entries()) {
|
|
129
|
+
if (now - entry.createdAt > ttlMs) {
|
|
130
|
+
cache.delete(key);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function shouldSkipSoftNavigation(
|
|
136
|
+
currentPath: string,
|
|
137
|
+
targetPath: string,
|
|
138
|
+
options: TransitionNavigationOptions,
|
|
139
|
+
): boolean {
|
|
140
|
+
return (
|
|
141
|
+
currentPath === targetPath
|
|
142
|
+
&& !options.isPopState
|
|
143
|
+
&& !options.historyManagedByNavigationApi
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function shouldHardNavigateForRedirectDepth(
|
|
148
|
+
depth: number,
|
|
149
|
+
maxDepth = MAX_REDIRECT_DEPTH,
|
|
150
|
+
): boolean {
|
|
151
|
+
return depth > maxDepth;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function isStaleNavigationToken(
|
|
155
|
+
activeToken: number,
|
|
156
|
+
candidateToken: number,
|
|
157
|
+
): boolean {
|
|
158
|
+
return activeToken !== candidateToken;
|
|
159
|
+
}
|
|
@@ -101,6 +101,8 @@ export function resolveConfig(config: FrameworkConfig = {}, cwd = process.cwd())
|
|
|
101
101
|
const rootModule = path.resolve(appDir, config.rootModule ?? "root.tsx");
|
|
102
102
|
const middlewareFile = path.resolve(appDir, config.middlewareFile ?? "middleware.ts");
|
|
103
103
|
const distDir = path.resolve(cwd, config.distDir ?? "dist");
|
|
104
|
+
const mode = config.mode ?? (process.env.NODE_ENV === "production" ? "production" : "development");
|
|
105
|
+
const serverBytecode = config.serverBytecode ?? mode === "production";
|
|
104
106
|
const headerRules = toHeaderRules(config);
|
|
105
107
|
|
|
106
108
|
return {
|
|
@@ -113,7 +115,8 @@ export function resolveConfig(config: FrameworkConfig = {}, cwd = process.cwd())
|
|
|
113
115
|
distDir,
|
|
114
116
|
host: config.host ?? "0.0.0.0",
|
|
115
117
|
port: config.port ?? 3000,
|
|
116
|
-
mode
|
|
118
|
+
mode,
|
|
119
|
+
serverBytecode,
|
|
117
120
|
headerRules,
|
|
118
121
|
};
|
|
119
122
|
}
|
|
@@ -16,9 +16,15 @@ export type {
|
|
|
16
16
|
RedirectResult,
|
|
17
17
|
ResponseHeaderRule,
|
|
18
18
|
RequestContext,
|
|
19
|
+
RouteCatchContext,
|
|
20
|
+
RouteErrorContext,
|
|
21
|
+
RouteErrorResponse,
|
|
19
22
|
RouteModule,
|
|
20
23
|
} from "./types";
|
|
21
24
|
|
|
22
25
|
export { createServer, startHttpServer } from "./server";
|
|
23
26
|
export { defer, json, redirect, defineConfig } from "./helpers";
|
|
27
|
+
export { isRouteErrorResponse, notFound, routeError } from "./route-errors";
|
|
28
|
+
export { Link, type LinkProps } from "./link";
|
|
29
|
+
export { useRouter, type Router, type RouterNavigateOptions } from "./router";
|
|
24
30
|
export { Outlet, useLoaderData, useParams, useRequestUrl, useRouteError } from "./tree";
|
package/framework/runtime/io.ts
CHANGED
|
@@ -122,7 +122,7 @@ export async function listEntries(dirPath: string): Promise<FileEntry[]> {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
export async function makeTempDir(prefix: string): Promise<string> {
|
|
125
|
-
const dirPath = path.join("/tmp", `${prefix}-${
|
|
125
|
+
const dirPath = path.join("/tmp", `${prefix}-${Bun.randomUUIDv7()}`);
|
|
126
126
|
await ensureDir(dirPath);
|
|
127
127
|
return dirPath;
|
|
128
128
|
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AnchorHTMLAttributes,
|
|
3
|
+
FocusEvent,
|
|
4
|
+
MouseEvent,
|
|
5
|
+
TouchEvent,
|
|
6
|
+
} from "react";
|
|
7
|
+
|
|
8
|
+
interface NavigateInfo {
|
|
9
|
+
from: string;
|
|
10
|
+
to: string;
|
|
11
|
+
status: number;
|
|
12
|
+
kind: "page" | "not_found" | "catch" | "error";
|
|
13
|
+
redirected: boolean;
|
|
14
|
+
prefetched: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LinkProps
|
|
18
|
+
extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
|
|
19
|
+
to: string;
|
|
20
|
+
replace?: boolean;
|
|
21
|
+
scroll?: boolean;
|
|
22
|
+
prefetch?: "intent" | "none";
|
|
23
|
+
onNavigate?: (info: NavigateInfo) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function shouldHandleNavigation(event: MouseEvent<HTMLAnchorElement>): boolean {
|
|
27
|
+
if (event.defaultPrevented) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (event.button !== 0) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toAbsoluteHref(to: string): string {
|
|
43
|
+
if (typeof window === "undefined") {
|
|
44
|
+
return to;
|
|
45
|
+
}
|
|
46
|
+
return new URL(to, window.location.href).toString();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isInternalHref(href: string): boolean {
|
|
50
|
+
if (typeof window === "undefined") {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const url = new URL(href, window.location.href);
|
|
56
|
+
return url.origin === window.location.origin;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isSameDocumentHashNavigation(href: string): boolean {
|
|
63
|
+
if (typeof window === "undefined") {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const target = new URL(href, window.location.href);
|
|
69
|
+
return (
|
|
70
|
+
target.origin === window.location.origin
|
|
71
|
+
&& target.pathname === window.location.pathname
|
|
72
|
+
&& target.search === window.location.search
|
|
73
|
+
&& target.hash.length > 0
|
|
74
|
+
);
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function prefetch(href: string): Promise<void> {
|
|
81
|
+
if (typeof window === "undefined") {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const runtime = await import("./client-runtime");
|
|
86
|
+
await runtime.prefetchTo(href);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function navigate(href: string, options: {
|
|
90
|
+
replace?: boolean;
|
|
91
|
+
scroll?: boolean;
|
|
92
|
+
onNavigate?: (info: NavigateInfo) => void;
|
|
93
|
+
}): Promise<void> {
|
|
94
|
+
if (typeof window === "undefined") {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const runtime = await import("./client-runtime");
|
|
99
|
+
await runtime.navigateWithNavigationApiOrFallback(href, {
|
|
100
|
+
replace: options.replace,
|
|
101
|
+
scroll: options.scroll,
|
|
102
|
+
onNavigate: options.onNavigate,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function Link(props: LinkProps) {
|
|
107
|
+
const {
|
|
108
|
+
to,
|
|
109
|
+
replace = false,
|
|
110
|
+
scroll = true,
|
|
111
|
+
prefetch: prefetchMode = "intent",
|
|
112
|
+
onNavigate,
|
|
113
|
+
onMouseEnter,
|
|
114
|
+
onTouchStart,
|
|
115
|
+
onFocus,
|
|
116
|
+
onClick,
|
|
117
|
+
target,
|
|
118
|
+
rel,
|
|
119
|
+
download,
|
|
120
|
+
...rest
|
|
121
|
+
} = props;
|
|
122
|
+
|
|
123
|
+
const href = to;
|
|
124
|
+
const resolvedHref = toAbsoluteHref(to);
|
|
125
|
+
|
|
126
|
+
const maybePrefetch = (): void => {
|
|
127
|
+
if (prefetchMode !== "intent") {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!isInternalHref(resolvedHref)) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
void prefetch(resolvedHref);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const handleMouseEnter = (event: MouseEvent<HTMLAnchorElement>): void => {
|
|
139
|
+
onMouseEnter?.(event);
|
|
140
|
+
if (event.defaultPrevented) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
maybePrefetch();
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const handleTouchStart = (event: TouchEvent<HTMLAnchorElement>): void => {
|
|
147
|
+
onTouchStart?.(event);
|
|
148
|
+
if (event.defaultPrevented) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
maybePrefetch();
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const handleFocus = (event: FocusEvent<HTMLAnchorElement>): void => {
|
|
155
|
+
onFocus?.(event);
|
|
156
|
+
if (event.defaultPrevented) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
maybePrefetch();
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const handleClick = (event: MouseEvent<HTMLAnchorElement>): void => {
|
|
163
|
+
onClick?.(event);
|
|
164
|
+
if (!shouldHandleNavigation(event)) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (download !== undefined) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (target && target !== "_self") {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!isInternalHref(resolvedHref)) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (isSameDocumentHashNavigation(resolvedHref)) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
event.preventDefault();
|
|
185
|
+
void navigate(resolvedHref, {
|
|
186
|
+
replace,
|
|
187
|
+
scroll,
|
|
188
|
+
onNavigate,
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<a
|
|
194
|
+
{...rest}
|
|
195
|
+
href={href}
|
|
196
|
+
target={target}
|
|
197
|
+
rel={rel}
|
|
198
|
+
download={download}
|
|
199
|
+
onClick={handleClick}
|
|
200
|
+
onMouseEnter={handleMouseEnter}
|
|
201
|
+
onTouchStart={handleTouchStart}
|
|
202
|
+
onFocus={handleFocus}
|
|
203
|
+
/>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface MarkdownHeadingEntry {
|
|
2
|
+
text: string;
|
|
3
|
+
id: string;
|
|
4
|
+
depth: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function decodeHtml(value: string): string {
|
|
8
|
+
return value
|
|
9
|
+
.replace(/</g, '<')
|
|
10
|
+
.replace(/>/g, '>')
|
|
11
|
+
.replace(/"/g, '"')
|
|
12
|
+
.replace(/'/g, "'")
|
|
13
|
+
.replace(/&/g, '&');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function plainTextFromHtml(value: string): string {
|
|
17
|
+
return decodeHtml(value.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function slugifyHeading(value: string): string {
|
|
21
|
+
const slug = value
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.normalize('NFKD')
|
|
24
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
25
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
26
|
+
.replace(/^-+|-+$/g, '');
|
|
27
|
+
|
|
28
|
+
return slug || 'section';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function addHeadingIds(html: string): string {
|
|
32
|
+
const seen = new Map<string, number>();
|
|
33
|
+
|
|
34
|
+
return html.replace(/<h([1-6])(\b[^>]*)>([\s\S]*?)<\/h\1>/g, (match, depth, attrs, inner) => {
|
|
35
|
+
const existingId = /\sid="([^"]+)"/.exec(attrs)?.[1];
|
|
36
|
+
if (existingId) {
|
|
37
|
+
return match;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const base = slugifyHeading(plainTextFromHtml(inner));
|
|
41
|
+
const count = seen.get(base) ?? 0;
|
|
42
|
+
seen.set(base, count + 1);
|
|
43
|
+
const id = count === 0 ? base : `${base}-${count + 1}`;
|
|
44
|
+
return `<h${depth}${attrs} id="${Bun.escapeHTML(id)}">${inner}</h${depth}>`;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function extractHeadingEntriesFromHtml(html: string): MarkdownHeadingEntry[] {
|
|
49
|
+
return Array.from(html.matchAll(/<h([1-6])\b[^>]*id="([^"]+)"[^>]*>([\s\S]*?)<\/h\1>/g)).map(match => ({
|
|
50
|
+
depth: Number(match[1]),
|
|
51
|
+
id: match[2] ?? '',
|
|
52
|
+
text: plainTextFromHtml(match[3] ?? ''),
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { addHeadingIds } from "./markdown-headings";
|
|
2
3
|
import { existsPath, readText, writeTextIfChanged } from "./io";
|
|
3
4
|
import { normalizeSlashes, stableHash, trimFileExtension } from "./utils";
|
|
4
5
|
|
|
5
6
|
const compiledMarkdownCache = new Map<string, { sourceHash: string; outputPath: string }>();
|
|
6
|
-
const
|
|
7
|
-
const MARKDOWN_WRAPPER_VERSION = "2";
|
|
7
|
+
const MARKDOWN_WRAPPER_VERSION = "3";
|
|
8
8
|
|
|
9
9
|
interface ParsedFrontmatter {
|
|
10
10
|
title?: string;
|
|
@@ -22,15 +22,6 @@ function decodeHtml(value: string): string {
|
|
|
22
22
|
.replace(/&/g, "&");
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function escapeHtml(value: string): string {
|
|
26
|
-
return value
|
|
27
|
-
.replace(/&/g, "&")
|
|
28
|
-
.replace(/</g, "<")
|
|
29
|
-
.replace(/>/g, ">")
|
|
30
|
-
.replace(/\"/g, """)
|
|
31
|
-
.replace(/'/g, "'");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
25
|
function highlightWithRegex(
|
|
35
26
|
source: string,
|
|
36
27
|
regex: RegExp,
|
|
@@ -45,16 +36,16 @@ function highlightWithRegex(
|
|
|
45
36
|
const index = match.index;
|
|
46
37
|
|
|
47
38
|
if (index > cursor) {
|
|
48
|
-
html +=
|
|
39
|
+
html += Bun.escapeHTML(source.slice(cursor, index));
|
|
49
40
|
}
|
|
50
41
|
|
|
51
|
-
html += `<span class="token ${classify(value)}">${
|
|
42
|
+
html += `<span class="token ${classify(value)}">${Bun.escapeHTML(value)}</span>`;
|
|
52
43
|
cursor = index + value.length;
|
|
53
44
|
match = regex.exec(source);
|
|
54
45
|
}
|
|
55
46
|
|
|
56
47
|
if (cursor < source.length) {
|
|
57
|
-
html +=
|
|
48
|
+
html += Bun.escapeHTML(source.slice(cursor));
|
|
58
49
|
}
|
|
59
50
|
|
|
60
51
|
return html;
|
|
@@ -96,7 +87,7 @@ function highlightCode(source: string, language: string): string {
|
|
|
96
87
|
});
|
|
97
88
|
}
|
|
98
89
|
|
|
99
|
-
return
|
|
90
|
+
return Bun.escapeHTML(source);
|
|
100
91
|
}
|
|
101
92
|
|
|
102
93
|
function applySyntaxHighlight(html: string): string {
|
|
@@ -105,7 +96,7 @@ function applySyntaxHighlight(html: string): string {
|
|
|
105
96
|
(_match, language: string, rawCode: string) => {
|
|
106
97
|
const code = decodeHtml(rawCode);
|
|
107
98
|
const highlighted = highlightCode(code, language);
|
|
108
|
-
return `<pre><code class="language-${
|
|
99
|
+
return `<pre><code class="language-${Bun.escapeHTML(language)}">${highlighted}</code></pre>`;
|
|
109
100
|
},
|
|
110
101
|
);
|
|
111
102
|
}
|
|
@@ -121,24 +112,11 @@ function resolveGeneratedRoot(routesDir: string, generatedMarkdownRootDir?: stri
|
|
|
121
112
|
return path.resolve(appRoutesMatch[1]!, ".rbssr", "generated", "markdown-routes");
|
|
122
113
|
}
|
|
123
114
|
|
|
124
|
-
const snapshotRoutesMatch = normalizedRoutesDir.match(
|
|
125
|
-
/^(.*)\/\.rbssr\/dev\/server-snapshots\/v\d+\/routes$/,
|
|
126
|
-
);
|
|
127
|
-
if (snapshotRoutesMatch) {
|
|
128
|
-
return path.resolve(snapshotRoutesMatch[1]!, ".rbssr", "generated", "markdown-routes");
|
|
129
|
-
}
|
|
130
|
-
|
|
131
115
|
return path.resolve(routesDir, "..", ".rbssr", "generated", "markdown-routes");
|
|
132
116
|
}
|
|
133
117
|
|
|
134
118
|
function toRouteGroupKey(routesDir: string): string {
|
|
135
|
-
|
|
136
|
-
const canonical = normalized.replace(
|
|
137
|
-
/\/\.rbssr\/dev\/server-snapshots\/v\d+\/routes$/,
|
|
138
|
-
"/.rbssr/dev/server-snapshots/routes",
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
return stableHash(`${MARKDOWN_WRAPPER_VERSION}\0${canonical}`);
|
|
119
|
+
return stableHash(`${MARKDOWN_WRAPPER_VERSION}\0${normalizeSlashes(path.resolve(routesDir))}`);
|
|
142
120
|
}
|
|
143
121
|
|
|
144
122
|
function parseFrontmatter(raw: string): {
|
|
@@ -172,15 +150,6 @@ function parseFrontmatter(raw: string): {
|
|
|
172
150
|
values.set(key, value);
|
|
173
151
|
}
|
|
174
152
|
|
|
175
|
-
for (const key of REQUIRED_FRONTMATTER_FIELDS) {
|
|
176
|
-
if (!values.has(key)) {
|
|
177
|
-
return {
|
|
178
|
-
frontmatter: { tags: [] },
|
|
179
|
-
markdown: raw,
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
153
|
const tags = (values.get("tags") ?? "")
|
|
185
154
|
.split(",")
|
|
186
155
|
.map(value => value.trim())
|
|
@@ -304,7 +273,7 @@ export async function compileMarkdownRouteModule(options: {
|
|
|
304
273
|
}
|
|
305
274
|
|
|
306
275
|
const parsed = parseFrontmatter(markdownSource);
|
|
307
|
-
const highlightedHtml = applySyntaxHighlight(Bun.markdown.html(parsed.markdown));
|
|
276
|
+
const highlightedHtml = applySyntaxHighlight(addHeadingIds(Bun.markdown.html(parsed.markdown)));
|
|
308
277
|
const html = parsed.frontmatter.title ? stripLeadingH1(highlightedHtml) : highlightedHtml;
|
|
309
278
|
await writeFileIfChanged(
|
|
310
279
|
outputPath,
|
|
@@ -60,10 +60,10 @@ function matchSegments(segments: RouteSegment[], pathname: string): Params | nul
|
|
|
60
60
|
return params;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
export function
|
|
64
|
-
routes:
|
|
63
|
+
export function matchRouteBySegments<T extends { segments: RouteSegment[] }>(
|
|
64
|
+
routes: T[],
|
|
65
65
|
pathname: string,
|
|
66
|
-
):
|
|
66
|
+
): { route: T; params: Params } | null {
|
|
67
67
|
for (const route of routes) {
|
|
68
68
|
const params = matchSegments(route.segments, pathname);
|
|
69
69
|
if (params) {
|
|
@@ -74,16 +74,16 @@ export function matchPageRoute(
|
|
|
74
74
|
return null;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
export function matchPageRoute(
|
|
78
|
+
routes: PageRouteDefinition[],
|
|
79
|
+
pathname: string,
|
|
80
|
+
): RouteMatch<PageRouteDefinition> | null {
|
|
81
|
+
return matchRouteBySegments(routes, pathname);
|
|
82
|
+
}
|
|
83
|
+
|
|
77
84
|
export function matchApiRoute(
|
|
78
85
|
routes: ApiRouteDefinition[],
|
|
79
86
|
pathname: string,
|
|
80
87
|
): RouteMatch<ApiRouteDefinition> | null {
|
|
81
|
-
|
|
82
|
-
const params = matchSegments(route.segments, pathname);
|
|
83
|
-
if (params) {
|
|
84
|
-
return { route, params };
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return null;
|
|
88
|
+
return matchRouteBySegments(routes, pathname);
|
|
89
89
|
}
|