akanjs 3.0.0 → 3.0.1

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.
@@ -116,6 +116,8 @@ export interface RouteRender {
116
116
  resolveHead?: ResolveHead;
117
117
  getPageConfig?: () => PromiseOrObject<PageConfig | undefined>;
118
118
  getLayoutPageConfig?: () => PromiseOrObject<PageConfig | undefined>;
119
+ /** A root layout's `theme`, readable before it renders. */
120
+ getLayoutTheme?: () => PromiseOrObject<string | undefined>;
119
121
  /** The `page()` chain behind a page render, when it was declared as one — what a page prompt is read off. */
120
122
  getRouteDefinition?: () => PromiseOrObject<RouteDefinition | undefined>;
121
123
  }
@@ -179,7 +181,6 @@ export interface Route {
179
181
  /** Synthetic layout render from a `_overrides.tsx` at this node; wraps the subtree in a UI-override provider. */
180
182
  renderOverrides?: RouteRender;
181
183
  pageIncludesOwnLayout?: boolean;
182
- isSpecialRoute?: boolean;
183
184
  loader?: () => unknown;
184
185
  pageState?: PageState;
185
186
  pageConfigChain?: PageConfig[];
@@ -355,7 +356,6 @@ export interface PathRoute {
355
356
  renderRootLayouts: RouteRender[];
356
357
  renderLayouts: RouteRender[];
357
358
  resolveHead?: ResolveHead;
358
- isSpecialRoute?: boolean;
359
359
  }
360
360
 
361
361
  export type FrameSlotScope = "page" | "layout";
package/common/index.ts CHANGED
@@ -139,7 +139,6 @@ export {
139
139
  getPageSourceFileViolation,
140
140
  getRouteExports,
141
141
  isRouteSourceFile,
142
- isSpecialRouteLeaf,
143
142
  LAYOUT_ROUTE_EXPORTS,
144
143
  matchRoutePattern,
145
144
  normalizeRoutePattern,
@@ -3,7 +3,6 @@ const SOURCE_EXT_RE = /\.(tsx|ts|jsx|js)$/;
3
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
- const SPECIAL_ROUTE_LEAVES = new Set(["robots.txt"]);
7
6
 
8
7
  const DIRECTORY_SCOPED_LEAVES = new Set(["_layout", "_index", "_overrides"]);
9
8
 
@@ -45,7 +44,6 @@ export interface ParsedRouteModuleKey {
45
44
  ext: string;
46
45
  leaf: string;
47
46
  pattern: string;
48
- isSpecialRoute: boolean;
49
47
  isInternalRootLayout: boolean;
50
48
  }
51
49
 
@@ -140,8 +138,7 @@ export function parseRouteModuleKey(key: string): ParsedRouteModuleKey {
140
138
 
141
139
  const sourceRouteSegments =
142
140
  DIRECTORY_SCOPED_LEAVES.has(leaf) || isInternalRootLayout ? moduleSegments.slice(0, -1) : moduleSegments;
143
- const isSpecialRoute = kind === "page" && SPECIAL_ROUTE_LEAVES.has(leaf);
144
- const routeSegments = isSpecialRoute ? sourceRouteSegments : [IMPLICIT_LOCALE_SEGMENT, ...sourceRouteSegments];
141
+ const routeSegments = [IMPLICIT_LOCALE_SEGMENT, ...sourceRouteSegments];
145
142
  for (const segment of routeSegments) validateRouteSegment(segment, key);
146
143
 
147
144
  return {
@@ -153,15 +150,10 @@ export function parseRouteModuleKey(key: string): ParsedRouteModuleKey {
153
150
  ext,
154
151
  leaf,
155
152
  pattern: normalizeRoutePattern(routeSegments),
156
- isSpecialRoute,
157
153
  isInternalRootLayout,
158
154
  };
159
155
  }
160
156
 
161
- export function isSpecialRouteLeaf(leaf: string): boolean {
162
- return SPECIAL_ROUTE_LEAVES.has(leaf);
163
- }
164
-
165
157
  export function tryParseRouteModuleKey(key: string): ParsedRouteModuleKey | null {
166
158
  try {
167
159
  return parseRouteModuleKey(key);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -7,6 +7,7 @@ export interface RouteCacheKeyInput {
7
7
  request: Request;
8
8
  url: URL;
9
9
  theme?: string;
10
+ variant?: string;
10
11
  }
11
12
 
12
13
  export interface RouteCacheRenderState {
@@ -116,7 +117,7 @@ export function isRouteCachePathAllowed(
116
117
  return matches(allow);
117
118
  }
118
119
 
119
- export function createRouteCacheKey({ request, url, theme = "" }: RouteCacheKeyInput): string {
120
+ export function createRouteCacheKey({ request, url, theme = "", variant }: RouteCacheKeyInput): string {
120
121
  return [
121
122
  getClientFacingOrigin(request, url),
122
123
  request.headers.get("x-base-path") ?? "",
@@ -126,6 +127,7 @@ export function createRouteCacheKey({ request, url, theme = "" }: RouteCacheKeyI
126
127
  url.search,
127
128
  request.headers.get("accept-language") ?? "",
128
129
  theme,
130
+ ...(variant ? [variant] : []),
129
131
  ].join("\n");
130
132
  }
131
133
 
@@ -153,7 +155,15 @@ export function resolvePublicRouteCacheEntryDecision(input: PublicRouteCacheEntr
153
155
  return { entry: null, reason: "path-excluded" };
154
156
  }
155
157
  if (!isPublicRouteCacheableRequest(input.request)) return { entry: null, reason: "request-not-public" };
156
- return { entry: createRouteCacheEntry({ request: input.request, url: input.url, theme: input.theme, ttl }) };
158
+ return {
159
+ entry: createRouteCacheEntry({
160
+ request: input.request,
161
+ url: input.url,
162
+ theme: input.theme,
163
+ variant: input.variant,
164
+ ttl,
165
+ }),
166
+ };
157
167
  }
158
168
 
159
169
  export function resolveRouteCacheStoreTtl(baseTtl: number, state: RouteCacheRenderState): number | null {
@@ -0,0 +1,6 @@
1
+
2
+ const crawlerPattern =
3
+ /(?<!cu)bot|crawl|spider|slurp|-user\b|preview|facebookexternalhit|facebookcatalog|meta-external|kakaotalk-scrap|whatsapp|vkshare|embedly|iframely|mastodon|anthropic-ai|cohere-ai|google-inspectiontool|googleother|mediapartners-google|yeti\b|daum\/|daumoa|ia_archiver/i;
4
+
5
+ export const isCrawlerUserAgent = (userAgent: string | null | undefined): boolean =>
6
+ !!userAgent && crawlerPattern.test(userAgent);
package/server/head.tsx CHANGED
@@ -39,10 +39,6 @@ export function renderAkanHeadSnapshot(snapshot: AkanHeadSnapshotV1): Head {
39
39
  );
40
40
  }
41
41
 
42
- export function shouldRenderLocaleAlternates(options: { isSpecialRoute?: boolean }): boolean {
43
- return options.isSpecialRoute !== true;
44
- }
45
-
46
42
  function isResolvedHead(value: unknown): value is ResolvedHead {
47
43
  return isRecord(value) && "node" in value;
48
44
  }
package/server/robots.ts CHANGED
@@ -2,33 +2,11 @@ import { getApiPrefix } from "akanjs/base";
2
2
 
3
3
  const DEFAULT_DISALLOW_PATHS = ["/_akan", "/admin", "/manager", "/private"] as const;
4
4
 
5
- const DEFAULT_AI_CRAWLERS = [
6
- "GPTBot",
7
- "ChatGPT-User",
8
- "CCBot",
9
- "ClaudeBot",
10
- "Claude-Web",
11
- "anthropic-ai",
12
- "Google-Extended",
13
- "PerplexityBot",
14
- "Perplexity-User",
15
- "Bytespider",
16
- "Amazonbot",
17
- "Applebot-Extended",
18
- "Meta-ExternalAgent",
19
- "FacebookBot",
20
- "Diffbot",
21
- "Omgilibot",
22
- "Omgili",
23
- ] as const;
24
-
25
5
  export function createDefaultRobotsTxt(): string {
26
6
  const lines = [
27
7
  "User-agent: *",
28
8
  "Allow: /",
29
9
  ...[getApiPrefix(), ...DEFAULT_DISALLOW_PATHS].map((path) => `Disallow: ${path}`),
30
- "",
31
- ...DEFAULT_AI_CRAWLERS.flatMap((crawler) => [`User-agent: ${crawler}`, "Disallow: /", ""]),
32
10
  ];
33
- return `${lines.join("\n").trimEnd()}\n`;
11
+ return `${lines.join("\n")}\n`;
34
12
  }
@@ -171,7 +171,6 @@ export class RouteTreeBuilder {
171
171
  : {
172
172
  renderPage: routeRender,
173
173
  pageIncludesOwnLayout: parsed.leaf === "_index",
174
- isSpecialRoute: parsed.isSpecialRoute,
175
174
  }),
176
175
  } as Route);
177
176
  }
@@ -198,7 +197,6 @@ export class RouteTreeBuilder {
198
197
  renderRootLayouts: layer.pageRenderRootLayouts,
199
198
  renderLayouts: layer.pageRenderLayouts,
200
199
  resolveHead: RouteTreeBuilder.#composeHeadResolvers(route.renderPage.resolveHead, pageHead),
201
- isSpecialRoute: route.isSpecialRoute,
202
200
  pageState: route.pageState ?? defaultPageState,
203
201
  },
204
202
  ]
@@ -301,6 +299,10 @@ export class RouteTreeBuilder {
301
299
  routeRender.getRouteDefinition = async () => (await loadModule()).definition;
302
300
  } else {
303
301
  routeRender.getLayoutPageConfig = pageConfigOf;
302
+ routeRender.getLayoutTheme = async () => {
303
+ const { module: mod } = await loadModule();
304
+ return "theme" in mod ? (mod.theme as string | undefined) : undefined;
305
+ };
304
306
  routeRender.resolveNotFound = async () => syncFallbacks((await loadModule()).module as LayoutModule).NotFound;
305
307
  routeRender.resolveError = async () => syncFallbacks((await loadModule()).module as LayoutModule).Error;
306
308
  }
@@ -17,6 +17,7 @@ import {
17
17
  pushRequestFallback,
18
18
  requestStorage,
19
19
  setRequestFrameState,
20
+ setRequestTheme,
20
21
  untrackedCookies,
21
22
  untrackedRequest,
22
23
  updateRequestPolicy,
@@ -36,12 +37,7 @@ import {
36
37
  resolveRouteCacheStoreTtl,
37
38
  shouldStoreRouteCache,
38
39
  } from "./cachePolicy";
39
- import {
40
- createAkanLocaleAlternateHeadSnapshot,
41
- mergeAkanHeadSnapshots,
42
- renderAkanHeadSnapshot,
43
- shouldRenderLocaleAlternates,
44
- } from "./head";
40
+ import { createAkanLocaleAlternateHeadSnapshot, mergeAkanHeadSnapshots, renderAkanHeadSnapshot } from "./head";
45
41
  import { LogForwarder } from "./logging/logForwarder";
46
42
  import { ProcessMetricsCollector } from "./processMetricsCollector";
47
43
  import { RouteElementComposer } from "./routeElementComposer";
@@ -1223,7 +1219,7 @@ export class RscRenderer {
1223
1219
  searchParams,
1224
1220
  })
1225
1221
  : { node: undefined };
1226
- const routeHeadSnapshot = this.#createRouteHeadSnapshot(url, routeHead, {});
1222
+ const routeHeadSnapshot = this.#createRouteHeadSnapshot(url, routeHead);
1227
1223
  return (
1228
1224
  <html lang={params.lang ?? getPathnameLocale(pathname, this.#i18n)} suppressHydrationWarning>
1229
1225
  <head key="head">
@@ -1254,14 +1250,16 @@ export class RscRenderer {
1254
1250
  basePath: this.#getBasePath(url),
1255
1251
  });
1256
1252
  setRequestFrameState(pathRoute.pageState);
1253
+
1254
+ const rootThemes = await Promise.all(pathRoute.renderRootLayouts.map((render) => render.getLayoutTheme?.()));
1255
+ const rootTheme = rootThemes.find((theme) => theme !== undefined);
1256
+ if (rootTheme) setRequestTheme(rootTheme);
1257
1257
  const routeHead = await RouteElementComposer.resolveHeadWithSnapshot({
1258
1258
  pathRoute,
1259
1259
  params: match.params,
1260
1260
  searchParams,
1261
1261
  });
1262
- const routeHeadSnapshot = this.#createRouteHeadSnapshot(url, routeHead, {
1263
- isSpecialRoute: pathRoute.isSpecialRoute,
1264
- });
1262
+ const routeHeadSnapshot = this.#createRouteHeadSnapshot(url, routeHead);
1265
1263
  const body = RouteElementComposer.compose({
1266
1264
  pathRoute,
1267
1265
  params: match.params,
@@ -1277,9 +1275,7 @@ export class RscRenderer {
1277
1275
  {routeHeadSnapshot
1278
1276
  ? renderAkanHeadSnapshot(routeHeadSnapshot)
1279
1277
  : (routeHead.node ?? this.#renderDefaultHead())}
1280
- {!routeHeadSnapshot && shouldRenderLocaleAlternates({ isSpecialRoute: pathRoute.isSpecialRoute })
1281
- ? this.#renderLocaleAlternates(url)
1282
- : null}
1278
+ {routeHeadSnapshot ? null : this.#renderLocaleAlternates(url)}
1283
1279
  {this.#renderStylesheet(url.pathname)}
1284
1280
  </head>
1285
1281
  <body key="body">{body}</body>
@@ -1370,22 +1366,14 @@ export class RscRenderer {
1370
1366
  params: match.params,
1371
1367
  searchParams,
1372
1368
  });
1373
- return this.#createRouteHeadSnapshot(url, routeHead, {
1374
- isSpecialRoute: match.pathRoute.isSpecialRoute,
1375
- });
1369
+ return this.#createRouteHeadSnapshot(url, routeHead);
1376
1370
  }
1377
1371
 
1378
- #createRouteHeadSnapshot(
1379
- url: URL,
1380
- routeHead: ResolvedHead,
1381
- options: { isSpecialRoute?: boolean },
1382
- ): ResolvedHead["headSnapshot"] {
1372
+ #createRouteHeadSnapshot(url: URL, routeHead: ResolvedHead): ResolvedHead["headSnapshot"] {
1383
1373
  if (!routeHead.headSnapshot) return undefined;
1384
1374
  return mergeAkanHeadSnapshots(
1385
1375
  routeHead.headSnapshot,
1386
- shouldRenderLocaleAlternates(options)
1387
- ? createAkanLocaleAlternateHeadSnapshot(this.#getLocaleAlternateLanguages(url))
1388
- : undefined,
1376
+ createAkanLocaleAlternateHeadSnapshot(this.#getLocaleAlternateLanguages(url)),
1389
1377
  );
1390
1378
  }
1391
1379
 
@@ -9,7 +9,7 @@ import { renderToReadableStream } from "react-dom/server.browser";
9
9
  import { createFromNodeStream } from "react-server-dom-webpack/client.node";
10
10
  import { parsePositiveInt } from "./cachePolicy";
11
11
  import { concatBytes } from "./rscHttp";
12
- import type { SsrChunkRegistryStats, SsrFromRscInput, SsrLateRedirect } from "./ssrTypes";
12
+ import type { SsrChunkRegistryStats, SsrDocumentOptions, SsrFromRscInput, SsrLateRedirect } from "./ssrTypes";
13
13
 
14
14
  const DEFAULT_SSR_CHUNK_REGISTRY_MAX_ENTRIES = 1024;
15
15
  const DEFAULT_MAX_PENDING_INLINE_RSC_SCRIPTS = 32;
@@ -593,21 +593,13 @@ export class SsrFromRscRenderer {
593
593
  const base = `${SsrFromRscRenderer.#clientBootstrap}${SsrFromRscRenderer.#prefixBootstrap()}`;
594
594
  const bootstrap = input.extraBootstrapInline ? `${base}\n${input.extraBootstrapInline}` : base;
595
595
 
596
- const waitForAllReady = input.waitForAllReady || process.env.AKAN_SSR_WAIT_FOR_ALL_READY === "1";
597
- const renderHtml = async () => {
598
- const root = await thenable;
599
- const stream = await renderToReadableStream(root, {
600
- bootstrapScriptContent: bootstrap,
601
- onError: (error) => SsrFromRscRenderer.#reportRenderError(error, input),
596
+ const renderHtml = async () =>
597
+ await SsrFromRscRenderer.renderDocument(await thenable, {
598
+ bootstrap,
599
+ waitForAllReady: input.waitForAllReady || process.env.AKAN_SSR_WAIT_FOR_ALL_READY === "1",
600
+ inlineBoundaries: input.inlineBoundaries,
601
+ onError: (error, phase) => SsrFromRscRenderer.#reportRenderError(error, input, phase),
602
602
  });
603
- if (waitForAllReady) await stream.allReady;
604
- else {
605
- SsrFromRscRenderer.holdPostShellErrors(stream, (error) =>
606
- SsrFromRscRenderer.#reportRenderError(error, input, "post-shell"),
607
- );
608
- }
609
- return stream;
610
- };
611
603
  const requestContext = input.requestStore ?? input.request;
612
604
  const htmlStream =
613
605
  requestContext && requestStorage ? await requestStorage.run(requestContext, renderHtml) : await renderHtml();
@@ -661,6 +653,18 @@ export class SsrFromRscRenderer {
661
653
  };
662
654
  }
663
655
 
656
+ static async renderDocument(root: ReactNode, options: SsrDocumentOptions): Promise<ReadableStream<Uint8Array>> {
657
+ const stream = await renderToReadableStream(root, {
658
+ bootstrapScriptContent: options.bootstrap,
659
+
660
+ ...(options.inlineBoundaries ? { progressiveChunkSize: Number.POSITIVE_INFINITY } : {}),
661
+ onError: (error) => options.onError(error),
662
+ });
663
+ if (options.waitForAllReady || options.inlineBoundaries) await stream.allReady;
664
+ else SsrFromRscRenderer.holdPostShellErrors(stream, (error) => options.onError(error, "post-shell"));
665
+ return stream;
666
+ }
667
+
664
668
  static holdPostShellErrors(stream: { allReady: Promise<void> }, report: (error: unknown) => void): void {
665
669
  void stream.allReady.catch(report);
666
670
  }
@@ -61,4 +61,13 @@ export interface SsrFromRscInput {
61
61
  onCancel?: (reason?: unknown) => void;
62
62
  /** Buffers until `stream.allReady` (`pageConfig.ssr: "block"`); `AKAN_SSR_WAIT_FOR_ALL_READY=1` forces it globally. */
63
63
  waitForAllReady?: boolean;
64
+ /** Buffers like `waitForAllReady` and writes every Suspense boundary in place, for a reader that runs no script. */
65
+ inlineBoundaries?: boolean;
66
+ }
67
+
68
+ export interface SsrDocumentOptions {
69
+ bootstrap: string;
70
+ waitForAllReady?: boolean;
71
+ inlineBoundaries?: boolean;
72
+ onError: (error: unknown, phase?: string) => void;
64
73
  }
@@ -36,6 +36,7 @@ import {
36
36
  shouldStoreRouteCache,
37
37
  } from "./cachePolicy";
38
38
  import { encodedFileResponse } from "./contentEncoding";
39
+ import { isCrawlerUserAgent } from "./crawler";
39
40
  import { HMR_CLIENT_SCRIPT } from "./hmr/clientScript";
40
41
  import { CSR_DEV_ROUTE_PREFIX, resolveDevCsrMode, SSR_DEV_DIRNAME, SSR_DEV_ROUTE_PREFIX } from "./hmr/csrDevManifest";
41
42
  import { CsrDevShell } from "./hmr/csrDevShell";
@@ -600,7 +601,8 @@ export class WebRouter {
600
601
  try {
601
602
  this.#requestStats.fullSsr += 1;
602
603
  const manifest = await this.#ensureRoute(url);
603
- const htmlCacheDecision = this.#getHtmlCacheEntry(req, url);
604
+ const isCrawler = isCrawlerUserAgent(req.headers.get("user-agent"));
605
+ const htmlCacheDecision = this.#getHtmlCacheEntry(req, url, isCrawler);
604
606
  const htmlCacheEntry = htmlCacheDecision.entry;
605
607
  const cachedHtml = htmlCacheEntry ? this.#getCachedHtml(htmlCacheEntry.key) : null;
606
608
  if (cachedHtml) {
@@ -643,6 +645,7 @@ export class WebRouter {
643
645
  },
644
646
  lateControl: rscResult.lateControl,
645
647
  waitForAllReady: rscResult.trace?.ssrBlocking ?? false,
648
+ inlineBoundaries: isCrawler,
646
649
  onCancel: (reason: unknown) => {
647
650
  rscResult.cancel(reason);
648
651
  },
@@ -814,7 +817,7 @@ export class WebRouter {
814
817
  }
815
818
  }
816
819
 
817
- #getHtmlCacheEntry(req: Request, url: URL): { entry: RouteCacheEntry | null; reason?: string } {
820
+ #getHtmlCacheEntry(req: Request, url: URL, isCrawler: boolean): { entry: RouteCacheEntry | null; reason?: string } {
818
821
 
819
822
  if (this.#ssrDevShell) {
820
823
  this.#htmlCacheBypass += 1;
@@ -824,6 +827,7 @@ export class WebRouter {
824
827
  request: req,
825
828
  url,
826
829
  theme: WebRouter.#cookieValue(req, "theme"),
830
+ variant: isCrawler ? "crawler" : undefined,
827
831
  defaultEnabled: this.#prodMode,
828
832
  defaultAllow: this.#prodMode,
829
833
  env: {
@@ -113,6 +113,8 @@ export interface RouteRender {
113
113
  resolveHead?: ResolveHead;
114
114
  getPageConfig?: () => PromiseOrObject<PageConfig | undefined>;
115
115
  getLayoutPageConfig?: () => PromiseOrObject<PageConfig | undefined>;
116
+ /** A root layout's `theme`, readable before it renders. */
117
+ getLayoutTheme?: () => PromiseOrObject<string | undefined>;
116
118
  /** The `page()` chain behind a page render, when it was declared as one — what a page prompt is read off. */
117
119
  getRouteDefinition?: () => PromiseOrObject<RouteDefinition | undefined>;
118
120
  }
@@ -176,7 +178,6 @@ export interface Route {
176
178
  /** Synthetic layout render from a `_overrides.tsx` at this node; wraps the subtree in a UI-override provider. */
177
179
  renderOverrides?: RouteRender;
178
180
  pageIncludesOwnLayout?: boolean;
179
- isSpecialRoute?: boolean;
180
181
  loader?: () => unknown;
181
182
  pageState?: PageState;
182
183
  pageConfigChain?: PageConfig[];
@@ -327,7 +328,6 @@ export interface PathRoute {
327
328
  renderRootLayouts: RouteRender[];
328
329
  renderLayouts: RouteRender[];
329
330
  resolveHead?: ResolveHead;
330
- isSpecialRoute?: boolean;
331
331
  }
332
332
  export type FrameSlotScope = "page" | "layout";
333
333
  export type FrameSlotType = "topInset" | "bottomInset";
@@ -34,7 +34,7 @@ export { randomPick, randomPicks } from "./randomPick.d.ts";
34
34
  export { hostFromRequest, isJsonContentType, originFromRequest } from "./requestOrigin.d.ts";
35
35
  export { RestClient, type RestClientOptions, type RestRequestOptions } from "./restClient.d.ts";
36
36
  export { round } from "./round.d.ts";
37
- export { assertUniqueRoutePatterns, compareRouteSpecificity, getPageSourceFileViolation, getRouteExports, isRouteSourceFile, isSpecialRouteLeaf, LAYOUT_ROUTE_EXPORTS, matchRoutePattern, normalizeRoutePattern, PAGE_ROUTE_EXPORTS, type ParsedRouteModuleKey, parseRouteModuleKey, RESERVED_ROUTE_CONFIG_EXPORTS, ROOT_LAYOUT_ROUTE_EXPORTS, type RouteModuleKind, routeSegmentToPatternPart, routeSegmentToTreePath, tryParseRouteModuleKey, type ValidatePageSourceFileOptions, type ValidateSubRoutePageKeyOptions, validatePageSourceFile, validateSubRoutePageKey, } from "./routeConvention.d.ts";
37
+ export { assertUniqueRoutePatterns, compareRouteSpecificity, getPageSourceFileViolation, getRouteExports, isRouteSourceFile, LAYOUT_ROUTE_EXPORTS, matchRoutePattern, normalizeRoutePattern, PAGE_ROUTE_EXPORTS, type ParsedRouteModuleKey, parseRouteModuleKey, RESERVED_ROUTE_CONFIG_EXPORTS, ROOT_LAYOUT_ROUTE_EXPORTS, type RouteModuleKind, routeSegmentToPatternPart, routeSegmentToTreePath, tryParseRouteModuleKey, type ValidatePageSourceFileOptions, type ValidateSubRoutePageKeyOptions, validatePageSourceFile, validateSubRoutePageKey, } from "./routeConvention.d.ts";
38
38
  export { sleep } from "./sleep.d.ts";
39
39
  export { getBasePathFromPathname, parseBasePaths, parseSubRouteHosts, resolveSubRouteHosts } from "./subRoute.d.ts";
40
40
  export { TrustedProxy } from "./TrustedProxy.d.ts";
@@ -16,7 +16,6 @@ export interface ParsedRouteModuleKey {
16
16
  ext: string;
17
17
  leaf: string;
18
18
  pattern: string;
19
- isSpecialRoute: boolean;
20
19
  isInternalRootLayout: boolean;
21
20
  }
22
21
  export interface ValidateSubRoutePageKeyOptions {
@@ -32,7 +31,6 @@ export declare function getPageSourceFileViolation(filePath: string): string | n
32
31
  export declare function validatePageSourceFile(filePath: string, options?: ValidatePageSourceFileOptions): boolean;
33
32
  export declare function validateSubRoutePageKey(key: string, basePaths: Iterable<string>, options?: ValidateSubRoutePageKeyOptions): void;
34
33
  export declare function parseRouteModuleKey(key: string): ParsedRouteModuleKey;
35
- export declare function isSpecialRouteLeaf(leaf: string): boolean;
36
34
  export declare function tryParseRouteModuleKey(key: string): ParsedRouteModuleKey | null;
37
35
  export declare function normalizeRoutePattern(segments: string[]): string;
38
36
  export declare function routeSegmentToPatternPart(segment: string): string;
@@ -4,6 +4,7 @@ export interface RouteCacheKeyInput {
4
4
  request: Request;
5
5
  url: URL;
6
6
  theme?: string;
7
+ variant?: string;
7
8
  }
8
9
  export interface RouteCacheRenderState {
9
10
  cacheable: boolean;
@@ -62,7 +63,7 @@ export declare function isRouteCachePathAllowed(pathname: string, options?: {
62
63
  deny?: string | null;
63
64
  defaultAllow?: boolean;
64
65
  }): boolean;
65
- export declare function createRouteCacheKey({ request, url, theme }: RouteCacheKeyInput): string;
66
+ export declare function createRouteCacheKey({ request, url, theme, variant }: RouteCacheKeyInput): string;
66
67
  export declare function createRouteCacheEntry(input: RouteCacheKeyInput & {
67
68
  ttl: number;
68
69
  }): RouteCacheEntry;
@@ -0,0 +1 @@
1
+ export declare const isCrawlerUserAgent: (userAgent: string | null | undefined) => boolean;
@@ -3,7 +3,4 @@ import { type AkanHeadSnapshotV1 } from "./routeState.d.ts";
3
3
  export declare function createAkanLocaleAlternateHeadSnapshot(languages: Record<string, string>): AkanHeadSnapshotV1;
4
4
  export declare function mergeAkanHeadSnapshots(...snapshots: Array<AkanHeadSnapshotV1 | null | undefined>): AkanHeadSnapshotV1 | undefined;
5
5
  export declare function renderAkanHeadSnapshot(snapshot: AkanHeadSnapshotV1): Head;
6
- export declare function shouldRenderLocaleAlternates(options: {
7
- isSpecialRoute?: boolean;
8
- }): boolean;
9
6
  export declare function resolveHeadResult(value: ResolveHeadResult): ResolvedHead;
@@ -1,5 +1,6 @@
1
1
  import { type AkanRequestStore } from "akanjs/fetch";
2
- import type { SsrChunkRegistryStats, SsrFromRscInput, SsrLateRedirect } from "./ssrTypes.d.ts";
2
+ import type { ReactNode } from "react";
3
+ import type { SsrChunkRegistryStats, SsrDocumentOptions, SsrFromRscInput, SsrLateRedirect } from "./ssrTypes.d.ts";
3
4
  export declare class SsrChunkRegistry<T> {
4
5
  #private;
5
6
  readonly maxEntries: number;
@@ -35,6 +36,7 @@ export declare class SsrFromRscRenderer {
35
36
  #private;
36
37
  static getChunkRegistryStats(): SsrChunkRegistryStats;
37
38
  render(input: SsrFromRscInput): Promise<ReadableStream<Uint8Array>>;
39
+ static renderDocument(root: ReactNode, options: SsrDocumentOptions): Promise<ReadableStream<Uint8Array>>;
38
40
  static holdPostShellErrors(stream: {
39
41
  allReady: Promise<void>;
40
42
  }, report: (error: unknown) => void): void;
@@ -57,4 +57,12 @@ export interface SsrFromRscInput {
57
57
  onCancel?: (reason?: unknown) => void;
58
58
  /** Buffers until `stream.allReady` (`pageConfig.ssr: "block"`); `AKAN_SSR_WAIT_FOR_ALL_READY=1` forces it globally. */
59
59
  waitForAllReady?: boolean;
60
+ /** Buffers like `waitForAllReady` and writes every Suspense boundary in place, for a reader that runs no script. */
61
+ inlineBoundaries?: boolean;
62
+ }
63
+ export interface SsrDocumentOptions {
64
+ bootstrap: string;
65
+ waitForAllReady?: boolean;
66
+ inlineBoundaries?: boolean;
67
+ onError: (error: unknown, phase?: string) => void;
60
68
  }
@@ -1,15 +1,15 @@
1
1
  /** 뱃지 look — 시맨틱 variant × size, outline 플래그는 색을 유지한 외곽선 스타일. `<Badge>` 가 소비하며, recipes.badge 슬롯으로 교체 가능. */
2
2
  export declare const badgeRecipe: (variants?: Omit<({
3
3
  size?: "lg" | "md" | "sm" | "xs" | undefined;
4
- outline?: boolean | undefined;
5
4
  variant?: "default" | "error" | "info" | "warning" | "success" | "primary" | "secondary" | "accent" | "neutral" | "outline" | undefined;
5
+ outline?: boolean | undefined;
6
6
  } & {
7
7
  class?: import("tailwind-variants").ClassValue;
8
8
  className?: never;
9
9
  }) | ({
10
10
  size?: "lg" | "md" | "sm" | "xs" | undefined;
11
- outline?: boolean | undefined;
12
11
  variant?: "default" | "error" | "info" | "warning" | "success" | "primary" | "secondary" | "accent" | "neutral" | "outline" | undefined;
12
+ outline?: boolean | undefined;
13
13
  } & {
14
14
  class?: never;
15
15
  className?: import("tailwind-variants").ClassValue;
@@ -1,16 +1,16 @@
1
1
  /** 버튼 look — 시맨틱 variant × size × shape, outline 플래그는 색을 유지한 외곽선 스타일. `<Button>` 이 소비하며, `_overrides.tsx` 의 recipes.button 슬롯으로 교체 가능. */
2
2
  export declare const buttonRecipe: (variants?: Omit<({
3
3
  size?: "lg" | "md" | "sm" | "xs" | "icon" | undefined;
4
- outline?: boolean | undefined;
5
4
  variant?: "default" | "link" | "info" | "warning" | "success" | "primary" | "secondary" | "accent" | "destructive" | "neutral" | "outline" | "ghost" | undefined;
5
+ outline?: boolean | undefined;
6
6
  shape?: "default" | "circle" | "square" | undefined;
7
7
  } & {
8
8
  class?: import("tailwind-variants").ClassValue;
9
9
  className?: never;
10
10
  }) | ({
11
11
  size?: "lg" | "md" | "sm" | "xs" | "icon" | undefined;
12
- outline?: boolean | undefined;
13
12
  variant?: "default" | "link" | "info" | "warning" | "success" | "primary" | "secondary" | "accent" | "destructive" | "neutral" | "outline" | "ghost" | undefined;
13
+ outline?: boolean | undefined;
14
14
  shape?: "default" | "circle" | "square" | undefined;
15
15
  } & {
16
16
  class?: never;
@@ -86,7 +86,6 @@ export declare function resolvePathRoutesWithFrameState(pathRoutes: PathRoute[],
86
86
  renderRootLayouts: import("akanjs/client").RouteRender[];
87
87
  renderLayouts: import("akanjs/client").RouteRender[];
88
88
  resolveHead?: import("akanjs/client").ResolveHead;
89
- isSpecialRoute?: boolean;
90
89
  }[];
91
90
  export declare function resolveLocationWithFrameState(location: Location | null, resolvedPathRouteMap: Map<string, PathRoute>): Location | null;
92
91
  export declare function useFrameRuntimeResync({ updateViewport }: {
@@ -71,7 +71,8 @@ function listFiles(dir: string): string[] {
71
71
  if (!existsSync(dir)) return [];
72
72
  return (readdirSync(dir, { recursive: true }) as string[])
73
73
  .map((file) => join(dir, file))
74
- .filter((file) => statSync(file).isFile());
74
+ .filter((file) => statSync(file).isFile())
75
+ .sort((a, b) => a.localeCompare(b));
75
76
  }
76
77
 
77
78
  /** The folder of the package a file belongs to: the deepest node_modules/<name> (or <@scope>/<name>) above it. */
@@ -62,14 +62,24 @@ export async function exec(cmd: string[], options: ExecOptions = {}): Promise<Ex
62
62
  const signal = signals.getStore();
63
63
 
64
64
  const stream = options.inherit && isTerminal() ? "inherit" : "pipe";
65
+
66
+ const group = !!signal && process.platform !== "win32";
65
67
  const proc = Bun.spawn(cmd, {
66
68
  cwd: options.cwd,
67
69
  env: options.env ? { ...process.env, ...options.env } : process.env,
68
70
  stdin: "ignore",
69
71
  stdout: stream,
70
72
  stderr: stream,
71
- ...(signal ? { signal } : {}),
73
+ ...(group ? { detached: true } : signal ? { signal } : {}),
72
74
  });
75
+ const killGroup = () => {
76
+ try {
77
+ process.kill(-proc.pid, "SIGTERM");
78
+ } catch {
79
+ }
80
+ };
81
+ if (group && signal) signal.addEventListener("abort", killGroup, { once: true });
82
+ void proc.exited.finally(() => signal?.removeEventListener("abort", killGroup));
73
83
  let stdout = "";
74
84
  let stderr = "";
75
85
  if (options.inherit && stream === "pipe") {