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.
- package/CHANGELOG.md +3659 -0
- package/client/csrTypes.ts +2 -2
- package/common/index.ts +0 -1
- package/common/routeConvention.ts +1 -9
- package/package.json +1 -1
- package/server/cachePolicy.ts +12 -2
- package/server/crawler.ts +6 -0
- package/server/head.tsx +0 -4
- package/server/robots.ts +1 -23
- package/server/routeTreeBuilder.ts +4 -2
- package/server/rscWorker.tsx +12 -24
- package/server/ssrFromRscRenderer.tsx +19 -15
- package/server/ssrTypes.ts +9 -0
- package/server/webRouter.ts +6 -2
- package/types/client/csrTypes.d.ts +2 -2
- package/types/common/index.d.ts +1 -1
- package/types/common/routeConvention.d.ts +0 -2
- package/types/server/cachePolicy.d.ts +2 -1
- package/types/server/crawler.d.ts +1 -0
- package/types/server/head.d.ts +0 -3
- package/types/server/ssrFromRscRenderer.d.ts +3 -1
- package/types/server/ssrTypes.d.ts +8 -0
- package/types/ui/recipe/badgeRecipe.d.ts +2 -2
- package/types/ui/recipe/buttonRecipe.d.ts +2 -2
- package/types/webkit/useFrameRuntime.d.ts +0 -1
- package/vendor/@akanjs/native/packages/cli/src/lib/addons.ts +2 -1
- package/vendor/@akanjs/native/packages/cli/src/lib/exec.ts +11 -1
- package/vendor/use-agentic/CHANGELOG.md +663 -0
- package/webkit/CsrRouteTable.ts +0 -2
- package/webkit/useCsrValues.ts +21 -11
package/client/csrTypes.ts
CHANGED
|
@@ -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
|
@@ -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
|
|
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
package/server/cachePolicy.ts
CHANGED
|
@@ -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 {
|
|
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")
|
|
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
|
}
|
package/server/rscWorker.tsx
CHANGED
|
@@ -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
|
-
{
|
|
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
|
-
|
|
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
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
}
|
package/server/ssrTypes.ts
CHANGED
|
@@ -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
|
}
|
package/server/webRouter.ts
CHANGED
|
@@ -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
|
|
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";
|
package/types/common/index.d.ts
CHANGED
|
@@ -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,
|
|
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;
|
package/types/server/head.d.ts
CHANGED
|
@@ -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 {
|
|
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") {
|