rari 0.3.2 → 0.4.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/{app-routes-DIZ265rW.js → app-routes-DnV_PBQL.js} +60 -6
- package/dist/{app-routes-BvbStRcg.js → app-routes-cr6yLAYA.js} +1 -1
- package/dist/{app-types-DUzcpcTH.d.ts → app-types-BwXHrEWG.d.ts} +10 -1
- package/dist/client.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/server.d.ts +29 -2
- package/dist/server.js +77 -6
- package/dist/{vite-BzuW6jU8.js → vite-C3iz9pDD.js} +7 -2
- package/package.json +10 -10
- package/src/api-routes.ts +49 -0
- package/src/router/app-routes.ts +75 -2
- package/src/router/app-types.ts +10 -0
- package/src/router/vite-plugin.ts +71 -2
- package/src/runtime/api-request.ts +158 -0
- package/src/server.ts +11 -0
- package/src/vite/index.ts +7 -2
|
@@ -9,13 +9,23 @@ const SPECIAL_FILES = {
|
|
|
9
9
|
ERROR: "error",
|
|
10
10
|
NOT_FOUND: "not-found",
|
|
11
11
|
TEMPLATE: "template",
|
|
12
|
-
DEFAULT: "default"
|
|
12
|
+
DEFAULT: "default",
|
|
13
|
+
ROUTE: "route"
|
|
13
14
|
};
|
|
14
15
|
const SEGMENT_PATTERNS = {
|
|
15
16
|
DYNAMIC: /^\[([^\]]+)\]$/,
|
|
16
17
|
CATCH_ALL: /^\[\.\.\.([^\]]+)\]$/,
|
|
17
18
|
OPTIONAL_CATCH_ALL: /^\[\[\.\.\.([^\]]+)\]\]$/
|
|
18
19
|
};
|
|
20
|
+
const HTTP_METHODS = [
|
|
21
|
+
"GET",
|
|
22
|
+
"POST",
|
|
23
|
+
"PUT",
|
|
24
|
+
"DELETE",
|
|
25
|
+
"PATCH",
|
|
26
|
+
"HEAD",
|
|
27
|
+
"OPTIONS"
|
|
28
|
+
];
|
|
19
29
|
var AppRouteGenerator = class {
|
|
20
30
|
appDir;
|
|
21
31
|
extensions;
|
|
@@ -37,12 +47,14 @@ var AppRouteGenerator = class {
|
|
|
37
47
|
const loading = [];
|
|
38
48
|
const errors = [];
|
|
39
49
|
const notFound = [];
|
|
40
|
-
|
|
50
|
+
const apiRoutes = [];
|
|
51
|
+
await this.scanDirectory("", routes, layouts, loading, errors, notFound, apiRoutes);
|
|
41
52
|
if (this.verbose) {
|
|
42
53
|
console.warn(`[AppRouter] Found ${routes.length} routes`);
|
|
43
54
|
console.warn(`[AppRouter] Found ${layouts.length} layouts`);
|
|
44
55
|
console.warn(`[AppRouter] Found ${loading.length} loading components`);
|
|
45
56
|
console.warn(`[AppRouter] Found ${errors.length} error boundaries`);
|
|
57
|
+
console.warn(`[AppRouter] Found ${apiRoutes.length} API routes`);
|
|
46
58
|
}
|
|
47
59
|
return {
|
|
48
60
|
routes: this.sortRoutes(routes),
|
|
@@ -50,10 +62,11 @@ var AppRouteGenerator = class {
|
|
|
50
62
|
loading,
|
|
51
63
|
errors,
|
|
52
64
|
notFound,
|
|
65
|
+
apiRoutes: this.sortApiRoutes(apiRoutes),
|
|
53
66
|
generated: (/* @__PURE__ */ new Date()).toISOString()
|
|
54
67
|
};
|
|
55
68
|
}
|
|
56
|
-
async scanDirectory(relativePath, routes, layouts, loading, errors, notFound) {
|
|
69
|
+
async scanDirectory(relativePath, routes, layouts, loading, errors, notFound, apiRoutes) {
|
|
57
70
|
const fullPath = path.join(this.appDir, relativePath);
|
|
58
71
|
let entries;
|
|
59
72
|
try {
|
|
@@ -70,13 +83,13 @@ var AppRouteGenerator = class {
|
|
|
70
83
|
if (this.shouldScanDirectory(entry)) dirs.push(entry);
|
|
71
84
|
} else if (stat.isFile()) files.push(entry);
|
|
72
85
|
}
|
|
73
|
-
await this.processSpecialFiles(relativePath, files, routes, layouts, loading, errors, notFound);
|
|
86
|
+
await this.processSpecialFiles(relativePath, files, routes, layouts, loading, errors, notFound, apiRoutes);
|
|
74
87
|
for (const dir of dirs) {
|
|
75
88
|
const subPath = relativePath ? path.join(relativePath, dir) : dir;
|
|
76
|
-
await this.scanDirectory(subPath, routes, layouts, loading, errors, notFound);
|
|
89
|
+
await this.scanDirectory(subPath, routes, layouts, loading, errors, notFound, apiRoutes);
|
|
77
90
|
}
|
|
78
91
|
}
|
|
79
|
-
async processSpecialFiles(relativePath, files, routes, layouts, loading, errors, notFound) {
|
|
92
|
+
async processSpecialFiles(relativePath, files, routes, layouts, loading, errors, notFound, apiRoutes) {
|
|
80
93
|
const routePath = this.pathToRoute(relativePath);
|
|
81
94
|
const pageFile = this.findFile(files, SPECIAL_FILES.PAGE);
|
|
82
95
|
if (pageFile) {
|
|
@@ -114,6 +127,11 @@ var AppRouteGenerator = class {
|
|
|
114
127
|
path: routePath,
|
|
115
128
|
filePath: path.join(relativePath, notFoundFile)
|
|
116
129
|
});
|
|
130
|
+
const routeFile = this.findFile(files, SPECIAL_FILES.ROUTE);
|
|
131
|
+
if (routeFile) {
|
|
132
|
+
const apiRoute = await this.processApiRouteFile(relativePath, routeFile);
|
|
133
|
+
apiRoutes.push(apiRoute);
|
|
134
|
+
}
|
|
117
135
|
}
|
|
118
136
|
findFile(files, baseName) {
|
|
119
137
|
for (const ext of this.extensions) {
|
|
@@ -196,6 +214,16 @@ var AppRouteGenerator = class {
|
|
|
196
214
|
return a.path.localeCompare(b.path);
|
|
197
215
|
});
|
|
198
216
|
}
|
|
217
|
+
sortApiRoutes(routes) {
|
|
218
|
+
return routes.sort((a, b) => {
|
|
219
|
+
if (!a.isDynamic && b.isDynamic) return -1;
|
|
220
|
+
if (a.isDynamic && !b.isDynamic) return 1;
|
|
221
|
+
const aDepth = a.path.split("/").length;
|
|
222
|
+
const bDepth = b.path.split("/").length;
|
|
223
|
+
if (aDepth !== bDepth) return aDepth - bDepth;
|
|
224
|
+
return a.path.localeCompare(b.path);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
199
227
|
sortLayouts(layouts) {
|
|
200
228
|
return layouts.sort((a, b) => {
|
|
201
229
|
if (a.path === "/" && b.path !== "/") return -1;
|
|
@@ -203,6 +231,32 @@ var AppRouteGenerator = class {
|
|
|
203
231
|
return a.path.split("/").length - b.path.split("/").length;
|
|
204
232
|
});
|
|
205
233
|
}
|
|
234
|
+
async detectHttpMethods(filePath) {
|
|
235
|
+
const fullPath = path.join(this.appDir, filePath);
|
|
236
|
+
const content = await promises.readFile(fullPath, "utf-8");
|
|
237
|
+
const methods = [];
|
|
238
|
+
for (const method of HTTP_METHODS) {
|
|
239
|
+
const functionExportRegex = /* @__PURE__ */ new RegExp(`export\\s+(?:async\\s+)?function\\s+${method}\\s*\\(`);
|
|
240
|
+
const constExportRegex = /* @__PURE__ */ new RegExp(`export\\s+(?:async\\s+)?(?:const|let|var)\\s+${method}\\s*=`);
|
|
241
|
+
if (functionExportRegex.test(content) || constExportRegex.test(content)) methods.push(method);
|
|
242
|
+
}
|
|
243
|
+
return methods;
|
|
244
|
+
}
|
|
245
|
+
async processApiRouteFile(relativePath, fileName) {
|
|
246
|
+
const filePath = path.join(relativePath, fileName);
|
|
247
|
+
const routePath = this.pathToRoute(relativePath);
|
|
248
|
+
const segments = this.parseRouteSegments(relativePath);
|
|
249
|
+
const params = this.extractParams(segments);
|
|
250
|
+
const methods = await this.detectHttpMethods(filePath);
|
|
251
|
+
return {
|
|
252
|
+
path: routePath,
|
|
253
|
+
filePath,
|
|
254
|
+
segments,
|
|
255
|
+
params,
|
|
256
|
+
isDynamic: params.length > 0,
|
|
257
|
+
methods
|
|
258
|
+
};
|
|
259
|
+
}
|
|
206
260
|
};
|
|
207
261
|
async function generateAppRouteManifest(appDir, options = {}) {
|
|
208
262
|
return new AppRouteGenerator({
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { i as writeManifest, n as generateAppRouteManifest, r as loadManifest, t as AppRouteGenerator } from "./app-routes-
|
|
1
|
+
import { i as writeManifest, n as generateAppRouteManifest, r as loadManifest, t as AppRouteGenerator } from "./app-routes-DnV_PBQL.js";
|
|
2
2
|
|
|
3
3
|
export { AppRouteGenerator, generateAppRouteManifest, loadManifest, writeManifest };
|
|
@@ -32,12 +32,21 @@ interface NotFoundEntry {
|
|
|
32
32
|
path: string;
|
|
33
33
|
filePath: string;
|
|
34
34
|
}
|
|
35
|
+
interface ApiRouteEntry {
|
|
36
|
+
path: string;
|
|
37
|
+
filePath: string;
|
|
38
|
+
segments: RouteSegment[];
|
|
39
|
+
params: string[];
|
|
40
|
+
isDynamic: boolean;
|
|
41
|
+
methods: string[];
|
|
42
|
+
}
|
|
35
43
|
interface AppRouteManifest {
|
|
36
44
|
routes: AppRouteEntry[];
|
|
37
45
|
layouts: LayoutEntry[];
|
|
38
46
|
loading: LoadingEntry[];
|
|
39
47
|
errors: ErrorEntry[];
|
|
40
48
|
notFound: NotFoundEntry[];
|
|
49
|
+
apiRoutes: ApiRouteEntry[];
|
|
41
50
|
generated: string;
|
|
42
51
|
}
|
|
43
52
|
interface RouteMetadata {
|
|
@@ -107,4 +116,4 @@ type ErrorComponent = (props: ErrorProps) => ReactNode;
|
|
|
107
116
|
type LoadingComponent = (props?: LoadingProps) => ReactNode;
|
|
108
117
|
type NotFoundComponent = (props?: NotFoundProps) => ReactNode;
|
|
109
118
|
//#endregion
|
|
110
|
-
export {
|
|
119
|
+
export { RouteSegment as C, RouteMetadata as S, ServerPropsResult as T, NotFoundComponent as _, AppRouterConfig as a, PageComponent as b, ErrorProps as c, LayoutComponent as d, LayoutEntry as f, LoadingProps as g, LoadingEntry as h, AppRouteMatch as i, GenerateMetadata as l, LoadingComponent as m, AppRouteEntry as n, ErrorComponent as o, LayoutProps as p, AppRouteManifest as r, ErrorEntry as s, ApiRouteEntry as t, GenerateStaticParams as u, NotFoundEntry as v, RouteSegmentType as w, PageProps as x, NotFoundProps as y };
|
package/dist/client.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { C as
|
|
1
|
+
import { C as RouteSegment, c as ErrorProps, f as LayoutEntry, g as LoadingProps, h as LoadingEntry, i as AppRouteMatch, l as GenerateMetadata, n as AppRouteEntry, p as LayoutProps, r as AppRouteManifest, s as ErrorEntry, u as GenerateStaticParams, v as NotFoundEntry, w as RouteSegmentType, x as PageProps, y as NotFoundProps } from "./app-types-BwXHrEWG.js";
|
|
2
2
|
import { _ as extractServerProps, a as LoadingSpinner, b as hasServerSideDataFetching, c as createErrorBoundary, d as MetadataResult, f as ServerPropsResult, g as extractMetadata, h as clearPropsCacheForComponent, i as HttpRuntimeClient, l as createHttpRuntimeClient, m as clearPropsCache, n as DefaultLoading, o as NotFound, p as StaticParamsResult, r as ErrorBoundary, s as RuntimeClient, t as DefaultError, u as createLoadingBoundary, v as extractServerPropsWithCache, y as extractStaticParams } from "./runtime-client-DXTHjUDN.js";
|
|
3
3
|
export { type AppRouteEntry, type AppRouteManifest, type AppRouteMatch, DefaultError, DefaultLoading, ErrorBoundary, type ErrorEntry, type ErrorProps, type GenerateMetadata, type GenerateStaticParams, HttpRuntimeClient, type LayoutEntry, type LayoutProps, type LoadingEntry, type LoadingProps, LoadingSpinner, type MetadataResult, NotFound, type NotFoundEntry, type NotFoundProps, type PageProps, type RouteSegment, type RouteSegmentType, type RuntimeClient, type ServerPropsResult, type StaticParamsResult, clearPropsCache, clearPropsCacheForComponent, createErrorBoundary, createHttpRuntimeClient, createLoadingBoundary, extractMetadata, extractServerProps, extractServerPropsWithCache, extractStaticParams, hasServerSideDataFetching };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as
|
|
1
|
+
import { C as RouteSegment, S as RouteMetadata, T as ServerPropsResult, _ as NotFoundComponent, a as AppRouterConfig, b as PageComponent, c as ErrorProps, d as LayoutComponent, f as LayoutEntry, g as LoadingProps, h as LoadingEntry, i as AppRouteMatch, l as GenerateMetadata, m as LoadingComponent, n as AppRouteEntry, o as ErrorComponent, p as LayoutProps, r as AppRouteManifest, s as ErrorEntry, t as ApiRouteEntry, u as GenerateStaticParams, v as NotFoundEntry, w as RouteSegmentType, x as PageProps, y as NotFoundProps } from "./app-types-BwXHrEWG.js";
|
|
2
2
|
import { a as useOptimisticAction, i as useActionStateWithFallback, n as ActionState, o as useValidatedAction, r as useActionState, t as ActionFunction } from "./useActionState-5zFMIoLK.js";
|
|
3
3
|
import { a as createServerReference, i as createFormAction, n as ServerActionResponse, o as enhanceFormWithAction, r as bindServerActions, t as ServerActionOptions } from "./actions-Ctw8vS6Y.js";
|
|
4
4
|
import { n as defineRariOptions, r as rari, t as defineRariConfig } from "./index-BlvegZl_.js";
|
|
5
|
-
export { ActionFunction, ActionState, AppRouteEntry, AppRouteManifest, AppRouteMatch, AppRouterConfig, ErrorComponent, ErrorEntry, ErrorProps, GenerateMetadata, GenerateStaticParams, LayoutComponent, LayoutEntry, LayoutProps, LoadingComponent, LoadingEntry, LoadingProps, NotFoundComponent, NotFoundEntry, NotFoundProps, PageComponent, PageProps, RouteMetadata, RouteSegment, RouteSegmentType, ServerActionOptions, ServerActionResponse, ServerPropsResult, bindServerActions, createFormAction, createServerReference, defineRariConfig, defineRariOptions, enhanceFormWithAction, rari, useActionState, useActionStateWithFallback, useOptimisticAction, useValidatedAction };
|
|
5
|
+
export { ActionFunction, ActionState, ApiRouteEntry, AppRouteEntry, AppRouteManifest, AppRouteMatch, AppRouterConfig, ErrorComponent, ErrorEntry, ErrorProps, GenerateMetadata, GenerateStaticParams, LayoutComponent, LayoutEntry, LayoutProps, LoadingComponent, LoadingEntry, LoadingProps, NotFoundComponent, NotFoundEntry, NotFoundProps, PageComponent, PageProps, RouteMetadata, RouteSegment, RouteSegmentType, ServerActionOptions, ServerActionResponse, ServerPropsResult, bindServerActions, createFormAction, createServerReference, defineRariConfig, defineRariOptions, enhanceFormWithAction, rari, useActionState, useActionStateWithFallback, useOptimisticAction, useValidatedAction };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as useValidatedAction, n as useActionStateWithFallback, r as useOptimisticAction, t as useActionState } from "./useActionState-AoBy7bZm.js";
|
|
2
2
|
import { i as enhanceFormWithAction, n as createFormAction, r as createServerReference, t as bindServerActions } from "./actions-5ADnvImc.js";
|
|
3
|
-
import { n as defineRariOptions, r as rari, t as defineRariConfig } from "./vite-
|
|
3
|
+
import { n as defineRariOptions, r as rari, t as defineRariConfig } from "./vite-C3iz9pDD.js";
|
|
4
4
|
import "./server-build-LuVSR-Im.js";
|
|
5
5
|
|
|
6
6
|
export { bindServerActions, createFormAction, createServerReference, defineRariConfig, defineRariOptions, enhanceFormWithAction, rari, useActionState, useActionStateWithFallback, useOptimisticAction, useValidatedAction };
|
package/dist/server.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as
|
|
1
|
+
import { C as RouteSegment, c as ErrorProps, f as LayoutEntry, g as LoadingProps, h as LoadingEntry, i as AppRouteMatch, l as GenerateMetadata, n as AppRouteEntry, p as LayoutProps, r as AppRouteManifest, s as ErrorEntry, u as GenerateStaticParams, v as NotFoundEntry, w as RouteSegmentType, x as PageProps, y as NotFoundProps } from "./app-types-BwXHrEWG.js";
|
|
2
2
|
import { _ as extractServerProps, b as hasServerSideDataFetching, d as MetadataResult, f as ServerPropsResult, g as extractMetadata, h as clearPropsCacheForComponent, i as HttpRuntimeClient, l as createHttpRuntimeClient, m as clearPropsCache, p as StaticParamsResult, s as RuntimeClient, v as extractServerPropsWithCache, y as extractStaticParams } from "./runtime-client-DXTHjUDN.js";
|
|
3
3
|
import { n as defineRariOptions, r as rari, t as defineRariConfig } from "./index-BlvegZl_.js";
|
|
4
4
|
import { Plugin } from "rolldown-vite";
|
|
@@ -25,7 +25,10 @@ declare class AppRouteGenerator {
|
|
|
25
25
|
findLayoutChain(routePath: string, manifest: AppRouteManifest): Promise<LayoutEntry[]>;
|
|
26
26
|
private shouldScanDirectory;
|
|
27
27
|
private sortRoutes;
|
|
28
|
+
private sortApiRoutes;
|
|
28
29
|
private sortLayouts;
|
|
30
|
+
private detectHttpMethods;
|
|
31
|
+
private processApiRouteFile;
|
|
29
32
|
}
|
|
30
33
|
declare function generateAppRouteManifest(appDir: string, options?: Partial<AppRouteGeneratorOptions>): Promise<AppRouteManifest>;
|
|
31
34
|
declare function writeManifest(manifest: AppRouteManifest, outputPath: string): Promise<void>;
|
|
@@ -39,7 +42,31 @@ interface RariRouterPluginOptions {
|
|
|
39
42
|
}
|
|
40
43
|
declare function rariRouter(options?: RariRouterPluginOptions): Plugin;
|
|
41
44
|
//#endregion
|
|
45
|
+
//#region src/api-routes.d.ts
|
|
46
|
+
interface RouteContext<TParams extends Record<string, string> = Record<string, string>> {
|
|
47
|
+
params: TParams;
|
|
48
|
+
}
|
|
49
|
+
type RouteHandler<TParams extends Record<string, string> = Record<string, string>> = (request: Request, context?: RouteContext<TParams>) => Response | Promise<Response> | any | Promise<any>;
|
|
50
|
+
interface ApiRouteHandlers<TParams extends Record<string, string> = Record<string, string>> {
|
|
51
|
+
GET?: RouteHandler<TParams>;
|
|
52
|
+
POST?: RouteHandler<TParams>;
|
|
53
|
+
PUT?: RouteHandler<TParams>;
|
|
54
|
+
DELETE?: RouteHandler<TParams>;
|
|
55
|
+
PATCH?: RouteHandler<TParams>;
|
|
56
|
+
HEAD?: RouteHandler<TParams>;
|
|
57
|
+
OPTIONS?: RouteHandler<TParams>;
|
|
58
|
+
}
|
|
59
|
+
declare class RariResponse extends Response {
|
|
60
|
+
static json(data: any, init?: ResponseInit): Response;
|
|
61
|
+
static redirect(url: string, status?: number): Response;
|
|
62
|
+
static noContent(init?: ResponseInit): Response;
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
42
65
|
//#region src/async-context.d.ts
|
|
43
66
|
declare function headers(): Promise<Headers>;
|
|
44
67
|
//#endregion
|
|
45
|
-
|
|
68
|
+
//#region src/server.d.ts
|
|
69
|
+
type Request$1 = globalThis.Request;
|
|
70
|
+
type Response$1 = globalThis.Response;
|
|
71
|
+
//#endregion
|
|
72
|
+
export { type ApiRouteHandlers, type AppRouteEntry, AppRouteGenerator, type AppRouteManifest, type AppRouteMatch, type ErrorEntry, type ErrorProps, type GenerateMetadata, type GenerateStaticParams, HttpRuntimeClient, type LayoutEntry, type LayoutProps, type LoadingEntry, type LoadingProps, type MetadataResult, type NotFoundEntry, type NotFoundProps, type PageProps, RariResponse, Request$1 as Request, Response$1 as Response, type RouteContext, type RouteHandler, type RouteSegment, type RouteSegmentType, type RuntimeClient, type ServerPropsResult, type StaticParamsResult, clearPropsCache, clearPropsCacheForComponent, createHttpRuntimeClient, defineRariConfig, defineRariOptions, extractMetadata, extractServerProps, extractServerPropsWithCache, extractStaticParams, generateAppRouteManifest, hasServerSideDataFetching, headers, loadManifest, rari, rariRouter, writeManifest };
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { n as defineRariOptions, r as rari, t as defineRariConfig } from "./vite-
|
|
1
|
+
import { n as defineRariOptions, r as rari, t as defineRariConfig } from "./vite-C3iz9pDD.js";
|
|
2
2
|
import "./server-build-LuVSR-Im.js";
|
|
3
|
-
import { i as writeManifest, n as generateAppRouteManifest, r as loadManifest, t as AppRouteGenerator } from "./app-routes-
|
|
3
|
+
import { i as writeManifest, n as generateAppRouteManifest, r as loadManifest, t as AppRouteGenerator } from "./app-routes-DnV_PBQL.js";
|
|
4
4
|
import { c as createHttpRuntimeClient, d as clearPropsCacheForComponent, f as extractMetadata, g as hasServerSideDataFetching, h as extractStaticParams, i as HttpRuntimeClient, m as extractServerPropsWithCache, p as extractServerProps, u as clearPropsCache } from "./runtime-client-0_G-RIhY.js";
|
|
5
5
|
import { promises } from "node:fs";
|
|
6
6
|
import path, { join, relative, resolve, sep } from "node:path";
|
|
@@ -1577,6 +1577,7 @@ function getAppRouterFileType(filePath) {
|
|
|
1577
1577
|
case "loading": return "loading";
|
|
1578
1578
|
case "error": return "error";
|
|
1579
1579
|
case "not-found": return "not-found";
|
|
1580
|
+
case "route": return "route";
|
|
1580
1581
|
default: return null;
|
|
1581
1582
|
}
|
|
1582
1583
|
}
|
|
@@ -1622,6 +1623,41 @@ function extractMetadata$1(fileContent) {
|
|
|
1622
1623
|
return null;
|
|
1623
1624
|
}
|
|
1624
1625
|
}
|
|
1626
|
+
function detectHttpMethods(fileContent) {
|
|
1627
|
+
const methods = [];
|
|
1628
|
+
for (const method of [
|
|
1629
|
+
"GET",
|
|
1630
|
+
"POST",
|
|
1631
|
+
"PUT",
|
|
1632
|
+
"DELETE",
|
|
1633
|
+
"PATCH",
|
|
1634
|
+
"HEAD",
|
|
1635
|
+
"OPTIONS"
|
|
1636
|
+
]) {
|
|
1637
|
+
const functionExportRegex = /* @__PURE__ */ new RegExp(`export\\s+(?:async\\s+)?function\\s+${method}\\s*\\(`);
|
|
1638
|
+
const constExportRegex = /* @__PURE__ */ new RegExp(`export\\s+(?:async\\s+)?(?:const|let|var)\\s+${method}\\s*=`);
|
|
1639
|
+
if (functionExportRegex.test(fileContent) || constExportRegex.test(fileContent)) methods.push(method);
|
|
1640
|
+
}
|
|
1641
|
+
return methods;
|
|
1642
|
+
}
|
|
1643
|
+
async function notifyApiRouteInvalidation(filePath) {
|
|
1644
|
+
try {
|
|
1645
|
+
const response = await fetch("http://localhost:3000/api/rsc/hmr-invalidate-api-route", {
|
|
1646
|
+
method: "POST",
|
|
1647
|
+
headers: { "Content-Type": "application/json" },
|
|
1648
|
+
body: JSON.stringify({ filePath })
|
|
1649
|
+
});
|
|
1650
|
+
if (!response.ok) {
|
|
1651
|
+
console.error(`Failed to invalidate API route cache: ${response.statusText}`);
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
const result = await response.json();
|
|
1655
|
+
if (result.success) console.warn(`[HMR] API route handler cache invalidated: ${filePath}`);
|
|
1656
|
+
else console.error(`[HMR] Failed to invalidate API route cache: ${result.error || "Unknown error"}`);
|
|
1657
|
+
} catch (error) {
|
|
1658
|
+
console.error("Failed to notify API route invalidation:", error);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1625
1661
|
function rariRouter(options = {}) {
|
|
1626
1662
|
const opts = {
|
|
1627
1663
|
...DEFAULT_OPTIONS,
|
|
@@ -1668,7 +1704,7 @@ function rariRouter(options = {}) {
|
|
|
1668
1704
|
console.warn("[Manifest] Route structure unchanged, using cached manifest");
|
|
1669
1705
|
return cachedManifestContent;
|
|
1670
1706
|
}
|
|
1671
|
-
const { generateAppRouteManifest: generateAppRouteManifest$1 } = await import("./app-routes-
|
|
1707
|
+
const { generateAppRouteManifest: generateAppRouteManifest$1 } = await import("./app-routes-cr6yLAYA.js");
|
|
1672
1708
|
const manifest = await generateAppRouteManifest$1(appDir, { extensions: opts.extensions });
|
|
1673
1709
|
const manifestContent = JSON.stringify(manifest, null, 2);
|
|
1674
1710
|
const outDir = path.resolve(root, opts.outDir);
|
|
@@ -1742,6 +1778,7 @@ function rariRouter(options = {}) {
|
|
|
1742
1778
|
const affectedRoutes = getAffectedRoutes(routePath, fileType, allRoutes);
|
|
1743
1779
|
let metadata;
|
|
1744
1780
|
let metadataChanged = false;
|
|
1781
|
+
let methods;
|
|
1745
1782
|
if (fileType === "page" || fileType === "layout") try {
|
|
1746
1783
|
const extractedMetadata = extractMetadata$1(await promises.readFile(file, "utf-8"));
|
|
1747
1784
|
if (extractedMetadata) {
|
|
@@ -1752,6 +1789,13 @@ function rariRouter(options = {}) {
|
|
|
1752
1789
|
} catch (error) {
|
|
1753
1790
|
console.error("Failed to extract metadata:", error);
|
|
1754
1791
|
}
|
|
1792
|
+
if (fileType === "route") try {
|
|
1793
|
+
methods = detectHttpMethods(await promises.readFile(file, "utf-8"));
|
|
1794
|
+
console.warn(`[HMR] API route methods detected: ${methods.join(", ")}`);
|
|
1795
|
+
await notifyApiRouteInvalidation(path.relative(appDir, file));
|
|
1796
|
+
} catch (error) {
|
|
1797
|
+
console.error("Failed to detect HTTP methods:", error);
|
|
1798
|
+
}
|
|
1755
1799
|
const hmrData = {
|
|
1756
1800
|
fileType,
|
|
1757
1801
|
filePath: path.relative(server$1.config.root, file),
|
|
@@ -1760,7 +1804,8 @@ function rariRouter(options = {}) {
|
|
|
1760
1804
|
manifestUpdated,
|
|
1761
1805
|
timestamp: Date.now(),
|
|
1762
1806
|
metadata,
|
|
1763
|
-
metadataChanged
|
|
1807
|
+
metadataChanged,
|
|
1808
|
+
methods
|
|
1764
1809
|
};
|
|
1765
1810
|
server$1.ws.send({
|
|
1766
1811
|
type: "custom",
|
|
@@ -1768,7 +1813,8 @@ function rariRouter(options = {}) {
|
|
|
1768
1813
|
data: hmrData
|
|
1769
1814
|
});
|
|
1770
1815
|
const metadataInfo = metadataChanged ? " [metadata updated]" : "";
|
|
1771
|
-
|
|
1816
|
+
const methodsInfo = methods ? ` [methods: ${methods.join(", ")}]` : "";
|
|
1817
|
+
console.warn(`[HMR] App router ${fileType} changed: ${hmrData.filePath} (affects ${affectedRoutes.length} route${affectedRoutes.length === 1 ? "" : "s"})${metadataInfo}${methodsInfo}`);
|
|
1772
1818
|
}, DEBOUNCE_DELAY);
|
|
1773
1819
|
pendingHMRUpdates.set(file, timer);
|
|
1774
1820
|
return [];
|
|
@@ -1794,6 +1840,31 @@ function rariRouter(options = {}) {
|
|
|
1794
1840
|
};
|
|
1795
1841
|
}
|
|
1796
1842
|
|
|
1843
|
+
//#endregion
|
|
1844
|
+
//#region src/api-routes.ts
|
|
1845
|
+
var RariResponse = class extends Response {
|
|
1846
|
+
static json(data, init) {
|
|
1847
|
+
const headers$1 = new Headers(init?.headers);
|
|
1848
|
+
if (!headers$1.has("content-type")) headers$1.set("content-type", "application/json");
|
|
1849
|
+
return new Response(JSON.stringify(data), {
|
|
1850
|
+
...init,
|
|
1851
|
+
headers: headers$1
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
static redirect(url, status = 307) {
|
|
1855
|
+
return new Response(null, {
|
|
1856
|
+
status,
|
|
1857
|
+
headers: { location: url }
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
static noContent(init) {
|
|
1861
|
+
return new Response(null, {
|
|
1862
|
+
...init,
|
|
1863
|
+
status: 204
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
};
|
|
1867
|
+
|
|
1797
1868
|
//#endregion
|
|
1798
1869
|
//#region src/async-context.ts
|
|
1799
1870
|
let currentContext = null;
|
|
@@ -1810,4 +1881,4 @@ async function headers() {
|
|
|
1810
1881
|
}
|
|
1811
1882
|
|
|
1812
1883
|
//#endregion
|
|
1813
|
-
export { AppRouteGenerator, HttpRuntimeClient, clearPropsCache, clearPropsCacheForComponent, createHttpRuntimeClient, defineRariConfig, defineRariOptions, extractMetadata, extractServerProps, extractServerPropsWithCache, extractStaticParams, generateAppRouteManifest, hasServerSideDataFetching, headers, loadManifest, rari, rariRouter, writeManifest };
|
|
1884
|
+
export { AppRouteGenerator, HttpRuntimeClient, RariResponse, clearPropsCache, clearPropsCacheForComponent, createHttpRuntimeClient, defineRariConfig, defineRariOptions, extractMetadata, extractServerProps, extractServerPropsWithCache, extractStaticParams, generateAppRouteManifest, hasServerSideDataFetching, headers, loadManifest, rari, rariRouter, writeManifest };
|
|
@@ -1065,8 +1065,13 @@ export async function renderApp() {
|
|
|
1065
1065
|
);
|
|
1066
1066
|
` : "const wrappedContent = contentToRender;"}
|
|
1067
1067
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1068
|
+
if (hasSSRContent) {
|
|
1069
|
+
hydrateRoot(rootElement, wrappedContent);
|
|
1070
|
+
isInitialHydration = false;
|
|
1071
|
+
} else {
|
|
1072
|
+
const root = createRoot(rootElement);
|
|
1073
|
+
root.render(wrappedContent);
|
|
1074
|
+
}
|
|
1070
1075
|
} catch (error) {
|
|
1071
1076
|
console.error('[Rari] Error rendering app:', error);
|
|
1072
1077
|
rootElement.innerHTML = \`
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rari",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"description": "Runtime Accelerated Rendering Infrastructure (Rari)",
|
|
6
6
|
"author": "Ryan Skinner",
|
|
7
7
|
"license": "MIT",
|
|
@@ -89,20 +89,20 @@
|
|
|
89
89
|
"picocolors": "^1.1.1"
|
|
90
90
|
},
|
|
91
91
|
"optionalDependencies": {
|
|
92
|
-
"rari-darwin-arm64": "0.
|
|
93
|
-
"rari-darwin-x64": "0.
|
|
94
|
-
"rari-linux-arm64": "0.
|
|
95
|
-
"rari-linux-x64": "0.
|
|
96
|
-
"rari-win32-x64": "0.
|
|
92
|
+
"rari-darwin-arm64": "0.4.0",
|
|
93
|
+
"rari-darwin-x64": "0.4.0",
|
|
94
|
+
"rari-linux-arm64": "0.4.0",
|
|
95
|
+
"rari-linux-x64": "0.4.0",
|
|
96
|
+
"rari-win32-x64": "0.4.0"
|
|
97
97
|
},
|
|
98
98
|
"devDependencies": {
|
|
99
|
-
"@types/node": "^24.9.
|
|
99
|
+
"@types/node": "^24.9.2",
|
|
100
100
|
"@types/react": "^19.2.2",
|
|
101
101
|
"@typescript/native-preview": "7.0.0-dev.20250923.1",
|
|
102
102
|
"chokidar": "^4.0.3",
|
|
103
103
|
"eslint": "^9.38.0",
|
|
104
|
-
"oxlint": "^1.
|
|
105
|
-
"rolldown-vite": "^7.1.
|
|
106
|
-
"tsdown": "^0.15.
|
|
104
|
+
"oxlint": "^1.25.0",
|
|
105
|
+
"rolldown-vite": "^7.1.20",
|
|
106
|
+
"tsdown": "^0.15.12"
|
|
107
107
|
}
|
|
108
108
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface RouteContext<TParams extends Record<string, string> = Record<string, string>> {
|
|
2
|
+
params: TParams
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export type RouteHandler<TParams extends Record<string, string> = Record<string, string>> = (
|
|
6
|
+
request: Request,
|
|
7
|
+
context?: RouteContext<TParams>,
|
|
8
|
+
) => Response | Promise<Response> | any | Promise<any>
|
|
9
|
+
|
|
10
|
+
export interface ApiRouteHandlers<TParams extends Record<string, string> = Record<string, string>> {
|
|
11
|
+
GET?: RouteHandler<TParams>
|
|
12
|
+
POST?: RouteHandler<TParams>
|
|
13
|
+
PUT?: RouteHandler<TParams>
|
|
14
|
+
DELETE?: RouteHandler<TParams>
|
|
15
|
+
PATCH?: RouteHandler<TParams>
|
|
16
|
+
HEAD?: RouteHandler<TParams>
|
|
17
|
+
OPTIONS?: RouteHandler<TParams>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class RariResponse extends Response {
|
|
21
|
+
static json(data: any, init?: ResponseInit): Response {
|
|
22
|
+
const headers = new Headers(init?.headers)
|
|
23
|
+
|
|
24
|
+
if (!headers.has('content-type')) {
|
|
25
|
+
headers.set('content-type', 'application/json')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return new Response(JSON.stringify(data), {
|
|
29
|
+
...init,
|
|
30
|
+
headers,
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static redirect(url: string, status: number = 307): Response {
|
|
35
|
+
return new Response(null, {
|
|
36
|
+
status,
|
|
37
|
+
headers: {
|
|
38
|
+
location: url,
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static noContent(init?: ResponseInit): Response {
|
|
44
|
+
return new Response(null, {
|
|
45
|
+
...init,
|
|
46
|
+
status: 204,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/router/app-routes.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
ApiRouteEntry,
|
|
2
3
|
AppRouteEntry,
|
|
3
4
|
AppRouteManifest,
|
|
4
5
|
ErrorEntry,
|
|
@@ -25,6 +26,7 @@ const SPECIAL_FILES = {
|
|
|
25
26
|
NOT_FOUND: 'not-found',
|
|
26
27
|
TEMPLATE: 'template',
|
|
27
28
|
DEFAULT: 'default',
|
|
29
|
+
ROUTE: 'route',
|
|
28
30
|
} as const
|
|
29
31
|
|
|
30
32
|
const SEGMENT_PATTERNS = {
|
|
@@ -33,6 +35,8 @@ const SEGMENT_PATTERNS = {
|
|
|
33
35
|
OPTIONAL_CATCH_ALL: /^\[\[\.\.\.([^\]]+)\]\]$/,
|
|
34
36
|
} as const
|
|
35
37
|
|
|
38
|
+
const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'] as const
|
|
39
|
+
|
|
36
40
|
export class AppRouteGenerator {
|
|
37
41
|
private appDir: string
|
|
38
42
|
private extensions: string[]
|
|
@@ -54,14 +58,16 @@ export class AppRouteGenerator {
|
|
|
54
58
|
const loading: LoadingEntry[] = []
|
|
55
59
|
const errors: ErrorEntry[] = []
|
|
56
60
|
const notFound: NotFoundEntry[] = []
|
|
61
|
+
const apiRoutes: ApiRouteEntry[] = []
|
|
57
62
|
|
|
58
|
-
await this.scanDirectory('', routes, layouts, loading, errors, notFound)
|
|
63
|
+
await this.scanDirectory('', routes, layouts, loading, errors, notFound, apiRoutes)
|
|
59
64
|
|
|
60
65
|
if (this.verbose) {
|
|
61
66
|
console.warn(`[AppRouter] Found ${routes.length} routes`)
|
|
62
67
|
console.warn(`[AppRouter] Found ${layouts.length} layouts`)
|
|
63
68
|
console.warn(`[AppRouter] Found ${loading.length} loading components`)
|
|
64
69
|
console.warn(`[AppRouter] Found ${errors.length} error boundaries`)
|
|
70
|
+
console.warn(`[AppRouter] Found ${apiRoutes.length} API routes`)
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
return {
|
|
@@ -70,6 +76,7 @@ export class AppRouteGenerator {
|
|
|
70
76
|
loading,
|
|
71
77
|
errors,
|
|
72
78
|
notFound,
|
|
79
|
+
apiRoutes: this.sortApiRoutes(apiRoutes),
|
|
73
80
|
generated: new Date().toISOString(),
|
|
74
81
|
}
|
|
75
82
|
}
|
|
@@ -81,6 +88,7 @@ export class AppRouteGenerator {
|
|
|
81
88
|
loading: LoadingEntry[],
|
|
82
89
|
errors: ErrorEntry[],
|
|
83
90
|
notFound: NotFoundEntry[],
|
|
91
|
+
apiRoutes: ApiRouteEntry[],
|
|
84
92
|
): Promise<void> {
|
|
85
93
|
const fullPath = path.join(this.appDir, relativePath)
|
|
86
94
|
|
|
@@ -117,11 +125,12 @@ export class AppRouteGenerator {
|
|
|
117
125
|
loading,
|
|
118
126
|
errors,
|
|
119
127
|
notFound,
|
|
128
|
+
apiRoutes,
|
|
120
129
|
)
|
|
121
130
|
|
|
122
131
|
for (const dir of dirs) {
|
|
123
132
|
const subPath = relativePath ? path.join(relativePath, dir) : dir
|
|
124
|
-
await this.scanDirectory(subPath, routes, layouts, loading, errors, notFound)
|
|
133
|
+
await this.scanDirectory(subPath, routes, layouts, loading, errors, notFound, apiRoutes)
|
|
125
134
|
}
|
|
126
135
|
}
|
|
127
136
|
|
|
@@ -133,6 +142,7 @@ export class AppRouteGenerator {
|
|
|
133
142
|
loading: LoadingEntry[],
|
|
134
143
|
errors: ErrorEntry[],
|
|
135
144
|
notFound: NotFoundEntry[],
|
|
145
|
+
apiRoutes: ApiRouteEntry[],
|
|
136
146
|
): Promise<void> {
|
|
137
147
|
const routePath = this.pathToRoute(relativePath)
|
|
138
148
|
|
|
@@ -183,6 +193,12 @@ export class AppRouteGenerator {
|
|
|
183
193
|
filePath: path.join(relativePath, notFoundFile),
|
|
184
194
|
})
|
|
185
195
|
}
|
|
196
|
+
|
|
197
|
+
const routeFile = this.findFile(files, SPECIAL_FILES.ROUTE)
|
|
198
|
+
if (routeFile) {
|
|
199
|
+
const apiRoute = await this.processApiRouteFile(relativePath, routeFile)
|
|
200
|
+
apiRoutes.push(apiRoute)
|
|
201
|
+
}
|
|
186
202
|
}
|
|
187
203
|
|
|
188
204
|
private findFile(files: string[], baseName: string): string | undefined {
|
|
@@ -327,6 +343,22 @@ export class AppRouteGenerator {
|
|
|
327
343
|
})
|
|
328
344
|
}
|
|
329
345
|
|
|
346
|
+
private sortApiRoutes(routes: ApiRouteEntry[]): ApiRouteEntry[] {
|
|
347
|
+
return routes.sort((a, b) => {
|
|
348
|
+
if (!a.isDynamic && b.isDynamic)
|
|
349
|
+
return -1
|
|
350
|
+
if (a.isDynamic && !b.isDynamic)
|
|
351
|
+
return 1
|
|
352
|
+
|
|
353
|
+
const aDepth = a.path.split('/').length
|
|
354
|
+
const bDepth = b.path.split('/').length
|
|
355
|
+
if (aDepth !== bDepth)
|
|
356
|
+
return aDepth - bDepth
|
|
357
|
+
|
|
358
|
+
return a.path.localeCompare(b.path)
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
|
|
330
362
|
private sortLayouts(layouts: LayoutEntry[]): LayoutEntry[] {
|
|
331
363
|
return layouts.sort((a, b) => {
|
|
332
364
|
if (a.path === '/' && b.path !== '/')
|
|
@@ -339,6 +371,47 @@ export class AppRouteGenerator {
|
|
|
339
371
|
return aDepth - bDepth
|
|
340
372
|
})
|
|
341
373
|
}
|
|
374
|
+
|
|
375
|
+
private async detectHttpMethods(filePath: string): Promise<string[]> {
|
|
376
|
+
const fullPath = path.join(this.appDir, filePath)
|
|
377
|
+
const content = await fs.readFile(fullPath, 'utf-8')
|
|
378
|
+
const methods: string[] = []
|
|
379
|
+
|
|
380
|
+
for (const method of HTTP_METHODS) {
|
|
381
|
+
const functionExportRegex = new RegExp(
|
|
382
|
+
`export\\s+(?:async\\s+)?function\\s+${method}\\s*\\(`,
|
|
383
|
+
)
|
|
384
|
+
const constExportRegex = new RegExp(
|
|
385
|
+
`export\\s+(?:async\\s+)?(?:const|let|var)\\s+${method}\\s*=`,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
if (functionExportRegex.test(content) || constExportRegex.test(content)) {
|
|
389
|
+
methods.push(method)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return methods
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private async processApiRouteFile(
|
|
397
|
+
relativePath: string,
|
|
398
|
+
fileName: string,
|
|
399
|
+
): Promise<ApiRouteEntry> {
|
|
400
|
+
const filePath = path.join(relativePath, fileName)
|
|
401
|
+
const routePath = this.pathToRoute(relativePath)
|
|
402
|
+
const segments = this.parseRouteSegments(relativePath)
|
|
403
|
+
const params = this.extractParams(segments)
|
|
404
|
+
const methods = await this.detectHttpMethods(filePath)
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
path: routePath,
|
|
408
|
+
filePath,
|
|
409
|
+
segments,
|
|
410
|
+
params,
|
|
411
|
+
isDynamic: params.length > 0,
|
|
412
|
+
methods,
|
|
413
|
+
}
|
|
414
|
+
}
|
|
342
415
|
}
|
|
343
416
|
|
|
344
417
|
export async function generateAppRouteManifest(
|
package/src/router/app-types.ts
CHANGED
|
@@ -42,12 +42,22 @@ export interface NotFoundEntry {
|
|
|
42
42
|
filePath: string
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
export interface ApiRouteEntry {
|
|
46
|
+
path: string
|
|
47
|
+
filePath: string
|
|
48
|
+
segments: RouteSegment[]
|
|
49
|
+
params: string[]
|
|
50
|
+
isDynamic: boolean
|
|
51
|
+
methods: string[]
|
|
52
|
+
}
|
|
53
|
+
|
|
45
54
|
export interface AppRouteManifest {
|
|
46
55
|
routes: AppRouteEntry[]
|
|
47
56
|
layouts: LayoutEntry[]
|
|
48
57
|
loading: LoadingEntry[]
|
|
49
58
|
errors: ErrorEntry[]
|
|
50
59
|
notFound: NotFoundEntry[]
|
|
60
|
+
apiRoutes: ApiRouteEntry[]
|
|
51
61
|
generated: string
|
|
52
62
|
}
|
|
53
63
|
|
|
@@ -17,7 +17,7 @@ const DEFAULT_OPTIONS: Required<RariRouterPluginOptions> = {
|
|
|
17
17
|
outDir: 'dist',
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
type AppRouterFileType = 'page' | 'layout' | 'loading' | 'error' | 'not-found' | 'server-action'
|
|
20
|
+
type AppRouterFileType = 'page' | 'layout' | 'loading' | 'error' | 'not-found' | 'route' | 'server-action'
|
|
21
21
|
|
|
22
22
|
interface AppRouterHMRData {
|
|
23
23
|
fileType: AppRouterFileType
|
|
@@ -29,6 +29,7 @@ interface AppRouterHMRData {
|
|
|
29
29
|
metadata?: Record<string, any>
|
|
30
30
|
metadataChanged?: boolean
|
|
31
31
|
actionExports?: string[]
|
|
32
|
+
methods?: string[]
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
function getAppRouterFileType(filePath: string): AppRouterFileType | null {
|
|
@@ -46,6 +47,8 @@ function getAppRouterFileType(filePath: string): AppRouterFileType | null {
|
|
|
46
47
|
return 'error'
|
|
47
48
|
case 'not-found':
|
|
48
49
|
return 'not-found'
|
|
50
|
+
case 'route':
|
|
51
|
+
return 'route'
|
|
49
52
|
default:
|
|
50
53
|
return null
|
|
51
54
|
}
|
|
@@ -130,6 +133,56 @@ function extractMetadata(fileContent: string): Record<string, any> | null {
|
|
|
130
133
|
}
|
|
131
134
|
}
|
|
132
135
|
|
|
136
|
+
function detectHttpMethods(fileContent: string): string[] {
|
|
137
|
+
const methods: string[] = []
|
|
138
|
+
const httpMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']
|
|
139
|
+
|
|
140
|
+
for (const method of httpMethods) {
|
|
141
|
+
const functionExportRegex = new RegExp(
|
|
142
|
+
`export\\s+(?:async\\s+)?function\\s+${method}\\s*\\(`,
|
|
143
|
+
)
|
|
144
|
+
const constExportRegex = new RegExp(
|
|
145
|
+
`export\\s+(?:async\\s+)?(?:const|let|var)\\s+${method}\\s*=`,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if (functionExportRegex.test(fileContent) || constExportRegex.test(fileContent)) {
|
|
149
|
+
methods.push(method)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return methods
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function notifyApiRouteInvalidation(filePath: string): Promise<void> {
|
|
157
|
+
try {
|
|
158
|
+
const response = await fetch('http://localhost:3000/api/rsc/hmr-invalidate-api-route', {
|
|
159
|
+
method: 'POST',
|
|
160
|
+
headers: {
|
|
161
|
+
'Content-Type': 'application/json',
|
|
162
|
+
},
|
|
163
|
+
body: JSON.stringify({
|
|
164
|
+
filePath,
|
|
165
|
+
}),
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
if (!response.ok) {
|
|
169
|
+
console.error(`Failed to invalidate API route cache: ${response.statusText}`)
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const result = await response.json()
|
|
174
|
+
if (result.success) {
|
|
175
|
+
console.warn(`[HMR] API route handler cache invalidated: ${filePath}`)
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
console.error(`[HMR] Failed to invalidate API route cache: ${result.error || 'Unknown error'}`)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
console.error('Failed to notify API route invalidation:', error)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
133
186
|
export function rariRouter(options: RariRouterPluginOptions = {}): Plugin {
|
|
134
187
|
const opts = { ...DEFAULT_OPTIONS, ...options }
|
|
135
188
|
|
|
@@ -321,6 +374,7 @@ export function rariRouter(options: RariRouterPluginOptions = {}): Plugin {
|
|
|
321
374
|
|
|
322
375
|
let metadata: Record<string, any> | undefined
|
|
323
376
|
let metadataChanged = false
|
|
377
|
+
let methods: string[] | undefined
|
|
324
378
|
|
|
325
379
|
if (fileType === 'page' || fileType === 'layout') {
|
|
326
380
|
try {
|
|
@@ -338,6 +392,19 @@ export function rariRouter(options: RariRouterPluginOptions = {}): Plugin {
|
|
|
338
392
|
}
|
|
339
393
|
}
|
|
340
394
|
|
|
395
|
+
if (fileType === 'route') {
|
|
396
|
+
try {
|
|
397
|
+
const fileContent = await fs.readFile(file, 'utf-8')
|
|
398
|
+
methods = detectHttpMethods(fileContent)
|
|
399
|
+
console.warn(`[HMR] API route methods detected: ${methods.join(', ')}`)
|
|
400
|
+
|
|
401
|
+
await notifyApiRouteInvalidation(path.relative(appDir, file))
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
console.error('Failed to detect HTTP methods:', error)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
341
408
|
const hmrData: AppRouterHMRData = {
|
|
342
409
|
fileType,
|
|
343
410
|
filePath: path.relative(server.config.root, file),
|
|
@@ -347,6 +414,7 @@ export function rariRouter(options: RariRouterPluginOptions = {}): Plugin {
|
|
|
347
414
|
timestamp: Date.now(),
|
|
348
415
|
metadata,
|
|
349
416
|
metadataChanged,
|
|
417
|
+
methods,
|
|
350
418
|
}
|
|
351
419
|
|
|
352
420
|
server.ws.send({
|
|
@@ -356,8 +424,9 @@ export function rariRouter(options: RariRouterPluginOptions = {}): Plugin {
|
|
|
356
424
|
})
|
|
357
425
|
|
|
358
426
|
const metadataInfo = metadataChanged ? ' [metadata updated]' : ''
|
|
427
|
+
const methodsInfo = methods ? ` [methods: ${methods.join(', ')}]` : ''
|
|
359
428
|
console.warn(
|
|
360
|
-
`[HMR] App router ${fileType} changed: ${hmrData.filePath} (affects ${affectedRoutes.length} route${affectedRoutes.length === 1 ? '' : 's'})${metadataInfo}`,
|
|
429
|
+
`[HMR] App router ${fileType} changed: ${hmrData.filePath} (affects ${affectedRoutes.length} route${affectedRoutes.length === 1 ? '' : 's'})${metadataInfo}${methodsInfo}`,
|
|
361
430
|
)
|
|
362
431
|
}, DEBOUNCE_DELAY)
|
|
363
432
|
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
export interface RequestData {
|
|
2
|
+
method: string
|
|
3
|
+
url: string
|
|
4
|
+
headers: Record<string, string>
|
|
5
|
+
body?: string
|
|
6
|
+
params: Record<string, string>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ResponseData {
|
|
10
|
+
status: number
|
|
11
|
+
statusText?: string
|
|
12
|
+
headers: Record<string, string>
|
|
13
|
+
body: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createApiRequest(
|
|
17
|
+
requestData: RequestData,
|
|
18
|
+
bodyStream?: ReadableStream<Uint8Array>,
|
|
19
|
+
): Request {
|
|
20
|
+
const url = new URL(requestData.url, 'http://localhost')
|
|
21
|
+
|
|
22
|
+
if (requestData.params) {
|
|
23
|
+
for (const [key, value] of Object.entries(requestData.params)) {
|
|
24
|
+
url.searchParams.set(key, value)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const headers = new Headers(requestData.headers || {})
|
|
29
|
+
|
|
30
|
+
let body: BodyInit | null = null
|
|
31
|
+
if (bodyStream) {
|
|
32
|
+
body = bodyStream
|
|
33
|
+
}
|
|
34
|
+
else if (requestData.body && requestData.body.length > 0) {
|
|
35
|
+
const methodSupportsBody = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(
|
|
36
|
+
requestData.method.toUpperCase(),
|
|
37
|
+
)
|
|
38
|
+
if (methodSupportsBody) {
|
|
39
|
+
body = requestData.body
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return new Request(url.toString(), {
|
|
44
|
+
method: requestData.method,
|
|
45
|
+
headers,
|
|
46
|
+
body,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function serializeApiResponse(
|
|
51
|
+
response: Response | any,
|
|
52
|
+
): Promise<ResponseData> {
|
|
53
|
+
if (response instanceof Response) {
|
|
54
|
+
const body = await response.text()
|
|
55
|
+
const headers: Record<string, string> = {}
|
|
56
|
+
|
|
57
|
+
response.headers.forEach((value, key) => {
|
|
58
|
+
headers[key] = value
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
status: response.status,
|
|
63
|
+
statusText: response.statusText,
|
|
64
|
+
headers,
|
|
65
|
+
body,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
status: 200,
|
|
71
|
+
statusText: 'OK',
|
|
72
|
+
headers: {
|
|
73
|
+
'content-type': 'application/json',
|
|
74
|
+
},
|
|
75
|
+
body: JSON.stringify(response),
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function jsonResponse(data: any, init?: ResponseInit): Response {
|
|
80
|
+
return new Response(JSON.stringify(data), {
|
|
81
|
+
...init,
|
|
82
|
+
headers: {
|
|
83
|
+
'content-type': 'application/json',
|
|
84
|
+
...init?.headers,
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function redirectResponse(url: string, status: number = 307): Response {
|
|
90
|
+
return new Response(null, {
|
|
91
|
+
status,
|
|
92
|
+
headers: {
|
|
93
|
+
location: url,
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function textResponse(text: string, init?: ResponseInit): Response {
|
|
99
|
+
return new Response(text, {
|
|
100
|
+
...init,
|
|
101
|
+
headers: {
|
|
102
|
+
'content-type': 'text/plain',
|
|
103
|
+
...init?.headers,
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function htmlResponse(html: string, init?: ResponseInit): Response {
|
|
109
|
+
return new Response(html, {
|
|
110
|
+
...init,
|
|
111
|
+
headers: {
|
|
112
|
+
'content-type': 'text/html',
|
|
113
|
+
...init?.headers,
|
|
114
|
+
},
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function errorResponse(message: string, status: number = 500): Response {
|
|
119
|
+
return jsonResponse(
|
|
120
|
+
{
|
|
121
|
+
error: true,
|
|
122
|
+
message,
|
|
123
|
+
},
|
|
124
|
+
{ status },
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function parseJsonBody<T = any>(request: Request): Promise<T> {
|
|
129
|
+
const contentType = request.headers.get('content-type') || ''
|
|
130
|
+
|
|
131
|
+
if (!contentType.includes('application/json')) {
|
|
132
|
+
throw new Error('Request body is not JSON')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return await request.json()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function parseFormBody(request: Request): Promise<FormData> {
|
|
139
|
+
const contentType = request.headers.get('content-type') || ''
|
|
140
|
+
|
|
141
|
+
if (!contentType.includes('application/x-www-form-urlencoded')
|
|
142
|
+
&& !contentType.includes('multipart/form-data')) {
|
|
143
|
+
throw new Error('Request body is not form data')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return await request.formData()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function getParams<T extends Record<string, string> = Record<string, string>>(
|
|
150
|
+
context: { params: T },
|
|
151
|
+
): T {
|
|
152
|
+
return context.params
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function getSearchParams(request: Request): URLSearchParams {
|
|
156
|
+
const url = new URL(request.url)
|
|
157
|
+
return url.searchParams
|
|
158
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
ApiRouteHandlers,
|
|
3
|
+
RouteContext,
|
|
4
|
+
RouteHandler,
|
|
5
|
+
} from './api-routes'
|
|
6
|
+
|
|
7
|
+
export { RariResponse } from './api-routes'
|
|
8
|
+
|
|
9
|
+
export type Request = globalThis.Request
|
|
10
|
+
export type Response = globalThis.Response
|
|
11
|
+
|
|
1
12
|
export { headers } from './async-context'
|
|
2
13
|
|
|
3
14
|
export type {
|
package/src/vite/index.ts
CHANGED
|
@@ -1341,8 +1341,13 @@ export async function renderApp() {
|
|
|
1341
1341
|
`
|
|
1342
1342
|
: 'const wrappedContent = contentToRender;'}
|
|
1343
1343
|
|
|
1344
|
-
|
|
1345
|
-
|
|
1344
|
+
if (hasSSRContent) {
|
|
1345
|
+
hydrateRoot(rootElement, wrappedContent);
|
|
1346
|
+
isInitialHydration = false;
|
|
1347
|
+
} else {
|
|
1348
|
+
const root = createRoot(rootElement);
|
|
1349
|
+
root.render(wrappedContent);
|
|
1350
|
+
}
|
|
1346
1351
|
} catch (error) {
|
|
1347
1352
|
console.error('[Rari] Error rendering app:', error);
|
|
1348
1353
|
rootElement.innerHTML = \`
|