akanjs 3.0.0-beta.16 → 3.0.0-beta.18

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 (95) hide show
  1. package/base/base.ts +1 -1
  2. package/client/csrTypes.ts +1 -31
  3. package/client/frameConfig.ts +2 -2
  4. package/client/route/RootLayoutDefinition.ts +0 -6
  5. package/client/route/RouteDefinition.ts +1 -13
  6. package/common/CodeTranscript.ts +12 -0
  7. package/common/codeAgentProfile.ts +54 -0
  8. package/common/codeAgentWire.ts +31 -0
  9. package/common/index.ts +5 -0
  10. package/common/routeConvention.ts +0 -3
  11. package/constant/constantRegistry.ts +8 -11
  12. package/document/dataLoader.ts +48 -15
  13. package/document/loaderInfo.ts +28 -7
  14. package/fetch/client/fetchClient.ts +14 -12
  15. package/fetch/client/httpClient.ts +1 -1
  16. package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
  17. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
  18. package/package.json +1 -1
  19. package/server/akanServer.ts +3 -1
  20. package/server/console.ts +9 -9
  21. package/server/decorators.ts +3 -64
  22. package/server/devtools/depsSerializer.ts +1 -4
  23. package/server/di/diLifecycle.ts +1 -3
  24. package/server/head.tsx +60 -0
  25. package/server/imageOptimizer.ts +3 -1
  26. package/server/resolver/database.resolver.ts +29 -26
  27. package/server/routeElementComposer.tsx +3 -3
  28. package/server/routeTreeBuilder.ts +1 -24
  29. package/server/rscWorker.tsx +10 -21
  30. package/server/webRouter.ts +12 -8
  31. package/service/injectInfo.ts +86 -72
  32. package/service/predefinedAdaptor/cache.adaptor.ts +42 -8
  33. package/service/predefinedAdaptor/storage.adaptor.ts +6 -6
  34. package/signal/endpointCache.ts +75 -0
  35. package/signal/guard.ts +4 -3
  36. package/signal/middleware.ts +0 -77
  37. package/signal/signalContext.ts +4 -14
  38. package/signal/types.ts +4 -5
  39. package/types/client/csrTypes.d.ts +1 -31
  40. package/types/client/route/RootLayoutDefinition.d.ts +0 -1
  41. package/types/client/route/RouteDefinition.d.ts +1 -3
  42. package/types/common/CodeTranscript.d.ts +3 -1
  43. package/types/common/codeAgentProfile.d.ts +54 -0
  44. package/types/common/codeAgentWire.d.ts +28 -0
  45. package/types/common/index.d.ts +2 -2
  46. package/types/constant/constantRegistry.d.ts +2 -2
  47. package/types/document/dataLoader.d.ts +6 -1
  48. package/types/document/loaderInfo.d.ts +9 -4
  49. package/types/server/console.d.ts +1 -1
  50. package/types/server/decorators.d.ts +1 -8
  51. package/types/server/head.d.ts +10 -0
  52. package/types/server/routeElementComposer.d.ts +1 -1
  53. package/types/service/injectInfo.d.ts +17 -14
  54. package/types/service/predefinedAdaptor/cache.adaptor.d.ts +1 -0
  55. package/types/service/predefinedAdaptor/storage.adaptor.d.ts +1 -2
  56. package/types/signal/endpointCache.d.ts +19 -0
  57. package/types/signal/guard.d.ts +4 -3
  58. package/types/signal/middleware.d.ts +0 -20
  59. package/types/signal/signalContext.d.ts +1 -6
  60. package/types/signal/types.d.ts +4 -5
  61. package/types/ui/Agent/Menu.d.ts +2 -2
  62. package/types/ui/Button.d.ts +1 -1
  63. package/types/ui/CsrImage.d.ts +2 -1
  64. package/types/ui/Data/Item.d.ts +1 -1
  65. package/types/ui/Link/CsrLink.d.ts +1 -1
  66. package/types/ui/Link/types.d.ts +2 -0
  67. package/types/ui/System/CSR.d.ts +2 -2
  68. package/types/ui/System/Client.d.ts +2 -3
  69. package/types/ui/System/Common.d.ts +0 -2
  70. package/types/ui/System/SSR.d.ts +2 -2
  71. package/types/ui/recipe/inputRecipe.d.ts +1 -1
  72. package/ui/Agent/Menu.tsx +2 -2
  73. package/ui/Button.tsx +1 -1
  74. package/ui/Constant/Doc.tsx +121 -77
  75. package/ui/Constant/Graph.tsx +11 -3
  76. package/ui/CsrImage.tsx +3 -2
  77. package/ui/Data/Item.tsx +1 -1
  78. package/ui/Field/Choice.tsx +1 -1
  79. package/ui/Field/Collection.tsx +4 -3
  80. package/ui/Image.tsx +1 -0
  81. package/ui/Link/CsrLink.tsx +5 -1
  82. package/ui/Link/types.ts +2 -0
  83. package/ui/System/CSR.tsx +1 -9
  84. package/ui/System/Client.tsx +2 -4
  85. package/ui/System/Common.tsx +0 -2
  86. package/ui/System/SSR.tsx +1 -9
  87. package/ui/recipe/inputRecipe.ts +1 -1
  88. package/webkit/bootCsr.tsx +0 -12
  89. package/webkit/useCamera.tsx +4 -3
  90. package/webkit/useCsrValues.ts +3 -3
  91. package/webkit/useFetch.ts +6 -7
  92. package/server/metadata.tsx +0 -198
  93. package/types/server/metadata.d.ts +0 -22
  94. package/types/ui/System/Gtag.d.ts +0 -4
  95. package/ui/System/Gtag.tsx +0 -69
@@ -207,7 +207,7 @@ export class HttpClient {
207
207
  const searchPath = searchParams.size > 0 ? `?${searchParams.toString()}` : "";
208
208
  const paramedPath = path.replace(/:(\w+)/g, (match, p1) => {
209
209
  const value = argMap.get(p1);
210
- return value === null || value === undefined ? match : String(value);
210
+ return value === null || value === undefined ? match : encodeURIComponent(String(value));
211
211
  });
212
212
  return `${paramedPath}${searchPath}`;
213
213
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-beta.16",
3
+ "version": "3.0.0-beta.18",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -268,7 +268,9 @@ export class AkanServer {
268
268
  }
269
269
  setMcp(mcp: boolean | McpServerOption = true) {
270
270
  if (this.status !== "stopped") throw new Error("MCP config must be set before app initialization.");
271
- this.mcp = typeof mcp === "boolean" ? mcp : (mcp.enabled ?? true);
271
+
272
+ const requested = typeof mcp === "boolean" ? mcp : (mcp.enabled ?? this.mcp);
273
+ this.mcp = requested && !AkanServer.#isEnvOff("AKAN_MCP", "AKAN_PUBLIC_MCP");
272
274
  if (typeof mcp === "boolean") return this;
273
275
  const { enabled: _enabled, readOnly, auth, ...rest } = mcp;
274
276
  if (readOnly !== undefined) this.mcpReadOnly = readOnly;
package/server/console.ts CHANGED
@@ -124,16 +124,16 @@ export interface AkanConsoleContext extends Record<string, unknown> {
124
124
  debug: () => ReturnType<AkanServer["inspectConsole"]>;
125
125
  }
126
126
 
127
- export const assertAkanConsoleAllowed = (
128
- env: Pick<BaseEnv, "environment" | "operationMode"> = {
129
- environment: (process.env.AKAN_PUBLIC_ENV ?? "debug") as BaseEnv["environment"],
130
- operationMode: (process.env.AKAN_PUBLIC_OPERATION_MODE ?? "cloud") as BaseEnv["operationMode"],
131
- },
132
- ) => {
127
+ export const assertAkanConsoleAllowed = (env: Partial<Pick<BaseEnv, "environment" | "operationMode">> = {}) => {
128
+ const environment = env.environment ?? ((process.env.AKAN_PUBLIC_ENV ?? "debug") as BaseEnv["environment"]);
129
+ const operationMode =
130
+ env.operationMode ??
131
+ ((process.env.AKAN_PUBLIC_OPERATION_MODE ??
132
+ (environment === "local" ? "local" : "cloud")) as BaseEnv["operationMode"]);
133
133
  const isProductionLike =
134
- env.environment === "main" ||
135
- env.operationMode === "cloud" ||
136
- env.operationMode === "edge" ||
134
+ environment === "main" ||
135
+ operationMode === "cloud" ||
136
+ operationMode === "edge" ||
137
137
  process.env.NODE_ENV === "production";
138
138
  if (!isProductionLike || process.env.AKAN_CONSOLE === "1") return;
139
139
 
@@ -1,27 +1,15 @@
1
1
  /**
2
- * Method decorators for a server class: `@Transaction`, `@Try`, `@CacheMethod`. Kept because `libs/util`'s
2
+ * Method decorators for a server class: `@Transaction`, `@Try`. Kept because `libs/util`'s
3
3
  * storage adaptors use `@Try` on their remote calls; new code follows the adaptor rule instead
4
4
  * (`catch` → `logger.error` → `return null`).
5
5
  */
6
6
 
7
7
  type DecoratedInstance = {
8
- logger?: { warn?: (message: string) => void; trace?: (message: string) => void };
8
+ logger?: { warn?: (message: string) => void };
9
9
  __database?: { transaction?: <T>(fn: () => Promise<T>) => Promise<T> };
10
- __databaseModel?: {
11
- __database?: { transaction?: <T>(fn: () => Promise<T>) => Promise<T> };
12
- __model?: { modelName: string };
13
- __cache?: {
14
- get: (key: string) => Promise<string | null>;
15
- set: (key: string, value: string, option: unknown) => unknown;
16
- };
17
- };
10
+ __databaseModel?: { __database?: { transaction?: <T>(fn: () => Promise<T>) => Promise<T> } };
18
11
  database?: { transaction?: <T>(fn: () => Promise<T>) => Promise<T> };
19
12
  connection?: { transaction?: <T>(fn: () => Promise<T>) => Promise<T> };
20
- __model?: { modelName: string };
21
- __cache?: {
22
- get: (key: string) => Promise<string | null>;
23
- set: (key: string, value: string, option: unknown) => unknown;
24
- };
25
13
  };
26
14
 
27
15
  /** Method decorator that catches errors and logs a warning instead of throwing. */
@@ -53,52 +41,3 @@ export const Transaction = (): MethodDecorator => {
53
41
  return descriptor;
54
42
  }) as MethodDecorator;
55
43
  };
56
-
57
- /**
58
- * Method decorator that caches method results for the given timeout in milliseconds.
59
- *
60
- * Not `Cache` — `akanjs/signal` exports a `Cache` **middleware class**, and an app importing both barrels got
61
- * one name meaning two things.
62
- */
63
- export const CacheMethod = (timeout = 1000, getCacheKey?: (...args: unknown[]) => string): MethodDecorator => {
64
- return ((_target: unknown, key: string, descriptor: PropertyDescriptor) => {
65
- const originMethod = descriptor.value as (this: unknown, ...args: unknown[]) => unknown;
66
- const cacheMap = new Map<string, unknown>();
67
- const timerMap = new Map<string, NodeJS.Timeout>();
68
- descriptor.value = async function (...args: unknown[]) {
69
- const instance = this as DecoratedInstance;
70
- const classType = instance.__model ? "doc" : instance.__databaseModel ? "service" : "class";
71
- const model = instance.__model ?? instance.__databaseModel?.__model;
72
- const cache = instance.__cache ?? instance.__databaseModel?.__cache;
73
- const cacheKeyValue = getCacheKey ? getCacheKey(...args) : JSON.stringify(args);
74
- const cacheKey = `${classType}:${model?.modelName ?? "unknown"}:${key}:${cacheKeyValue}`;
75
- const getCache = async (cacheKey: string) => {
76
- if (classType === "class") return cacheMap.get(cacheKey);
77
- const cached = (await cache?.get(cacheKey)) as string | null;
78
- if (cached) return JSON.parse(cached);
79
- return null;
80
- };
81
- const setCache = async (cacheKey: string, value: unknown) => {
82
- if (classType === "class") {
83
- const existingTimer = timerMap.get(cacheKey);
84
- if (existingTimer) clearTimeout(existingTimer);
85
- cacheMap.set(cacheKey, value);
86
- const timer = setTimeout(() => {
87
- cacheMap.delete(cacheKey);
88
- timerMap.delete(cacheKey);
89
- }, timeout);
90
- timerMap.set(cacheKey, timer);
91
- } else await cache?.set(cacheKey, JSON.stringify(value), { PX: timeout });
92
- };
93
- const cachedData = await getCache(cacheKey);
94
- if (cachedData) {
95
- instance.logger?.trace?.(`${model?.modelName ?? "unknown"} cache hit: ${cacheKey}`);
96
- return cachedData;
97
- }
98
- const result = await originMethod.apply(this, args);
99
- await setCache(cacheKey, result);
100
- instance.logger?.trace?.(`${model?.modelName ?? "unknown"} cache set: ${cacheKey}`);
101
- return result;
102
- };
103
- }) as MethodDecorator;
104
- };
@@ -1,7 +1,6 @@
1
1
  import { type BaseEnv, INJECT_META } from "akanjs/base";
2
2
  import type { AdaptorCls, InjectInfo, ServiceCls } from "akanjs/service";
3
3
  import type { DiLifecycle } from "../di/diLifecycle";
4
- import { DevtoolsJson } from "./devtoolsJson";
5
4
  import type { DepEdge, DepEdgeKind, DepNode, DepNodeKind, DepsData } from "./types";
6
5
 
7
6
  export interface DepsSerializerContext {
@@ -193,9 +192,7 @@ export class DepsSerializer {
193
192
  detail: {
194
193
  local: Boolean(injectInfo.local),
195
194
  isMap: Boolean(injectInfo.isMap),
196
- ...(injectInfo.cacheOption?.expireAt !== undefined
197
- ? { expireAt: DevtoolsJson.toSafe(injectInfo.cacheOption.expireAt) }
198
- : {}),
195
+ ...(injectInfo.ttl !== undefined ? { ttl: injectInfo.ttl } : {}),
199
196
  },
200
197
  };
201
198
  }
@@ -17,7 +17,7 @@ import { agentTurnConstant, agentTurnDocument } from "../../signal/agentTurn";
17
17
  import { Base, BaseEndpoint, BaseInternal } from "../../signal/base.signal";
18
18
  import type { Endpoint } from "../../signal/endpoint";
19
19
  import type { Internal } from "../../signal/internal";
20
- import { Cache, Logging, type MiddlewareCls, Timeout } from "../../signal/middleware";
20
+ import { Logging, type MiddlewareCls, Timeout } from "../../signal/middleware";
21
21
  import type { ServerSignal, ServerSignalCls } from "../../signal/serverSignal";
22
22
  import { SignalRegistry } from "../../signal/signalRegistry";
23
23
  import type { AkanLib, DatabaseModule, ScalarModule, ServiceModule } from "../akanLib";
@@ -128,8 +128,6 @@ export class DiLifecycle {
128
128
  this.#middleware.set(Logging.refName, Logging);
129
129
 
130
130
  this.#middleware.set(Timeout.refName, Timeout);
131
-
132
- this.#middleware.set(Cache.refName, Cache);
133
131
  const defaultOption = createDefaultAkanOption();
134
132
  defaultOption.getMiddlewares().forEach((middleware) => {
135
133
  this.#middleware.set(middleware.refName, middleware);
@@ -0,0 +1,60 @@
1
+ import type { Head, ResolvedHead, ResolveHeadResult } from "akanjs/client";
2
+ import { AKAN_RSC_HEAD_SNAPSHOT_VERSION, type AkanHeadSnapshotV1 } from "./routeState";
3
+
4
+ function isRecord(value: unknown): value is Record<string, unknown> {
5
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
6
+ }
7
+
8
+ export function createAkanLocaleAlternateHeadSnapshot(languages: Record<string, string>): AkanHeadSnapshotV1 {
9
+ return {
10
+ version: AKAN_RSC_HEAD_SNAPSHOT_VERSION,
11
+ nodes: Object.entries(languages).map(([lang, href]) => ({
12
+ tag: "link",
13
+ attrs: { rel: "alternate", hrefLang: lang, href },
14
+ })),
15
+ };
16
+ }
17
+
18
+ export function mergeAkanHeadSnapshots(
19
+ ...snapshots: Array<AkanHeadSnapshotV1 | null | undefined>
20
+ ): AkanHeadSnapshotV1 | undefined {
21
+ const nodes = snapshots.flatMap((snapshot) => snapshot?.nodes ?? []);
22
+ return snapshots.some(Boolean) ? { version: AKAN_RSC_HEAD_SNAPSHOT_VERSION, nodes } : undefined;
23
+ }
24
+
25
+ export function renderAkanHeadSnapshot(snapshot: AkanHeadSnapshotV1): Head {
26
+ return (
27
+ <>
28
+ {snapshot.nodes.map((node, index) => {
29
+ const marker = {
30
+ "data-akan-head": "route",
31
+ "data-akan-head-key": `${node.tag}:${index}`,
32
+ };
33
+ if (node.tag === "title") {
34
+ return (
35
+ <title key={`${node.tag}:${index}`} {...marker}>
36
+ {node.text ?? ""}
37
+ </title>
38
+ );
39
+ }
40
+ if (node.tag === "meta") {
41
+ return <meta key={`${node.tag}:${index}`} {...node.attrs} {...marker} />;
42
+ }
43
+ return <link key={`${node.tag}:${index}`} {...node.attrs} {...marker} />;
44
+ })}
45
+ </>
46
+ );
47
+ }
48
+
49
+ export function shouldRenderLocaleAlternates(options: { isSpecialRoute?: boolean }): boolean {
50
+ return options.isSpecialRoute !== true;
51
+ }
52
+
53
+ export function isResolvedHead(value: unknown): value is ResolvedHead {
54
+ return isRecord(value) && "node" in value;
55
+ }
56
+
57
+ export function resolveHeadResult(value: ResolveHeadResult): ResolvedHead {
58
+ if (isResolvedHead(value)) return value;
59
+ return { node: value };
60
+ }
@@ -61,6 +61,7 @@ export class ImageOptimizer {
61
61
  static readonly #avif = "image/avif";
62
62
  static readonly #heic = "image/heic";
63
63
  static readonly #bypassTypes = new Set(["image/x-icon", "image/x-icns", "image/bmp", "image/jxl", "image/heic"]);
64
+ static readonly #encodableTypes = new Set(["image/jpeg", "image/png", "image/webp", "image/avif"]);
64
65
 
65
66
  #publicDir: string;
66
67
  #cacheDir: string;
@@ -329,6 +330,7 @@ export class ImageOptimizer {
329
330
  shouldBypass || !params.outputType || inputType === ImageOptimizer.#webp || inputType === ImageOptimizer.#avif
330
331
  ? inputType
331
332
  : params.outputType;
333
+ const shouldEncode = !shouldBypass && ImageOptimizer.#encodableTypes.has(outputType);
332
334
  const cachePath = this.#getCachePath({
333
335
  href: params.href,
334
336
  width: params.width,
@@ -351,7 +353,7 @@ export class ImageOptimizer {
351
353
  let buffer = source.buffer;
352
354
  let contentType = inputType;
353
355
  let cacheable = true;
354
- if (!shouldBypass) {
356
+ if (shouldEncode) {
355
357
  try {
356
358
  buffer = await this.#semaphore.run(() =>
357
359
  ImageOptimizer.#optimize(source.buffer, {
@@ -155,37 +155,40 @@ export class DatabaseResolver {
155
155
  });
156
156
  Object.entries(getLoaderInfos(database.model)).forEach(([key, loaderInfo]) => {
157
157
  Object.assign(this, {
158
- [key]: new DataLoader<any, any>(async (keys) => {
159
- traceDataLoaderBatch(keys.length);
160
- if (loaderInfo.type === "query") {
161
- const fields = loaderInfo.field as string[];
162
- const query = { kind: "any", queries: keys } as QueryOf<unknown>;
158
+ [key]: new DataLoader<any, any>(
159
+ async (keys) => {
160
+ traceDataLoaderBatch(keys.length);
161
+ if (loaderInfo.type === "query") {
162
+ const fields = loaderInfo.field as string[];
163
+ const query = { kind: "any", queries: keys } as QueryOf<unknown>;
164
+ const docs = await timedQuery(() =>
165
+ this.__store.find(documentQueryHelper.all(loaderInfo.defaultQuery, query)),
166
+ );
167
+ const byKey = new Map(docs.map((doc) => [fields.map((field) => String(doc[field])).join(""), doc]));
168
+ return keys.map(
169
+ (queryKey) => byKey.get(fields.map((field) => String(queryKey[field])).join("")) ?? null,
170
+ );
171
+ }
172
+ const field = loaderInfo.field as string;
173
+ const query = {
174
+ [field]: documentQueryHelper.oneOf([...keys]),
175
+ };
163
176
  const docs = await timedQuery(() =>
164
177
  this.__store.find(documentQueryHelper.all(loaderInfo.defaultQuery, query)),
165
178
  );
166
- const byKey = new Map(docs.map((doc) => [fields.map((field) => String(doc[field])).join(""), doc]));
167
- return keys.map(
168
- (queryKey) => byKey.get(fields.map((field) => String(queryKey[field])).join("")) ?? null,
169
- );
170
- }
171
- const field = loaderInfo.field as string;
172
- const query = {
173
- [field]: documentQueryHelper.oneOf([...keys]),
174
- };
175
- const docs = await timedQuery(() =>
176
- this.__store.find(documentQueryHelper.all(loaderInfo.defaultQuery, query)),
177
- );
178
- if (loaderInfo.type === "arrayField") {
179
- const byKey = new Map<string, unknown>();
180
- for (const doc of docs) {
181
- const values = Array.isArray(doc[field]) ? doc[field] : [];
182
- for (const value of values) if (!byKey.has(String(value))) byKey.set(String(value), doc);
179
+ if (loaderInfo.type === "arrayField") {
180
+ const byKey = new Map<string, unknown>();
181
+ for (const doc of docs) {
182
+ const values = Array.isArray(doc[field]) ? doc[field] : [];
183
+ for (const value of values) if (!byKey.has(String(value))) byKey.set(String(value), doc);
184
+ }
185
+ return keys.map((key) => byKey.get(String(key)) ?? null);
183
186
  }
187
+ const byKey = new Map(docs.map((doc) => [String(doc[field]), doc]));
184
188
  return keys.map((key) => byKey.get(String(key)) ?? null);
185
- }
186
- const byKey = new Map(docs.map((doc) => [String(doc[field]), doc]));
187
- return keys.map((key) => byKey.get(String(key)) ?? null);
188
- }),
189
+ },
190
+ { name: key, cache: loaderInfo.cache },
191
+ ),
189
192
  });
190
193
  });
191
194
  }
@@ -10,7 +10,7 @@ import type {
10
10
  } from "akanjs/client";
11
11
  import { Children, cloneElement, isValidElement, type ReactElement, type ReactNode, Suspense } from "react";
12
12
  import { getExplicitPageConfigKeys, resolvePageState } from "../client/frameConfig";
13
- import { resolveHeadResult } from "./metadata";
13
+ import { resolveHeadResult } from "./head";
14
14
  import { type AkanRouteSegmentState, createAkanRouteSegments, createAkanSegmentOutletKey } from "./routeState";
15
15
  import { isAkanRscPartialCommitEnabled } from "./rscPartialCommit";
16
16
  import { AkanSegmentOutletReference } from "./rscSegmentOutletReference";
@@ -118,7 +118,7 @@ export class RouteElementComposer {
118
118
  searchParams: Record<string, string | string[]>;
119
119
  }): Promise<Head | null | undefined> {
120
120
  return (
121
- await RouteElementComposer.resolveHeadWithMetadata({
121
+ await RouteElementComposer.resolveHeadWithSnapshot({
122
122
  pathRoute,
123
123
  params,
124
124
  searchParams,
@@ -126,7 +126,7 @@ export class RouteElementComposer {
126
126
  ).node;
127
127
  }
128
128
 
129
- static async resolveHeadWithMetadata({
129
+ static async resolveHeadWithSnapshot({
130
130
  pathRoute,
131
131
  params,
132
132
  searchParams,
@@ -26,7 +26,6 @@ import {
26
26
  type RouteModuleSource,
27
27
  resolveRouteModule,
28
28
  } from "../client/route/resolveRouteModule";
29
- import { resolveHeadExport, resolveMetadataHead } from "./metadata";
30
29
 
31
30
  type RouteModuleKindWithOverrides = "page" | "layout" | "overrides";
32
31
 
@@ -295,18 +294,6 @@ export class RouteTreeBuilder {
295
294
  if ("head" in mod && "generateHead" in mod) {
296
295
  throw new Error(`[route-convention] head and generateHead cannot both be exported in ${key}`);
297
296
  }
298
- if (
299
- !parsed.isInternalRootLayout &&
300
- ("head" in mod || "generateHead" in mod) &&
301
- ("metadata" in mod || "generateMetadata" in mod)
302
- ) {
303
- throw new Error(
304
- `[route-convention] head/generateHead and metadata/generateMetadata cannot both be exported in ${key}`,
305
- );
306
- }
307
- if ("metadata" in mod && "generateMetadata" in mod) {
308
- throw new Error(`[route-convention] metadata and generateMetadata cannot both be exported in ${key}`);
309
- }
310
297
  }
311
298
 
312
299
  static #makeRouteRender(key: string, kind: "page" | "layout", loader: () => Promise<RouteModuleSource>): RouteRender {
@@ -336,17 +323,7 @@ export class RouteTreeBuilder {
336
323
  routeRender.NotFound = layoutMod.NotFound;
337
324
  routeRender.Error = layoutMod.Error;
338
325
  }
339
- if (mod.generateHead) {
340
- const head = await mod.generateHead(props);
341
- if (head !== null && head !== undefined) return resolveHeadExport(head, { includeHeadSnapshot: false });
342
- }
343
- if (mod.generateMetadata) {
344
- const metadata = await mod.generateMetadata(props);
345
- return metadata === null || metadata === undefined ? metadata : resolveMetadataHead(metadata);
346
- }
347
- if (mod.head !== undefined)
348
- return mod.head === null ? null : resolveHeadExport(mod.head, { includeHeadSnapshot: false });
349
- return mod.metadata === undefined ? undefined : resolveMetadataHead(mod.metadata);
326
+ return mod.generateHead ? await mod.generateHead(props) : mod.head;
350
327
  },
351
328
  };
352
329
  if (kind === "page") {
@@ -36,13 +36,13 @@ import {
36
36
  resolveRouteCacheStoreTtl,
37
37
  shouldStoreRouteCache,
38
38
  } from "./cachePolicy";
39
- import { LogForwarder } from "./logging/logForwarder";
40
39
  import {
41
40
  createAkanLocaleAlternateHeadSnapshot,
42
41
  mergeAkanHeadSnapshots,
43
42
  renderAkanHeadSnapshot,
44
43
  shouldRenderLocaleAlternates,
45
- } from "./metadata";
44
+ } from "./head";
45
+ import { LogForwarder } from "./logging/logForwarder";
46
46
  import { ProcessMetricsCollector } from "./processMetricsCollector";
47
47
  import { RouteElementComposer } from "./routeElementComposer";
48
48
  import {
@@ -1311,15 +1311,13 @@ export class RscRenderer {
1311
1311
  if (!body) return null;
1312
1312
  const routeHead =
1313
1313
  "resolveHead" in route
1314
- ? await RouteElementComposer.resolveHeadWithMetadata({
1314
+ ? await RouteElementComposer.resolveHeadWithSnapshot({
1315
1315
  pathRoute: route,
1316
1316
  params,
1317
1317
  searchParams,
1318
1318
  })
1319
- : { node: undefined, hasExplicitLanguageAlternates: false };
1320
- const routeHeadSnapshot = this.#createRouteHeadSnapshot(url, routeHead, {
1321
- hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates,
1322
- });
1319
+ : { node: undefined };
1320
+ const routeHeadSnapshot = this.#createRouteHeadSnapshot(url, routeHead, {});
1323
1321
  return (
1324
1322
  <html lang={params.lang ?? RscRenderer.#getLocale(pathname, this.#i18n)} suppressHydrationWarning>
1325
1323
  <head key="head">
@@ -1329,10 +1327,7 @@ export class RscRenderer {
1329
1327
  {routeHeadSnapshot
1330
1328
  ? renderAkanHeadSnapshot(routeHeadSnapshot)
1331
1329
  : (routeHead.node ?? this.#renderDefaultHead())}
1332
- {!routeHeadSnapshot &&
1333
- shouldRenderLocaleAlternates({ hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates })
1334
- ? this.#renderLocaleAlternates(url)
1335
- : null}
1330
+ {routeHeadSnapshot ? null : this.#renderLocaleAlternates(url)}
1336
1331
  {this.#renderStylesheet(pathname)}
1337
1332
  </head>
1338
1333
  <body key="body">{body}</body>
@@ -1353,14 +1348,13 @@ export class RscRenderer {
1353
1348
  basePath: this.#getBasePath(url),
1354
1349
  });
1355
1350
  setRequestFrameState(pathRoute.pageState);
1356
- const routeHead = await RouteElementComposer.resolveHeadWithMetadata({
1351
+ const routeHead = await RouteElementComposer.resolveHeadWithSnapshot({
1357
1352
  pathRoute,
1358
1353
  params: match.params,
1359
1354
  searchParams,
1360
1355
  });
1361
1356
  const routeHeadSnapshot = this.#createRouteHeadSnapshot(url, routeHead, {
1362
1357
  isSpecialRoute: pathRoute.isSpecialRoute,
1363
- hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates,
1364
1358
  });
1365
1359
  const body = RouteElementComposer.compose({
1366
1360
  pathRoute,
@@ -1377,11 +1371,7 @@ export class RscRenderer {
1377
1371
  {routeHeadSnapshot
1378
1372
  ? renderAkanHeadSnapshot(routeHeadSnapshot)
1379
1373
  : (routeHead.node ?? this.#renderDefaultHead())}
1380
- {!routeHeadSnapshot &&
1381
- shouldRenderLocaleAlternates({
1382
- isSpecialRoute: pathRoute.isSpecialRoute,
1383
- hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates,
1384
- })
1374
+ {!routeHeadSnapshot && shouldRenderLocaleAlternates({ isSpecialRoute: pathRoute.isSpecialRoute })
1385
1375
  ? this.#renderLocaleAlternates(url)
1386
1376
  : null}
1387
1377
  {this.#renderStylesheet(url.pathname)}
@@ -1470,21 +1460,20 @@ export class RscRenderer {
1470
1460
  match: { pathRoute: PathRoute; params: Record<string, string> },
1471
1461
  searchParams: Record<string, string | string[]>,
1472
1462
  ): Promise<ResolvedHead["headSnapshot"]> {
1473
- const routeHead = await RouteElementComposer.resolveHeadWithMetadata({
1463
+ const routeHead = await RouteElementComposer.resolveHeadWithSnapshot({
1474
1464
  pathRoute: match.pathRoute,
1475
1465
  params: match.params,
1476
1466
  searchParams,
1477
1467
  });
1478
1468
  return this.#createRouteHeadSnapshot(url, routeHead, {
1479
1469
  isSpecialRoute: match.pathRoute.isSpecialRoute,
1480
- hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates,
1481
1470
  });
1482
1471
  }
1483
1472
 
1484
1473
  #createRouteHeadSnapshot(
1485
1474
  url: URL,
1486
1475
  routeHead: ResolvedHead,
1487
- options: { isSpecialRoute?: boolean; hasExplicitLanguageAlternates?: boolean },
1476
+ options: { isSpecialRoute?: boolean },
1488
1477
  ): ResolvedHead["headSnapshot"] {
1489
1478
  if (!routeHead.headSnapshot) return undefined;
1490
1479
  return mergeAkanHeadSnapshots(
@@ -1209,18 +1209,22 @@ export class WebRouter {
1209
1209
  if (details.length === 0) return null;
1210
1210
  return WebRouter.#jsonResponse({ applinks: { apps: [], details } }, cacheControl);
1211
1211
  }
1212
+
1213
+ const packageSuffixes = process.env.AKAN_PUBLIC_ENV === "main" ? [""] : ["", ".debug"];
1212
1214
  const assetLinks = associations
1213
1215
  .filter(
1214
1216
  (association) => association.domains.length > 0 && (association.androidSha256CertFingerprints?.length ?? 0) > 0,
1215
1217
  )
1216
- .map((association) => ({
1217
- relation: ["delegate_permission/common.handle_all_urls"],
1218
- target: {
1219
- namespace: "android_app",
1220
- package_name: association.appId,
1221
- sha256_cert_fingerprints: association.androidSha256CertFingerprints,
1222
- },
1223
- }));
1218
+ .flatMap((association) =>
1219
+ packageSuffixes.map((suffix) => ({
1220
+ relation: ["delegate_permission/common.handle_all_urls"],
1221
+ target: {
1222
+ namespace: "android_app",
1223
+ package_name: `${association.appId}${suffix}`,
1224
+ sha256_cert_fingerprints: association.androidSha256CertFingerprints,
1225
+ },
1226
+ })),
1227
+ );
1224
1228
  if (assetLinks.length === 0) return null;
1225
1229
  return WebRouter.#jsonResponse(assetLinks, cacheControl);
1226
1230
  }