akanjs 2.3.11-rc.1 → 2.3.11-rc.10

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.
Files changed (71) hide show
  1. package/client/capacitor.ts +13 -0
  2. package/client/csrTypes.ts +2 -0
  3. package/common/routeConvention.ts +8 -5
  4. package/document/documentQuery.ts +55 -9
  5. package/document/into.ts +8 -4
  6. package/package.json +5 -1
  7. package/server/proxy/localeWebProxy.ts +10 -1
  8. package/server/resolver/database.resolver.ts +4 -3
  9. package/server/resolver/signal.resolver.ts +18 -2
  10. package/server/routeTreeBuilder.ts +37 -4
  11. package/service/predefinedAdaptor/database.adaptor.ts +558 -170
  12. package/types/client/capacitor.d.ts +11 -0
  13. package/types/client/csrTypes.d.ts +2 -0
  14. package/types/common/routeConvention.d.ts +1 -1
  15. package/types/document/documentQuery.d.ts +38 -9
  16. package/types/document/into.d.ts +4 -4
  17. package/types/service/predefinedAdaptor/database.adaptor.d.ts +105 -17
  18. package/types/ui/Button.d.ts +6 -1
  19. package/types/ui/DatePicker.d.ts +12 -9
  20. package/types/ui/Dropdown.d.ts +6 -1
  21. package/types/ui/Empty.d.ts +7 -1
  22. package/types/ui/Input.d.ts +12 -8
  23. package/types/ui/Loading/index.d.ts +11 -6
  24. package/types/ui/Menu.d.ts +9 -4
  25. package/types/ui/Modal.d.ts +11 -1
  26. package/types/ui/Pagination.d.ts +6 -1
  27. package/types/ui/Popconfirm.d.ts +7 -1
  28. package/types/ui/Radio.d.ts +7 -4
  29. package/types/ui/Select.d.ts +6 -1
  30. package/types/ui/Table.d.ts +6 -1
  31. package/types/ui/ToggleSelect.d.ts +11 -7
  32. package/types/ui/UiOverride/Provider.d.ts +14 -0
  33. package/types/ui/UiOverride/context.d.ts +72 -0
  34. package/types/ui/UiOverride/createOverridable.d.ts +9 -0
  35. package/types/ui/UiOverride/index.d.ts +5 -0
  36. package/types/ui/UiOverride/override.d.ts +18 -0
  37. package/types/ui/UiOverride/useUiOverride.d.ts +7 -0
  38. package/types/ui/UiOverride.d.ts +1 -0
  39. package/types/ui/Unauthorized.d.ts +8 -3
  40. package/types/ui/index.d.ts +1 -0
  41. package/types/webkit/index.d.ts +1 -1
  42. package/types/webkit/usePushNotification.d.ts +28 -0
  43. package/ui/Button.tsx +15 -2
  44. package/ui/DatePicker.tsx +19 -9
  45. package/ui/Dropdown.tsx +9 -1
  46. package/ui/Empty.tsx +11 -1
  47. package/ui/Input.tsx +22 -14
  48. package/ui/Loading/index.tsx +15 -1
  49. package/ui/Menu.tsx +12 -3
  50. package/ui/Modal.tsx +13 -1
  51. package/ui/Pagination.tsx +9 -1
  52. package/ui/Popconfirm.tsx +9 -1
  53. package/ui/Radio.tsx +14 -3
  54. package/ui/Select.tsx +22 -2
  55. package/ui/System/CSR.tsx +1 -9
  56. package/ui/System/Client.tsx +1 -0
  57. package/ui/Table.tsx +8 -1
  58. package/ui/ToggleSelect.tsx +31 -5
  59. package/ui/UiOverride/Provider.tsx +22 -0
  60. package/ui/UiOverride/context.ts +85 -0
  61. package/ui/UiOverride/createOverridable.tsx +25 -0
  62. package/ui/UiOverride/index.ts +5 -0
  63. package/ui/UiOverride/override.ts +19 -0
  64. package/ui/UiOverride/useUiOverride.ts +15 -0
  65. package/ui/Unauthorized.tsx +12 -3
  66. package/ui/index.ts +10 -0
  67. package/webkit/bootCsr.tsx +28 -5
  68. package/webkit/index.ts +8 -1
  69. package/webkit/usePushNotification.tsx +267 -0
  70. package/types/webkit/usePushNoti.d.ts +0 -7
  71. package/webkit/usePushNoti.tsx +0 -48
@@ -107,6 +107,19 @@ export type CapacitorPushNotificationsModule = {
107
107
  requestPermissions: () => Promise<{ receive: "granted" | "denied" | string }>;
108
108
  checkPermissions: () => Promise<{ receive: "granted" | "denied" | string }>;
109
109
  register: () => Promise<void> | void;
110
+ addListener: (
111
+ eventName:
112
+ | "registration"
113
+ | "registrationError"
114
+ | "pushNotificationReceived"
115
+ | "pushNotificationActionPerformed"
116
+ | string,
117
+ listenerFunc: (event: {
118
+ value?: string;
119
+ error?: string;
120
+ notification?: { data?: Record<string, unknown> };
121
+ }) => void,
122
+ ) => Promise<{ remove?: () => Promise<void> | void } | void> | { remove?: () => Promise<void> | void } | void;
110
123
  };
111
124
  };
112
125
 
@@ -191,6 +191,8 @@ export interface Route {
191
191
  path: string;
192
192
  renderPage?: RouteRender;
193
193
  renderLayout?: RouteRender;
194
+ /** Synthetic layout render from a `_overrides.tsx` at this node; wraps the subtree in a UI-override provider. */
195
+ renderOverrides?: RouteRender;
194
196
  pageIncludesOwnLayout?: boolean;
195
197
  isSpecialRoute?: boolean;
196
198
 
@@ -1,11 +1,13 @@
1
1
  const ROUTE_SOURCE_RE = /^\.\/(.+)\.(tsx|ts|jsx|js)$/;
2
2
  const SOURCE_EXT_RE = /\.(tsx|ts|jsx|js)$/;
3
- const RESERVED_ROUTE_FILES = new Set(["_layout", "_index"]);
3
+ const RESERVED_ROUTE_FILES = new Set(["_layout", "_index", "_overrides"]);
4
4
  const INTERNAL_ROOT_LAYOUT_LEAF = "__root_layout";
5
5
  const IMPLICIT_LOCALE_SEGMENT = "[lang]";
6
6
  const SPECIAL_ROUTE_LEAVES = new Set(["robots.txt"]);
7
7
 
8
- export type RouteModuleKind = "page" | "layout";
8
+ const DIRECTORY_SCOPED_LEAVES = new Set(["_layout", "_index", "_overrides"]);
9
+
10
+ export type RouteModuleKind = "page" | "layout" | "overrides";
9
11
 
10
12
  export interface ParsedRouteModuleKey {
11
13
  key: string;
@@ -51,7 +53,7 @@ export function validatePageSourceFile(filePath: string, options: ValidatePageSo
51
53
  if (ext !== "tsx") throw new Error(`[route-convention] route source files under page/ must use .tsx: ${displayPath}`);
52
54
  if (leaf.startsWith("_") && !RESERVED_ROUTE_FILES.has(leaf) && leaf !== INTERNAL_ROOT_LAYOUT_LEAF)
53
55
  throw new Error(
54
- `[route-convention] only _index.tsx and _layout.tsx are allowed as reserved route files under page/: ${displayPath}`,
56
+ `[route-convention] only _index.tsx, _layout.tsx and _overrides.tsx are allowed as reserved route files under page/: ${displayPath}`,
55
57
  );
56
58
  if (/^[A-Z]/.test(leaf))
57
59
  throw new Error(`[route-convention] route page filenames must not start with an uppercase letter: ${displayPath}`);
@@ -97,13 +99,14 @@ export function parseRouteModuleKey(key: string): ParsedRouteModuleKey {
97
99
  }
98
100
 
99
101
  const isInternalRootLayout = leaf === INTERNAL_ROOT_LAYOUT_LEAF;
100
- const kind: RouteModuleKind = leaf === "_layout" || isInternalRootLayout ? "layout" : "page";
102
+ const kind: RouteModuleKind =
103
+ leaf === "_layout" || isInternalRootLayout ? "layout" : leaf === "_overrides" ? "overrides" : "page";
101
104
  if (leaf.startsWith("_") && !RESERVED_ROUTE_FILES.has(leaf) && !isInternalRootLayout) {
102
105
  throw new Error(`[route-convention] unsupported reserved route file "${leaf}" in ${key}`);
103
106
  }
104
107
 
105
108
  const sourceRouteSegments =
106
- leaf === "_layout" || leaf === "_index" || isInternalRootLayout ? moduleSegments.slice(0, -1) : moduleSegments;
109
+ DIRECTORY_SCOPED_LEAVES.has(leaf) || isInternalRootLayout ? moduleSegments.slice(0, -1) : moduleSegments;
107
110
  const isSpecialRoute = kind === "page" && SPECIAL_ROUTE_LEAVES.has(leaf);
108
111
  const routeSegments = isSpecialRoute ? sourceRouteSegments : [IMPLICIT_LOCALE_SEGMENT, ...sourceRouteSegments];
109
112
  for (const segment of routeSegments) validateRouteSegment(segment, key);
@@ -53,21 +53,67 @@ export type DocumentQueryValue =
53
53
  | undefined
54
54
  | Record<string, unknown>;
55
55
 
56
- export interface DocumentUpdate {
57
- [key: string]: unknown;
58
- set?: Record<string, unknown>;
59
- unset?: Record<string, unknown> | string[];
60
- addToSet?: Record<string, unknown>;
61
- pull?: Record<string, unknown>;
62
- push?: Record<string, unknown>;
63
- inc?: Record<string, number>;
64
- setOnInsert?: Record<string, unknown>;
56
+ export type DocumentUpdateOperator =
57
+ | "set"
58
+ | "unset"
59
+ | "inc"
60
+ | "mul"
61
+ | "min"
62
+ | "max"
63
+ | "push"
64
+ | "pull"
65
+ | "addToSet"
66
+ | "setOnInsert";
67
+
68
+ export interface DocumentUpdateNode {
69
+ kind: "update";
70
+ op: DocumentUpdateOperator;
71
+ value?: unknown;
65
72
  }
66
73
 
74
+ export type DocumentUpdateValue =
75
+ | DocumentUpdateNode
76
+ | DocumentPrimitive
77
+ | DocumentPrimitive[]
78
+ | Record<string, unknown>
79
+ | undefined;
80
+
81
+ export type DocumentUpdate<T = any> = {
82
+ [K in DocumentPath<T>]?: DocumentUpdateValue;
83
+ };
84
+
67
85
  export interface DocumentUpdateOptions {
68
86
  upsert?: boolean;
69
87
  }
70
88
 
89
+ const updateOp = (op: DocumentUpdateOperator, value?: unknown): DocumentUpdateNode => ({ kind: "update", op, value });
90
+
91
+ export const createDocumentUpdateHelper = () => ({
92
+ set: (value: unknown) => updateOp("set", value),
93
+ unset: () => updateOp("unset"),
94
+ inc: (by = 1) => updateOp("inc", by),
95
+ mul: (by: number) => updateOp("mul", by),
96
+ min: (value: unknown) => updateOp("min", value),
97
+ max: (value: unknown) => updateOp("max", value),
98
+ push: (value: unknown) => updateOp("push", value),
99
+ pull: (value: unknown) => updateOp("pull", value),
100
+ addToSet: (value: unknown) => updateOp("addToSet", value),
101
+ setOnInsert: (value: unknown) => updateOp("setOnInsert", value),
102
+ });
103
+
104
+ export type DocumentUpdateHelper = ReturnType<typeof createDocumentUpdateHelper>;
105
+
106
+ export const documentUpdateHelper = createDocumentUpdateHelper();
107
+
108
+ export type DocumentUpdateBuilder<T = any> = (helper: DocumentUpdateHelper) => DocumentUpdate<T>;
109
+ export type DocumentUpdateInput<T = any> = DocumentUpdate<T> | DocumentUpdateBuilder<T>;
110
+
111
+ export const resolveDocumentUpdate = <T>(update: DocumentUpdateInput<T>): DocumentUpdate<T> =>
112
+ typeof update === "function" ? update(documentUpdateHelper) : update;
113
+
114
+ export const isDocumentUpdateNode = (value: unknown): value is DocumentUpdateNode =>
115
+ !!value && typeof value === "object" && (value as { kind?: unknown }).kind === "update";
116
+
71
117
  export type DocumentQuery<T = any> =
72
118
  | DocumentQueryNode
73
119
  | {
package/document/into.ts CHANGED
@@ -4,7 +4,7 @@ import type { DocumentModel, QueryOf } from "akanjs/constant";
4
4
  import type { FilterCls, FilterQueryOf, FilterSortOf, SchemaOf } from ".";
5
5
  import type { CacheDatabase, QueryMethodPart } from "./database";
6
6
  import type { DataLoader } from "./dataLoader";
7
- import type { DocumentQuery, DocumentUpdate, DocumentUpdateOptions } from "./documentQuery";
7
+ import type { DocumentQuery, DocumentUpdateInput, DocumentUpdateOptions } from "./documentQuery";
8
8
  import { type LoaderBuilder, type ModelCls, makeLoaderBuilder } from "./loaderInfo";
9
9
  import type { DocumentProjection } from "./types";
10
10
 
@@ -50,7 +50,7 @@ export interface UpdateResult {
50
50
  export interface BulkWriteOperation<Raw, _RawDoc = DocumentModel<Raw>, _RawQuery = DocumentQuery<_RawDoc>> {
51
51
  updateOne: {
52
52
  filter: _RawQuery;
53
- update: DocumentUpdate;
53
+ update: DocumentUpdateInput<_RawDoc>;
54
54
  upsert?: boolean;
55
55
  };
56
56
  }
@@ -79,8 +79,12 @@ export type Mdl<
79
79
  findById(id: string | undefined, projection?: _Projection): Promise<Doc | null>;
80
80
  countDocuments(query: _RawQuery): Promise<number>;
81
81
  exists(query: _RawQuery): Promise<string | null>;
82
- updateOne(query: _RawQuery, update: DocumentUpdate, options?: DocumentUpdateOptions): Promise<UpdateResult>;
83
- updateMany(query: _RawQuery, update: DocumentUpdate): Promise<UpdateResult>;
82
+ updateOne(
83
+ query: _RawQuery,
84
+ update: DocumentUpdateInput<_RawDoc>,
85
+ options?: DocumentUpdateOptions,
86
+ ): Promise<UpdateResult>;
87
+ updateMany(query: _RawQuery, update: DocumentUpdateInput<_RawDoc>): Promise<UpdateResult>;
84
88
  deleteMany(query: _RawQuery): Promise<UpdateResult>;
85
89
  bulkWrite(operations: BulkWriteOperation<Raw, _RawDoc, _RawQuery>[]): Promise<UpdateResult>;
86
90
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "2.3.11-rc.1",
3
+ "version": "2.3.11-rc.10",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -198,6 +198,7 @@
198
198
  "cordova-plugin-purchase": "^13.16.0",
199
199
  "croner": "^10.0.1",
200
200
  "daisyui": "5.5.23",
201
+ "firebase": "^12.13.0",
201
202
  "ioredis": "^5.10.1",
202
203
  "mermaid": "^11.15.0",
203
204
  "postgres": "^3.4.9",
@@ -288,6 +289,9 @@
288
289
  "daisyui": {
289
290
  "optional": true
290
291
  },
292
+ "firebase": {
293
+ "optional": true
294
+ },
291
295
  "ioredis": {
292
296
  "optional": true
293
297
  },
@@ -49,7 +49,12 @@ export class LocaleWebProxy implements WebProxy {
49
49
  (locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`,
50
50
  );
51
51
 
52
- if (!isInternalProxyRequest(requestUrl) && !isWellKnownRequest(pathname) && pathnameIsMissingLocale) {
52
+ if (
53
+ !isInternalProxyRequest(requestUrl) &&
54
+ !isWellKnownRequest(pathname) &&
55
+ !isApiRequest(pathname) &&
56
+ pathnameIsMissingLocale
57
+ ) {
53
58
  return Response.redirect(
54
59
  new URL(`/${getLocale(request)}/${pathname.slice(1)}${targetUrl.search}`, getPublicRequestUrl(request)),
55
60
  307,
@@ -90,3 +95,7 @@ function isInternalProxyRequest(requestUrl: URL): boolean {
90
95
  function isWellKnownRequest(pathname: string): boolean {
91
96
  return pathname === "/.well-known" || pathname.startsWith("/.well-known/");
92
97
  }
98
+
99
+ function isApiRequest(pathname: string): boolean {
100
+ return pathname === "/api" || pathname.startsWith("/api/");
101
+ }
@@ -9,6 +9,7 @@ import {
9
9
  type DataInputOf,
10
10
  DataLoader,
11
11
  DocumentSchema,
12
+ type DocumentUpdateInput,
12
13
  documentQueryHelper,
13
14
  type FindQueryOption,
14
15
  fillMissingFilterArgs,
@@ -219,12 +220,12 @@ export class DatabaseResolver {
219
220
  findOne: (query: QueryOf<any>) => createFindOneChain(query),
220
221
  findById: (id: string | undefined) => (id ? store.findOne({ id }) : Promise.resolve(null)),
221
222
  countDocuments: (query: QueryOf<any>) => store.count(query),
222
- updateOne: (query: QueryOf<any>, update: Record<string, unknown>, options?: { upsert?: boolean }) =>
223
+ updateOne: (query: QueryOf<any>, update: DocumentUpdateInput, options?: { upsert?: boolean }) =>
223
224
  store.updateOneByQuery(query, update, options),
224
- updateMany: (query: QueryOf<any>, update: Record<string, unknown>) => store.updateManyByQuery(query, update),
225
+ updateMany: (query: QueryOf<any>, update: DocumentUpdateInput) => store.updateManyByQuery(query, update),
225
226
  deleteMany: (query: QueryOf<any>) => store.deleteManyByQuery(query),
226
227
  bulkWrite: (
227
- operations: { updateOne: { filter: QueryOf<any>; update: Record<string, unknown>; upsert?: boolean } }[],
228
+ operations: { updateOne: { filter: QueryOf<any>; update: DocumentUpdateInput; upsert?: boolean } }[],
228
229
  ) => store.bulkWrite(operations),
229
230
  listenPre: (type: SaveEventType, listener: (doc: any, type: CRUDEventType) => PromiseOrObject<void>) =>
230
231
  schema.pre(type, function (this: any, _next, crudType) {
@@ -158,6 +158,16 @@ export class SignalResolver {
158
158
  const serviceName = `${refName}Service`;
159
159
  const capitalizedRefName = capitalize(refName);
160
160
 
161
+ const assertSliceQuery = (query: unknown, key: string) => {
162
+ if (Array.isArray(query))
163
+ throw new Error(
164
+ `Slice "${refName}.${key}" exec returned an array instead of a query descriptor. ` +
165
+ `Return a query from the slice's service (e.g. this.${refName}Service.queryBy...(...)), ` +
166
+ `not an executed list (listBy.../findMany...), which resolves to an array.`,
167
+ );
168
+ return query;
169
+ };
170
+
161
171
  class SliceEndpoint extends sliceEndpoint(sliceCls.srv, (builder) => {
162
172
  const endpointObj: { [key: string]: EndpointInfo } = {};
163
173
  Object.entries(sliceMeta).forEach(([key, sliceInfo]) => {
@@ -179,7 +189,10 @@ export class SignalResolver {
179
189
  const limit = Number(requestArgs[argLength + 1] ?? 20);
180
190
  const sort = requestArgs[argLength + 2] ?? "latest";
181
191
  const internalArgs = requestArgs.slice(argLength + 3);
182
- const query = await sliceInfo.execFn?.apply(this, [...args, ...internalArgs, documentQueryHelper]);
192
+ const query = assertSliceQuery(
193
+ await sliceInfo.execFn?.apply(this, [...args, ...internalArgs, documentQueryHelper]),
194
+ key,
195
+ );
183
196
  return (await this[serviceName].__list(query, {
184
197
  skip,
185
198
  limit,
@@ -196,7 +209,10 @@ export class SignalResolver {
196
209
  .exec(async function (this: any, ...requestArgs: any) {
197
210
  const args = requestArgs.slice(0, argLength);
198
211
  const internalArgs = requestArgs.slice(argLength);
199
- const query = await sliceInfo.execFn?.apply(this, [...args, ...internalArgs, documentQueryHelper]);
212
+ const query = assertSliceQuery(
213
+ await sliceInfo.execFn?.apply(this, [...args, ...internalArgs, documentQueryHelper]),
214
+ key,
215
+ );
200
216
  return await this[serviceName].__insight(query);
201
217
  });
202
218
  });
@@ -18,9 +18,12 @@ import {
18
18
  parseRouteModuleKey,
19
19
  routeSegmentToTreePath,
20
20
  } from "akanjs/common";
21
+ import { createElement } from "react";
21
22
  import { validatePageConfig } from "../client/frameConfig";
22
23
  import { resolveHeadExport, resolveMetadataHead } from "./metadata";
23
24
 
25
+ type RouteModuleKindWithOverrides = "page" | "layout" | "overrides";
26
+
24
27
  export type PagesContext = Record<string, () => Promise<RouteModule>>;
25
28
 
26
29
  export const defaultPageState: PageState = {
@@ -197,6 +200,14 @@ export class RouteTreeBuilder {
197
200
  const targetPath = pathSegments[pathSegments.length - 1];
198
201
  if (!targetPath) return;
199
202
 
203
+ if (parsed.kind === "overrides") {
204
+ targetRouteMap.set(targetPath, {
205
+ ...(targetRouteMap.get(targetPath) ?? { path: targetPath, children: new Map<string, Route>() }),
206
+ renderOverrides: this.#makeOverridesRender(filePath, loader),
207
+ } as Route);
208
+ return;
209
+ }
210
+
200
211
  const routeRender = RouteTreeBuilder.#makeRouteRender(filePath, parsed.kind, loader);
201
212
  targetRouteMap.set(targetPath, {
202
213
  ...(targetRouteMap.get(targetPath) ?? { path: targetPath, children: new Map<string, Route>() }),
@@ -224,8 +235,10 @@ export class RouteTreeBuilder {
224
235
  const pathSegments = [...parentPaths, ...(currentPathSegment ? [currentPathSegment] : [])];
225
236
  const currentRootLayout = isRoot && route.renderLayout ? route.renderLayout : null;
226
237
  const currentLayout = !isRoot && route.renderLayout ? route.renderLayout : null;
238
+
239
+ const currentOverrideRenders = route.renderOverrides ? [route.renderOverrides] : [];
227
240
  const renderRootLayouts = [...parentRootLayouts, ...(currentRootLayout ? [currentRootLayout] : [])];
228
- const renderLayouts = [...parentLayouts, ...(currentLayout ? [currentLayout] : [])];
241
+ const renderLayouts = [...parentLayouts, ...currentOverrideRenders, ...(currentLayout ? [currentLayout] : [])];
229
242
  if (route.renderLayout) {
230
243
  this.#fallbackRoutes.push({
231
244
  path: routePath,
@@ -237,7 +250,10 @@ export class RouteTreeBuilder {
237
250
  const routeHead = RouteTreeBuilder.#composeHeadResolvers(route.renderLayout?.resolveHead, parentHead);
238
251
  const pageRenderRootLayouts =
239
252
  route.pageIncludesOwnLayout === false && currentRootLayout ? parentRootLayouts : renderRootLayouts;
240
- const pageRenderLayouts = route.pageIncludesOwnLayout === false && currentLayout ? parentLayouts : renderLayouts;
253
+ const pageRenderLayouts =
254
+ route.pageIncludesOwnLayout === false && currentLayout
255
+ ? [...parentLayouts, ...currentOverrideRenders]
256
+ : renderLayouts;
241
257
  const pageHead = route.pageIncludesOwnLayout === false ? parentHead : routeHead;
242
258
  return [
243
259
  ...(route.renderPage
@@ -262,7 +278,7 @@ export class RouteTreeBuilder {
262
278
  ];
263
279
  }
264
280
 
265
- static #makeLazyModule(key: string, kind: "page" | "layout", loader: () => Promise<RouteModule>) {
281
+ static #makeLazyModule(key: string, kind: RouteModuleKindWithOverrides, loader: () => Promise<RouteModule>) {
266
282
  let cached: RouteModule | null = null;
267
283
  let loaded = false;
268
284
  RouteTreeBuilder.#moduleCacheStats.moduleCount += 1;
@@ -285,7 +301,12 @@ export class RouteTreeBuilder {
285
301
  };
286
302
  }
287
303
 
288
- static #validateRouteModuleExports(key: string, kind: "page" | "layout", mod: RouteModule) {
304
+ static #validateRouteModuleExports(key: string, kind: RouteModuleKindWithOverrides, mod: RouteModule) {
305
+ if (kind === "overrides") {
306
+
307
+ if (!mod.default) throw new Error(`[route-convention] ${key} generated override wrapper has no default export`);
308
+ return;
309
+ }
289
310
  const parsed = parseRouteModuleKey(key);
290
311
  const allowed =
291
312
  kind === "page"
@@ -378,6 +399,18 @@ export class RouteTreeBuilder {
378
399
  return routeRender;
379
400
  }
380
401
 
402
+ #makeOverridesRender(key: string, loader: () => Promise<RouteModule>): RouteRender {
403
+ const loadModule = RouteTreeBuilder.#makeLazyModule(key, "overrides", loader);
404
+ return {
405
+ isAsync: true,
406
+ render: (async ({ children }: LayoutProps) => {
407
+ const mod = await loadModule();
408
+ if (!mod.default) throw new Error(`[route-convention] ${key} generated override wrapper has no default export`);
409
+ return createElement(mod.default as never, { children } as never);
410
+ }) as RouteRender["render"],
411
+ };
412
+ }
413
+
381
414
  static #composeHeadResolvers(...resolvers: (ResolveHead | undefined)[]): ResolveHead | undefined {
382
415
  const chain = resolvers.filter((resolver): resolver is ResolveHead => Boolean(resolver));
383
416
  if (chain.length === 0) return undefined;