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.
@@ -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
- await this.scanDirectory("", routes, layouts, loading, errors, notFound);
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-DIZ265rW.js";
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 { RouteSegmentType as C, RouteSegment as S, NotFoundEntry as _, ErrorComponent as a, PageProps as b, GenerateMetadata as c, LayoutEntry as d, LayoutProps as f, NotFoundComponent as g, LoadingProps as h, AppRouterConfig as i, GenerateStaticParams as l, LoadingEntry as m, AppRouteManifest as n, ErrorEntry as o, LoadingComponent as p, AppRouteMatch as r, ErrorProps as s, AppRouteEntry as t, LayoutComponent as u, NotFoundProps as v, ServerPropsResult as w, RouteMetadata as x, PageComponent as y };
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 RouteSegmentType, S as RouteSegment, _ as NotFoundEntry, b as PageProps, c as GenerateMetadata, d as LayoutEntry, f as LayoutProps, h as LoadingProps, l as GenerateStaticParams, m as LoadingEntry, n as AppRouteManifest, o as ErrorEntry, r as AppRouteMatch, s as ErrorProps, t as AppRouteEntry, v as NotFoundProps } from "./app-types-DUzcpcTH.js";
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 RouteSegmentType, S as RouteSegment, _ as NotFoundEntry, a as ErrorComponent, b as PageProps, c as GenerateMetadata, d as LayoutEntry, f as LayoutProps, g as NotFoundComponent, h as LoadingProps, i as AppRouterConfig, l as GenerateStaticParams, m as LoadingEntry, n as AppRouteManifest, o as ErrorEntry, p as LoadingComponent, r as AppRouteMatch, s as ErrorProps, t as AppRouteEntry, u as LayoutComponent, v as NotFoundProps, w as ServerPropsResult, x as RouteMetadata, y as PageComponent } from "./app-types-DUzcpcTH.js";
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-BzuW6jU8.js";
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 RouteSegmentType, S as RouteSegment, _ as NotFoundEntry, b as PageProps, c as GenerateMetadata, d as LayoutEntry, f as LayoutProps, h as LoadingProps, l as GenerateStaticParams, m as LoadingEntry, n as AppRouteManifest, o as ErrorEntry, r as AppRouteMatch, s as ErrorProps, t as AppRouteEntry, v as NotFoundProps } from "./app-types-DUzcpcTH.js";
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
- export { 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, 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 };
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-BzuW6jU8.js";
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-DIZ265rW.js";
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-BvbStRcg.js");
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
- console.warn(`[HMR] App router ${fileType} changed: ${hmrData.filePath} (affects ${affectedRoutes.length} route${affectedRoutes.length === 1 ? "" : "s"})${metadataInfo}`);
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
- const root = createRoot(rootElement);
1069
- root.render(wrappedContent);
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.3.2",
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.3.2",
93
- "rari-darwin-x64": "0.3.2",
94
- "rari-linux-arm64": "0.3.2",
95
- "rari-linux-x64": "0.3.2",
96
- "rari-win32-x64": "0.3.2"
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.1",
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.23.0",
105
- "rolldown-vite": "^7.1.19",
106
- "tsdown": "^0.15.9"
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
+ }
@@ -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(
@@ -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
- const root = createRoot(rootElement);
1345
- root.render(wrappedContent);
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 = \`