akanjs 3.0.0-beta.24 → 3.0.0-beta.26
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/common/hmrPhase.ts +2 -0
- package/package.json +1 -1
- package/server/SSR_MEMORY_DIAGNOSIS.md +2 -2
- package/server/akanApp.ts +1 -0
- package/server/akanServer.ts +7 -1
- package/server/artifact/builderRpc.ts +13 -2
- package/server/artifact/devBuildRecovery.ts +12 -0
- package/server/artifact/index.ts +1 -0
- package/server/artifact/ipcTypes.ts +46 -3
- package/server/artifact/routeClientCache.ts +128 -29
- package/server/di/diLifecycle.ts +1 -1
- package/server/hmr/clientScript.ts +148 -86
- package/server/hmr/csrDevManifest.ts +17 -0
- package/server/hmr/csrDevRuntime.ts +238 -18
- package/server/hmr/csrDevShell.ts +90 -10
- package/server/hmr/devArtifactPruner.ts +77 -0
- package/server/hmr/devHmrController.ts +233 -137
- package/server/hmr/ssrDevShim.ts +112 -0
- package/server/hmr/ssrUpdateQueue.ts +111 -0
- package/server/hmr/wsHub.ts +19 -4
- package/server/routing/apiRouter.ts +12 -0
- package/server/rscWorker.tsx +11 -1
- package/server/rscWorkerHost.ts +223 -59
- package/server/systemPageDocument.tsx +6 -0
- package/server/types.tsx +4 -0
- package/server/webRouter.ts +57 -22
- package/types/common/hmrPhase.d.ts +2 -0
- package/types/server/artifact/builderRpc.d.ts +3 -2
- package/types/server/artifact/devBuildRecovery.d.ts +4 -0
- package/types/server/artifact/index.d.ts +1 -0
- package/types/server/artifact/ipcTypes.d.ts +47 -2
- package/types/server/artifact/routeClientCache.d.ts +9 -2
- package/types/server/hmr/csrDevManifest.d.ts +15 -0
- package/types/server/hmr/csrDevRuntime.d.ts +34 -0
- package/types/server/hmr/csrDevShell.d.ts +9 -1
- package/types/server/hmr/devArtifactPruner.d.ts +9 -0
- package/types/server/hmr/devHmrController.d.ts +8 -5
- package/types/server/hmr/ssrDevShim.d.ts +7 -0
- package/types/server/hmr/ssrUpdateQueue.d.ts +23 -0
- package/types/server/hmr/wsHub.d.ts +16 -7
- package/types/server/routing/apiRouter.d.ts +8 -0
- package/types/server/rscWorkerHost.d.ts +14 -2
- package/types/server/systemPageDocument.d.ts +2 -0
- package/types/server/types.d.ts +4 -0
- package/types/server/webRouter.d.ts +8 -2
package/server/webRouter.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
RouteClientCache,
|
|
20
20
|
type RouteSeedIndex,
|
|
21
21
|
RouteSeedIndexStore,
|
|
22
|
+
type RoutesManifest,
|
|
22
23
|
RoutesManifestStore,
|
|
23
24
|
} from "./artifact";
|
|
24
25
|
import {
|
|
@@ -36,9 +37,10 @@ import {
|
|
|
36
37
|
} from "./cachePolicy";
|
|
37
38
|
import { encodedFileResponse } from "./contentEncoding";
|
|
38
39
|
import { HMR_CLIENT_SCRIPT } from "./hmr/clientScript";
|
|
39
|
-
import { CSR_DEV_ROUTE_PREFIX, resolveDevCsrMode } from "./hmr/csrDevManifest";
|
|
40
|
+
import { CSR_DEV_ROUTE_PREFIX, resolveDevCsrMode, SSR_DEV_DIRNAME, SSR_DEV_ROUTE_PREFIX } from "./hmr/csrDevManifest";
|
|
40
41
|
import { CsrDevShell } from "./hmr/csrDevShell";
|
|
41
42
|
import { DevHmrController } from "./hmr/devHmrController";
|
|
43
|
+
import { SsrDevShim } from "./hmr/ssrDevShim";
|
|
42
44
|
import type { HmrWsData, HmrWsHub } from "./hmr/wsHub";
|
|
43
45
|
import { ImageOptimizer } from "./imageOptimizer";
|
|
44
46
|
import { normalizeHost, resolveArtifactDir, warnIgnoredSubRouteBasePaths } from "./proxy/hostBasePathWebProxy";
|
|
@@ -250,6 +252,8 @@ interface WebRouterOptions {
|
|
|
250
252
|
rsc: RscWorker;
|
|
251
253
|
seedIndex: RouteSeedIndex;
|
|
252
254
|
upgradeHmrWs: (req: Request, data: HmrWsData) => boolean;
|
|
255
|
+
/** The production build's route manifest, which the worker was already handed at boot. */
|
|
256
|
+
prebuilt?: RoutesManifest | null;
|
|
253
257
|
}
|
|
254
258
|
|
|
255
259
|
interface CachedHtmlResult {
|
|
@@ -272,6 +276,8 @@ export class WebRouter {
|
|
|
272
276
|
#routeCache: RouteClientCache;
|
|
273
277
|
#devHmr: DevHmrController | null = null;
|
|
274
278
|
#csrDevShell: CsrDevShell | null = null;
|
|
279
|
+
/** Dev only: SSR pages load their client code from the dev module registry it serves. */
|
|
280
|
+
#ssrDevShell: CsrDevShell | null = null;
|
|
275
281
|
#csrArmed = false;
|
|
276
282
|
#csrOnDemandBuild: Promise<unknown> | null = null;
|
|
277
283
|
readonly #requestStats = {
|
|
@@ -297,7 +303,8 @@ export class WebRouter {
|
|
|
297
303
|
/** What this router actually mounts, already intersected with what the artifact carries. */
|
|
298
304
|
readonly web: AkanWebConfig;
|
|
299
305
|
#seedIndex: RouteSeedIndex;
|
|
300
|
-
|
|
306
|
+
readonly #prebuilt: RoutesManifest | null;
|
|
307
|
+
constructor({ artifact, web, cssBytesByUrl, rsc, seedIndex, upgradeHmrWs, prebuilt = null }: WebRouterOptions) {
|
|
301
308
|
this.#logger.verbose(`[SSR] loaded ${Object.keys(cssBytesByUrl).length} CSS assets`);
|
|
302
309
|
this.web = web;
|
|
303
310
|
if (process.env.NODE_ENV === "production" && !this.#prodMode)
|
|
@@ -317,6 +324,7 @@ export class WebRouter {
|
|
|
317
324
|
cssBytesByUrl,
|
|
318
325
|
};
|
|
319
326
|
this.#seedIndex = seedIndex;
|
|
327
|
+
this.#prebuilt = prebuilt;
|
|
320
328
|
if (this.#prodMode) {
|
|
321
329
|
this.#builderRpc = null;
|
|
322
330
|
this.#routeCache = new RouteClientCache({
|
|
@@ -333,25 +341,21 @@ export class WebRouter {
|
|
|
333
341
|
rsc: this.#rsc,
|
|
334
342
|
seedIndex: this.#seedIndex,
|
|
335
343
|
upgradeHmrWs,
|
|
344
|
+
pagesBundlePath: this.#artifact.pagesBundlePath,
|
|
336
345
|
});
|
|
337
346
|
this.#builderRpc = this.#devHmr.builderRpc;
|
|
338
347
|
this.#routeCache = this.#devHmr.routeCache;
|
|
339
348
|
this.#hub = this.#devHmr.hub;
|
|
340
349
|
if (resolveDevCsrMode() === "registry") this.#csrDevShell = new CsrDevShell(this.#artifactDir);
|
|
350
|
+
this.#ssrDevShell = new CsrDevShell(this.#artifactDir, {
|
|
351
|
+
dirName: SSR_DEV_DIRNAME,
|
|
352
|
+
routePrefix: SSR_DEV_ROUTE_PREFIX,
|
|
353
|
+
});
|
|
341
354
|
}
|
|
342
355
|
}
|
|
343
356
|
|
|
344
357
|
async initializeRoute() {
|
|
345
|
-
|
|
346
|
-
if (prebuilt) {
|
|
347
|
-
this.#routeCache.seed(prebuilt);
|
|
348
|
-
await this.#rsc.reload({
|
|
349
|
-
clientManifest: this.#mergeRuntimeManifest().clientManifest,
|
|
350
|
-
cssAssets: this.renderState.cssAssets,
|
|
351
|
-
|
|
352
|
-
buildId: this.#artifact.pagesBundleBuildId,
|
|
353
|
-
});
|
|
354
|
-
}
|
|
358
|
+
if (this.#prebuilt) this.#routeCache.seed(this.#prebuilt);
|
|
355
359
|
|
|
356
360
|
const clientServePrefix = `/_akan/client`;
|
|
357
361
|
const clientOutputDir = `${this.#artifactDir}/client`;
|
|
@@ -390,7 +394,7 @@ export class WebRouter {
|
|
|
390
394
|
<script type="module" src="/csr.js"></script>
|
|
391
395
|
</body>
|
|
392
396
|
</html>`;
|
|
393
|
-
return new Response(this.#withCsrHmr(htmlText), { headers:
|
|
397
|
+
return new Response(this.#withCsrHmr(htmlText), { headers: this.#htmlResponseHeaders(200) });
|
|
394
398
|
},
|
|
395
399
|
...(this.#csrDevShell
|
|
396
400
|
? {
|
|
@@ -402,6 +406,14 @@ export class WebRouter {
|
|
|
402
406
|
: {}),
|
|
403
407
|
}
|
|
404
408
|
: {}),
|
|
409
|
+
...(this.#ssrDevShell
|
|
410
|
+
? {
|
|
411
|
+
[`${SSR_DEV_ROUTE_PREFIX}*`]: async (req: Request) => {
|
|
412
|
+
this.#requestStats.staticAsset += 1;
|
|
413
|
+
return (await this.#ssrDevShell?.serve(req)) ?? new Response("Not Found", { status: 404 });
|
|
414
|
+
},
|
|
415
|
+
}
|
|
416
|
+
: {}),
|
|
405
417
|
[`${clientServePrefix}/*`]: async (req) => {
|
|
406
418
|
this.#requestStats.staticAsset += 1;
|
|
407
419
|
const url = new URL(req.url);
|
|
@@ -453,8 +465,6 @@ export class WebRouter {
|
|
|
453
465
|
? {
|
|
454
466
|
"/_akan/hmr": (req: Request) =>
|
|
455
467
|
this.#devHmr?.handleWs(req) ?? new Response("HMR unavailable", { status: 404 }),
|
|
456
|
-
"/_akan/hmr/client-refresh": (req: Request) =>
|
|
457
|
-
this.#devHmr?.handleClientRefresh(req) ?? new Response("HMR unavailable", { status: 404 }),
|
|
458
468
|
}
|
|
459
469
|
: {}),
|
|
460
470
|
"/__rsc": async (req) => {
|
|
@@ -535,7 +545,7 @@ export class WebRouter {
|
|
|
535
545
|
const csrHtml = await this.#resolveCsrHtml(csrOutputDir, url.pathname);
|
|
536
546
|
if (!csrHtml) return this.#csrUnavailableResponse(url.pathname);
|
|
537
547
|
const html = await Bun.file(csrHtml).text();
|
|
538
|
-
return new Response(this.#withCsrHmr(html), { headers:
|
|
548
|
+
return new Response(this.#withCsrHmr(html), { headers: this.#htmlResponseHeaders(200) });
|
|
539
549
|
}
|
|
540
550
|
|
|
541
551
|
const csrAssetPath = path.extname(url.pathname) ? resolveStaticPath(csrOutputDir, url.pathname) : null;
|
|
@@ -594,7 +604,7 @@ export class WebRouter {
|
|
|
594
604
|
const htmlCacheEntry = htmlCacheDecision.entry;
|
|
595
605
|
const cachedHtml = htmlCacheEntry ? this.#getCachedHtml(htmlCacheEntry.key) : null;
|
|
596
606
|
if (cachedHtml) {
|
|
597
|
-
const cachedHeaders =
|
|
607
|
+
const cachedHeaders = this.#htmlResponseHeaders(200);
|
|
598
608
|
cachedHeaders.set("X-Akan-Cache", "HIT");
|
|
599
609
|
return new Response(cachedHtml, { headers: cachedHeaders });
|
|
600
610
|
}
|
|
@@ -612,6 +622,9 @@ export class WebRouter {
|
|
|
612
622
|
rscResult.trace?.routeState
|
|
613
623
|
? `self.__AKAN_RSC_INITIAL_STATE__=${JSON.stringify(rscResult.trace.routeState)};`
|
|
614
624
|
: "",
|
|
625
|
+
this.#ssrDevShell
|
|
626
|
+
? SsrDevShim.script(await this.#ssrDevShell.readManifest(), Object.keys(this.#artifact.vendorMap))
|
|
627
|
+
: "",
|
|
615
628
|
!this.#prodMode ? HMR_CLIENT_SCRIPT : "",
|
|
616
629
|
]
|
|
617
630
|
.filter(Boolean)
|
|
@@ -632,7 +645,7 @@ export class WebRouter {
|
|
|
632
645
|
},
|
|
633
646
|
});
|
|
634
647
|
const responseStatus = rscResult.status ?? 200;
|
|
635
|
-
const responseHeaders =
|
|
648
|
+
const responseHeaders = this.#htmlResponseHeaders(responseStatus);
|
|
636
649
|
if (req.method === "HEAD") {
|
|
637
650
|
const headers = new Headers(responseHeaders);
|
|
638
651
|
if (htmlCacheEntry && responseStatus === 200) headers.set("X-Akan-Cache", "MISS");
|
|
@@ -704,6 +717,12 @@ export class WebRouter {
|
|
|
704
717
|
};
|
|
705
718
|
return { renderEnvRoutes, hmrHub: this.#hub, builderRpc: this.#builderRpc };
|
|
706
719
|
}
|
|
720
|
+
refreshHmrState(): void {
|
|
721
|
+
this.#devHmr?.refreshRegistryState();
|
|
722
|
+
}
|
|
723
|
+
hmrBuildErrors(): { phase: string }[] {
|
|
724
|
+
return this.#devHmr?.buildErrorMessages() ?? [];
|
|
725
|
+
}
|
|
707
726
|
dispose() {
|
|
708
727
|
this.#devHmr?.dispose();
|
|
709
728
|
this.#devHmr = null;
|
|
@@ -793,6 +812,11 @@ export class WebRouter {
|
|
|
793
812
|
}
|
|
794
813
|
|
|
795
814
|
#getHtmlCacheEntry(req: Request, url: URL): { entry: RouteCacheEntry | null; reason?: string } {
|
|
815
|
+
|
|
816
|
+
if (this.#ssrDevShell) {
|
|
817
|
+
this.#htmlCacheBypass += 1;
|
|
818
|
+
return { entry: null, reason: "dev-registry" };
|
|
819
|
+
}
|
|
796
820
|
const decision = resolvePublicRouteCacheEntryDecision({
|
|
797
821
|
request: req,
|
|
798
822
|
url,
|
|
@@ -893,6 +917,7 @@ export class WebRouter {
|
|
|
893
917
|
stylesheetHref: this.#getStylesheetHref(req, new URL(req.url).pathname),
|
|
894
918
|
showDetails: !this.#prodMode,
|
|
895
919
|
error: err,
|
|
920
|
+
...(this.#prodMode ? {} : { script: `self.__AKAN_HMR_SYSTEM_PAGE__=true;${HMR_CLIENT_SCRIPT}` }),
|
|
896
921
|
});
|
|
897
922
|
}
|
|
898
923
|
|
|
@@ -942,9 +967,9 @@ export class WebRouter {
|
|
|
942
967
|
return this.renderState.cssAssets[basePath ?? ""]?.cssUrl ?? null;
|
|
943
968
|
}
|
|
944
969
|
|
|
945
|
-
|
|
970
|
+
#htmlResponseHeaders(status: number): Headers {
|
|
946
971
|
const headers = new Headers({ "Content-Type": "text/html; charset=utf-8" });
|
|
947
|
-
if (status >= 400) headers.set("Cache-Control", "no-store");
|
|
972
|
+
if (status >= 400 || !this.#prodMode) headers.set("Cache-Control", "no-store");
|
|
948
973
|
return WebRouter.#applySecurityHeaders(headers, { html: true });
|
|
949
974
|
}
|
|
950
975
|
#withCsrHmr(html: string): string {
|
|
@@ -965,7 +990,7 @@ export class WebRouter {
|
|
|
965
990
|
cssHref: this.#getStylesheetHref(req, pathname),
|
|
966
991
|
});
|
|
967
992
|
if (!html) return this.#csrUnavailableResponse(pathname);
|
|
968
|
-
return new Response(this.#withCsrHmr(html), { headers:
|
|
993
|
+
return new Response(this.#withCsrHmr(html), { headers: this.#htmlResponseHeaders(200) });
|
|
969
994
|
}
|
|
970
995
|
async #resolveCsrHtml(csrOutputDir: string, pathname: string): Promise<string | null> {
|
|
971
996
|
const resolved = WebRouter.#resolveCsrHtmlPath(csrOutputDir, pathname, this.#artifact);
|
|
@@ -1015,7 +1040,16 @@ export class WebRouter {
|
|
|
1015
1040
|
const cssBytesByUrl = await WebRouter.#loadCssBytesByUrl(artifact, artifactDir);
|
|
1016
1041
|
const prodMode = process.env.NODE_ENV === "production" && process.env.AKAN_COMMAND_TYPE !== "start";
|
|
1017
1042
|
|
|
1018
|
-
const
|
|
1043
|
+
const prebuilt = prodMode ? await RoutesManifestStore.read(artifactDir) : null;
|
|
1044
|
+
const rsc = new RscWorker(
|
|
1045
|
+
prebuilt
|
|
1046
|
+
? {
|
|
1047
|
+
...artifact,
|
|
1048
|
+
rscRuntimeClientManifest: { ...artifact.rscRuntimeClientManifest, ...prebuilt.clientManifest },
|
|
1049
|
+
}
|
|
1050
|
+
: artifact,
|
|
1051
|
+
{ failBeforeReady: prodMode },
|
|
1052
|
+
);
|
|
1019
1053
|
if (prodMode)
|
|
1020
1054
|
void rsc.ready.catch((error: unknown) => {
|
|
1021
1055
|
new Logger("WebRouter").error(`RSC worker failed to load the pages bundle: ${String(error)}`);
|
|
@@ -1030,6 +1064,7 @@ export class WebRouter {
|
|
|
1030
1064
|
rsc,
|
|
1031
1065
|
seedIndex,
|
|
1032
1066
|
upgradeHmrWs,
|
|
1067
|
+
prebuilt,
|
|
1033
1068
|
});
|
|
1034
1069
|
}
|
|
1035
1070
|
|
|
@@ -2,5 +2,7 @@ export type AkanHmrPhase = "refresh-import" | "react-refresh" | null;
|
|
|
2
2
|
declare global {
|
|
3
3
|
var __AKAN_HMR_PHASE__: AkanHmrPhase | undefined;
|
|
4
4
|
}
|
|
5
|
+
/** @deprecated Always `null`: the dev client that set the phase applies modules through its registry now. */
|
|
5
6
|
export declare function getAkanHmrPhase(): AkanHmrPhase;
|
|
7
|
+
/** @deprecated Always `false`, for the reason `getAkanHmrPhase` is always `null`. */
|
|
6
8
|
export declare function isAkanHmrApplying(): boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BuildRouteClientResult, CsrUpdatedPayload, CssPayload, DevBuildStatus, PagesBundlePayload } from "./ipcTypes.d.ts";
|
|
1
|
+
import type { BuildRouteClientResult, CsrUpdatedPayload, CssPayload, DevBuildStatus, PagesBundlePayload, SsrUpdatedPayload } from "./ipcTypes.d.ts";
|
|
2
2
|
export interface BuilderRpcEventHandlers {
|
|
3
3
|
/** A watcher batch changed files: clear the route cache and broadcast a dev reload. */
|
|
4
4
|
onInvalidate?: (event: {
|
|
@@ -8,8 +8,9 @@ export interface BuilderRpcEventHandlers {
|
|
|
8
8
|
}) => void;
|
|
9
9
|
onCssUpdated?: (css: CssPayload) => void;
|
|
10
10
|
/** A fresh `pages-*.js` bundle: re-import it in the running worker (`RscWorker.reload`) rather than respawning. */
|
|
11
|
-
onPagesUpdated?: (bundle: PagesBundlePayload) => void
|
|
11
|
+
onPagesUpdated?: (bundle: PagesBundlePayload) => void | Promise<void>;
|
|
12
12
|
onCsrUpdated?: (update: CsrUpdatedPayload) => void;
|
|
13
|
+
onSsrUpdated?: (update: SsrUpdatedPayload) => void;
|
|
13
14
|
onBuildStatus?: (status: DevBuildStatus) => void;
|
|
14
15
|
}
|
|
15
16
|
/** One per backend process: subscribes to the IPC channel on construction and unsubscribes on `dispose()`. */
|
|
@@ -18,6 +18,8 @@ export interface BuildRouteResultPayload {
|
|
|
18
18
|
clientDepsByEntry?: Record<string, string[]>;
|
|
19
19
|
routeId?: string;
|
|
20
20
|
generation?: number;
|
|
21
|
+
/** The newest save batch whose files its client-entry discovery had taken in when it started. */
|
|
22
|
+
seenGeneration?: number;
|
|
21
23
|
}
|
|
22
24
|
/** Re-announces a recycled builder's artifact (not an edit); the host drops it when the hashed output did not move. */
|
|
23
25
|
export type BuilderStateReason = "builder-recycle";
|
|
@@ -41,13 +43,15 @@ export interface DevChangePlan {
|
|
|
41
43
|
actions: DevChangeAction[];
|
|
42
44
|
reasonByFile: Record<string, string[]>;
|
|
43
45
|
}
|
|
44
|
-
export type BuildPhase = "scan" | "barrel" | "csr" | "pages" | "css" | "route" | "backend";
|
|
46
|
+
export type BuildPhase = "scan" | "barrel" | "csr" | "ssr" | "pages" | "css" | "route" | "backend";
|
|
45
47
|
export interface DevBuildStatus {
|
|
46
48
|
generation: number;
|
|
47
49
|
phase: BuildPhase;
|
|
48
50
|
ok: boolean;
|
|
49
51
|
files: string[];
|
|
50
52
|
message?: string;
|
|
53
|
+
/** The route a route build's status speaks for. */
|
|
54
|
+
scope?: string;
|
|
51
55
|
}
|
|
52
56
|
export type BuilderReq = {
|
|
53
57
|
type: "build-route";
|
|
@@ -90,18 +94,33 @@ export type BuilderControl = {
|
|
|
90
94
|
type: "builder-shutdown";
|
|
91
95
|
reason: string;
|
|
92
96
|
};
|
|
97
|
+
/** Epoch-ms marks a save picks up on its way to the page, so a slow update says which hop it waited in. */
|
|
98
|
+
export interface HmrTrace {
|
|
99
|
+
eventAt?: number;
|
|
100
|
+
flushAt?: number;
|
|
101
|
+
batchAt?: number;
|
|
102
|
+
spawnAt?: number;
|
|
103
|
+
workerStartAt?: number;
|
|
104
|
+
workerAt?: number;
|
|
105
|
+
patchAt?: number;
|
|
106
|
+
sentAt?: number;
|
|
107
|
+
broadcastAt?: number;
|
|
108
|
+
}
|
|
93
109
|
export interface PagesBundlePayload {
|
|
94
110
|
bundlePath: string;
|
|
95
111
|
buildId: number;
|
|
96
112
|
generation?: number;
|
|
97
113
|
changedFiles?: string[];
|
|
98
114
|
reason?: BuilderStateReason;
|
|
115
|
+
trace?: HmrTrace;
|
|
116
|
+
/** False when no changed file is one the server renders (a `"use client"` edit that kept its export names). */
|
|
117
|
+
serverTouched?: boolean;
|
|
99
118
|
}
|
|
100
119
|
/** `Bun.build` keeps native arenas `Bun.gc(true)` never frees (macOS returns none when idle): hence recycling. */
|
|
101
120
|
export interface BuilderMetrics {
|
|
102
121
|
/** A peak sampled when the queues drain; stale within seconds on Linux, so the host re-reads RSS from the OS. */
|
|
103
122
|
rssBytes: number;
|
|
104
|
-
/** The builder's newest generation;
|
|
123
|
+
/** The builder's newest generation; a replacement builder continues from the last one its host saw. */
|
|
105
124
|
generation: number;
|
|
106
125
|
/** Work items completed since this builder spawned, so a host can require real work before recycling. */
|
|
107
126
|
workCount: number;
|
|
@@ -113,6 +132,23 @@ export interface CsrUpdatedPayload {
|
|
|
113
132
|
reason?: string;
|
|
114
133
|
patchUrl?: string;
|
|
115
134
|
changedIds?: string[];
|
|
135
|
+
trace?: HmrTrace;
|
|
136
|
+
}
|
|
137
|
+
/** A new generation of the SSR dev registry, where SSR pages load their client code from: a patch, or a reload. */
|
|
138
|
+
export interface SsrUpdatedPayload {
|
|
139
|
+
generation: number;
|
|
140
|
+
reload: boolean;
|
|
141
|
+
reason?: string;
|
|
142
|
+
patchUrl?: string;
|
|
143
|
+
changedIds?: string[];
|
|
144
|
+
trace?: HmrTrace;
|
|
145
|
+
/** The save also changed what the server renders: the tabs get this patch with that batch's RSC refresh. */
|
|
146
|
+
hold?: boolean;
|
|
147
|
+
/** The watch batch this patch came from, which the `pages-updated` releasing it names as its generation. */
|
|
148
|
+
batchGeneration?: number;
|
|
149
|
+
epoch?: number;
|
|
150
|
+
/** The registry's first build: no tab holds a module of it, so there is nothing to send them. */
|
|
151
|
+
first?: boolean;
|
|
116
152
|
}
|
|
117
153
|
export type BuilderEvent = {
|
|
118
154
|
type: "builder-ready";
|
|
@@ -135,12 +171,19 @@ export type BuilderEvent = {
|
|
|
135
171
|
} | {
|
|
136
172
|
type: "csr-updated";
|
|
137
173
|
data: CsrUpdatedPayload;
|
|
174
|
+
} | {
|
|
175
|
+
type: "ssr-updated";
|
|
176
|
+
data: SsrUpdatedPayload;
|
|
138
177
|
} | {
|
|
139
178
|
type: "build-status";
|
|
140
179
|
data: DevBuildStatus;
|
|
141
180
|
} | {
|
|
142
181
|
type: "builder-metrics";
|
|
143
182
|
data: BuilderMetrics;
|
|
183
|
+
}
|
|
184
|
+
/** The boot builds settled (the SSR registry's, and CSR's when the env arms it): their workers are a boot's peak. */
|
|
185
|
+
| {
|
|
186
|
+
type: "boot-armed";
|
|
144
187
|
};
|
|
145
188
|
export type BuilderMessage = BuilderReq | BuilderRes | BuilderCsrReq | BuilderCsrRes | BuilderControl | BuilderEvent;
|
|
146
189
|
export interface ClientManifestEntry {
|
|
@@ -157,4 +200,6 @@ export interface BuildRouteClientResult {
|
|
|
157
200
|
discoveredEntries?: string[];
|
|
158
201
|
clientDeps: string[];
|
|
159
202
|
clientDepsByEntry?: Record<string, string[]>;
|
|
203
|
+
/** The newest save batch whose files its client-entry discovery had taken in when it started. */
|
|
204
|
+
seenGeneration?: number;
|
|
160
205
|
}
|
|
@@ -26,6 +26,10 @@ interface RouteClientCacheOptions {
|
|
|
26
26
|
export interface InvalidateClientEntriesOptions {
|
|
27
27
|
routePredicate: (routeId: string) => boolean;
|
|
28
28
|
staleEntries: Iterable<string>;
|
|
29
|
+
/** The saved files behind it: a build still running that bundled one of them is built again. */
|
|
30
|
+
files?: Iterable<string>;
|
|
31
|
+
/** The save batch behind it: a route build whose discovery had taken that batch in is neither dropped nor rebuilt. */
|
|
32
|
+
batch?: number;
|
|
29
33
|
}
|
|
30
34
|
export declare class RouteClientCache {
|
|
31
35
|
#private;
|
|
@@ -36,8 +40,11 @@ export declare class RouteClientCache {
|
|
|
36
40
|
seed(manifest: RoutesManifest): void;
|
|
37
41
|
ensure(routeId: string, seeds: string[]): Promise<MergedManifest>;
|
|
38
42
|
snapshot(): MergedManifest;
|
|
39
|
-
invalidate(predicate: (routeId: string) => boolean
|
|
40
|
-
|
|
43
|
+
invalidate(predicate: (routeId: string) => boolean, { files, batch }?: {
|
|
44
|
+
files?: Iterable<string>;
|
|
45
|
+
batch?: number;
|
|
46
|
+
}): string[];
|
|
47
|
+
invalidateClientEntries({ routePredicate, staleEntries, files, batch, }: InvalidateClientEntriesOptions): string[];
|
|
41
48
|
clear(): string[];
|
|
42
49
|
}
|
|
43
50
|
export {};
|
|
@@ -2,13 +2,28 @@ export declare const CSR_DEV_DIRNAME = "csr-dev";
|
|
|
2
2
|
export declare const CSR_DEV_ROUTE_PREFIX = "/_akan/csr-dev/";
|
|
3
3
|
export declare const CSR_DEV_APP_FILE = "app.js";
|
|
4
4
|
export declare const CSR_DEV_MANIFEST_FILE = "manifest.json";
|
|
5
|
+
/** Present only while the resident builder patches: a builder that dies holding it takes the patcher off. */
|
|
6
|
+
export declare const CSR_DEV_PATCHING_MARKER = ".patching";
|
|
7
|
+
/** Patches kept on disk behind the newest, and as many as a reconnecting tab catches up on before it reloads instead. */
|
|
8
|
+
export declare const CSR_DEV_KEPT_PATCHES = 40;
|
|
9
|
+
export declare const SSR_DEV_DIRNAME = "ssr-dev";
|
|
10
|
+
export declare const SSR_DEV_ROUTE_PREFIX = "/_akan/ssr-dev/";
|
|
11
|
+
/** The one chunk every SSR registry row names: loading it boots the registry in the tab. */
|
|
12
|
+
export declare const SSR_DEV_CHUNK = "ssr-dev";
|
|
13
|
+
/** Marks a row id as a registry module, so the dev shim tells it from the runtime rows that stay chunk URLs. */
|
|
14
|
+
export declare const SSR_DEV_ID_PREFIX = "ssr-dev:";
|
|
5
15
|
export type DevCsrMode = "registry" | "artifact";
|
|
6
16
|
export interface CsrDevManifest {
|
|
7
17
|
version: 1;
|
|
8
18
|
generation: number;
|
|
19
|
+
/** The generation `app.js` holds; a patch is announced before `app.js` catches up, so it may trail `generation`. */
|
|
20
|
+
appGeneration?: number;
|
|
9
21
|
vendorFile: string;
|
|
10
22
|
entries: Record<string, string>;
|
|
23
|
+
/** When the registry was last built whole: a tab from an earlier registry must reload, whatever its generation. */
|
|
24
|
+
epoch?: number;
|
|
11
25
|
}
|
|
26
|
+
export declare const appGenerationOf: (manifest: CsrDevManifest) => number;
|
|
12
27
|
/** Where each module's generated code starts in a combined file (`app.js`, a patch), for its source map. */
|
|
13
28
|
export interface CsrDevLayout {
|
|
14
29
|
lineCount: number;
|
|
@@ -26,17 +26,43 @@ export interface CsrDevRuntimeApi {
|
|
|
26
26
|
generation: number;
|
|
27
27
|
refresh: string;
|
|
28
28
|
}): void;
|
|
29
|
+
/** No entry to run: an SSR page requires the modules its RSC payload names, after the bootstrap has run. */
|
|
30
|
+
startLibrary(options: LibraryStart): void;
|
|
31
|
+
/** Registers a module the page already loaded outside the registry (an import map vendor). */
|
|
32
|
+
provide(id: string, namespace: unknown): void;
|
|
33
|
+
require(id: string): unknown;
|
|
34
|
+
has(id: string): boolean;
|
|
35
|
+
/** Settles once a patch defines `id`: the RSC payload can name a module whose patch is still on its way. */
|
|
36
|
+
whenDefined(id: string): Promise<void>;
|
|
37
|
+
/** Settles once every update handed to `hot` so far is applied, its accept callbacks included. */
|
|
38
|
+
whenSettled(): Promise<void>;
|
|
29
39
|
update(generation: number, factories: Record<string, CsrModuleFactory>): void;
|
|
30
40
|
accept(ownerId: string, deps: string[], callback: CsrAcceptCallback): void;
|
|
31
41
|
hot(message: CsrUpdateMessage): void;
|
|
42
|
+
/** Loads every patch after the newest generation handed to `hot` up to `generation`, from `<prefix>patch-<n>.js`. */
|
|
43
|
+
catchUp(generation: number, prefix: string): void;
|
|
32
44
|
toESM(mod: unknown, isNodeMode?: number): unknown;
|
|
33
45
|
reExport(target: object, mod: unknown, secondTarget?: object): object | undefined;
|
|
46
|
+
/** `target` is the newest generation handed to `hot`, applied or still loading. */
|
|
34
47
|
inspect(): {
|
|
35
48
|
generation: number;
|
|
49
|
+
target: number;
|
|
50
|
+
started: boolean;
|
|
51
|
+
failed: boolean;
|
|
36
52
|
executed: string[];
|
|
37
53
|
modules: number;
|
|
54
|
+
/** What the library app.js was built with; undefined for a CSR page. */
|
|
55
|
+
vendorFile?: string;
|
|
56
|
+
epoch?: number;
|
|
38
57
|
};
|
|
39
58
|
}
|
|
59
|
+
export interface LibraryStart {
|
|
60
|
+
generation: number;
|
|
61
|
+
refresh: string;
|
|
62
|
+
bootstrap: string;
|
|
63
|
+
vendorFile?: string;
|
|
64
|
+
epoch?: number;
|
|
65
|
+
}
|
|
40
66
|
interface CsrScriptElement {
|
|
41
67
|
src: string;
|
|
42
68
|
dataset: Record<string, string | undefined>;
|
|
@@ -49,6 +75,8 @@ export interface CsrDevRuntimeHost {
|
|
|
49
75
|
__AKAN_CSR_LAST_UPDATE__?: {
|
|
50
76
|
generation: number;
|
|
51
77
|
executed: string[];
|
|
78
|
+
appliedAt?: number;
|
|
79
|
+
refreshedAt?: number;
|
|
52
80
|
};
|
|
53
81
|
document: {
|
|
54
82
|
currentScript: unknown;
|
|
@@ -63,6 +91,12 @@ export interface CsrDevRuntimeHost {
|
|
|
63
91
|
console: {
|
|
64
92
|
warn(...args: unknown[]): void;
|
|
65
93
|
};
|
|
94
|
+
fetch?(url: string, init: {
|
|
95
|
+
method: string;
|
|
96
|
+
cache: "no-store";
|
|
97
|
+
}): Promise<{
|
|
98
|
+
ok: boolean;
|
|
99
|
+
}>;
|
|
66
100
|
setTimeout(callback: () => void, ms: number): unknown;
|
|
67
101
|
clearTimeout(handle: unknown): void;
|
|
68
102
|
}
|
|
@@ -5,9 +5,17 @@ export interface CsrDevShellRenderOptions {
|
|
|
5
5
|
title: string;
|
|
6
6
|
cssHref: string | null;
|
|
7
7
|
}
|
|
8
|
+
export interface CsrDevShellOptions {
|
|
9
|
+
/** The registry this serves: the CSR one by default, `ssr-dev` for the client code of SSR pages. */
|
|
10
|
+
dirName?: string;
|
|
11
|
+
routePrefix?: string;
|
|
12
|
+
appWaitMs?: number;
|
|
13
|
+
appPollMs?: number;
|
|
14
|
+
bootWaitMs?: number;
|
|
15
|
+
}
|
|
8
16
|
export declare class CsrDevShell {
|
|
9
17
|
#private;
|
|
10
|
-
constructor(artifactDir: string);
|
|
18
|
+
constructor(artifactDir: string, { dirName, routePrefix, appWaitMs, appPollMs, bootWaitMs, }?: CsrDevShellOptions);
|
|
11
19
|
readManifest(): Promise<CsrDevManifest | null>;
|
|
12
20
|
render({ basePath, lang, title, cssHref }: CsrDevShellRenderOptions): Promise<string | null>;
|
|
13
21
|
serve(req: Request): Promise<Response>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CssAsset } from "../types.d.ts";
|
|
2
|
+
export declare class DevArtifactPruner {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(artifactDir: string, { graceMs }?: {
|
|
5
|
+
graceMs?: number;
|
|
6
|
+
});
|
|
7
|
+
prunePages(runningPath: string): Promise<void>;
|
|
8
|
+
pruneStyles(cssAssets: Record<string, CssAsset>): Promise<void>;
|
|
9
|
+
}
|
|
@@ -14,17 +14,20 @@ export interface DevHmrControllerOptions {
|
|
|
14
14
|
rsc: RscWorker;
|
|
15
15
|
seedIndex: RouteSeedIndex;
|
|
16
16
|
upgradeHmrWs: (req: Request, data: HmrWsData) => boolean;
|
|
17
|
+
/** The pages bundle the RSC worker booted with: the base build's, however many pages builds came after it. */
|
|
18
|
+
pagesBundlePath?: string;
|
|
17
19
|
}
|
|
18
20
|
export declare class DevHmrController {
|
|
19
21
|
#private;
|
|
20
22
|
readonly routeCache: RouteClientCache;
|
|
21
|
-
constructor({ artifactDir, renderState, rsc, seedIndex, upgradeHmrWs }: DevHmrControllerOptions);
|
|
23
|
+
constructor({ artifactDir, renderState, rsc, seedIndex, upgradeHmrWs, pagesBundlePath }: DevHmrControllerOptions);
|
|
24
|
+
refreshRegistryState(): void;
|
|
25
|
+
/** The phases failing now, for a socket that connects after their status went out (a page opened after a failed boot). */
|
|
26
|
+
buildErrorMessages(): Extract<HmrMessage, {
|
|
27
|
+
type: "build-status";
|
|
28
|
+
}>[];
|
|
22
29
|
get hub(): HmrWsHub;
|
|
23
30
|
get builderRpc(): BuilderRpc;
|
|
24
31
|
dispose(): void;
|
|
25
32
|
handleWs(req: Request): Response | undefined;
|
|
26
|
-
handleClientRefresh(req: Request): Promise<Response>;
|
|
27
|
-
ensureRoute(url: URL): Promise<import("..").MergedManifest>;
|
|
28
|
-
routeIdsForPath(pathname: string): string[] | undefined;
|
|
29
|
-
static clientChunkUrls(clientManifest: ClientManifest): string[];
|
|
30
33
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type CsrDevManifest } from "./csrDevManifest.d.ts";
|
|
2
|
+
export declare class SsrDevShim {
|
|
3
|
+
#private;
|
|
4
|
+
static readonly runtimeUrl: string;
|
|
5
|
+
/** `vendors` are the import map's specifiers: the registry requires them as `vendor:<specifier>`, never compiles them. */
|
|
6
|
+
static script(manifest: CsrDevManifest | null, vendors: string[]): string;
|
|
7
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { SsrUpdatedPayload } from "../artifact.d.ts";
|
|
2
|
+
import type { HmrMessage } from "./wsHub.d.ts";
|
|
3
|
+
export type SsrUpdateMessage = Extract<HmrMessage, {
|
|
4
|
+
type: "ssr-update";
|
|
5
|
+
}>;
|
|
6
|
+
export declare class SsrUpdateQueue {
|
|
7
|
+
#private;
|
|
8
|
+
constructor(send: (message: SsrUpdateMessage) => void, { maxHoldMs }?: {
|
|
9
|
+
maxHoldMs?: number;
|
|
10
|
+
});
|
|
11
|
+
get size(): number;
|
|
12
|
+
push(update: SsrUpdatedPayload): void;
|
|
13
|
+
/**
|
|
14
|
+
* The pages batch of `generation` finished or failed: sends what it covers and everything queued behind that, and
|
|
15
|
+
* says whether a reload went out (the tabs then need no RSC refresh).
|
|
16
|
+
*/
|
|
17
|
+
release(generation: number | undefined): {
|
|
18
|
+
released: number;
|
|
19
|
+
reload: boolean;
|
|
20
|
+
};
|
|
21
|
+
/** Drops what the pages batch of `generation` covers, or everything without one: the tabs are about to reload. */
|
|
22
|
+
clear(generation?: number): void;
|
|
23
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { BuildPhase } from "../artifact.d.ts";
|
|
1
|
+
import type { BuildPhase, HmrTrace } from "../artifact.d.ts";
|
|
2
2
|
export type ChangeKind = "code" | "css" | "config" | "ignore";
|
|
3
3
|
export interface ChangeBatch {
|
|
4
4
|
files: string[];
|
|
5
5
|
kinds: Set<Exclude<ChangeKind, "ignore">>;
|
|
6
|
+
trace?: HmrTrace;
|
|
6
7
|
}
|
|
7
8
|
export type HmrClientKind = "ssr" | "csr";
|
|
8
9
|
export interface HmrWsData {
|
|
@@ -18,6 +19,10 @@ export type HmrMessage = {
|
|
|
18
19
|
cssRelPath: string;
|
|
19
20
|
}>;
|
|
20
21
|
csrGeneration?: number;
|
|
22
|
+
ssrGeneration?: number;
|
|
23
|
+
ssrEpoch?: number;
|
|
24
|
+
/** The phases failing now, each sent right after: a tab drops an error it holds for any other. */
|
|
25
|
+
failingPhases?: string[];
|
|
21
26
|
} | {
|
|
22
27
|
type: "reload";
|
|
23
28
|
buildId: number;
|
|
@@ -28,18 +33,22 @@ export type HmrMessage = {
|
|
|
28
33
|
changedIds?: string[];
|
|
29
34
|
reload?: boolean;
|
|
30
35
|
reason?: string;
|
|
36
|
+
trace?: HmrTrace;
|
|
31
37
|
} | {
|
|
32
|
-
type: "
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
type: "ssr-update";
|
|
39
|
+
generation: number;
|
|
40
|
+
url?: string;
|
|
41
|
+
changedIds?: string[];
|
|
42
|
+
reload?: boolean;
|
|
43
|
+
reason?: string;
|
|
44
|
+
trace?: HmrTrace;
|
|
37
45
|
} | {
|
|
38
|
-
type: "
|
|
46
|
+
type: "rsc-refresh";
|
|
39
47
|
buildId: number;
|
|
40
48
|
generation?: number;
|
|
41
49
|
changedFiles?: string[];
|
|
42
50
|
routeIds?: string[];
|
|
51
|
+
trace?: HmrTrace;
|
|
43
52
|
} | {
|
|
44
53
|
type: "css-update";
|
|
45
54
|
cssAssets?: Record<string, {
|
|
@@ -12,7 +12,15 @@ export interface HmrStateSource {
|
|
|
12
12
|
cssRelPath: string;
|
|
13
13
|
}>;
|
|
14
14
|
csrGeneration?: number;
|
|
15
|
+
ssrGeneration?: number;
|
|
16
|
+
ssrEpoch?: number;
|
|
15
17
|
};
|
|
18
|
+
/** Called before each hello: brings what the state says about the dev registries up to what is on disk. */
|
|
19
|
+
refresh?: () => void;
|
|
20
|
+
/** Sent right after hello: the build statuses failing now, which the socket connected too late to hear. */
|
|
21
|
+
errors?: () => {
|
|
22
|
+
phase: string;
|
|
23
|
+
}[];
|
|
16
24
|
}
|
|
17
25
|
export type NonNullHttpRoutes = NonNullable<HttpRoutes>;
|
|
18
26
|
export interface ApiRouteInputs {
|