akanjs 3.0.0-beta.25 → 3.0.0-beta.27
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/client/csrTypes.ts +2 -0
- package/client/device.ts +9 -9
- package/common/Logger.ts +26 -2
- package/common/RouteLayering.ts +52 -0
- package/common/hmrPhase.ts +2 -0
- package/common/index.ts +1 -0
- package/package.json +1 -1
- package/server/artifact/devBuildRecovery.ts +12 -0
- package/server/artifact/index.ts +1 -0
- package/server/artifact/ipcTypes.ts +2 -0
- package/server/di/diLifecycle.ts +1 -1
- package/server/hmr/clientScript.ts +18 -5
- package/server/hmr/csrDevRuntime.ts +11 -7
- package/server/hmr/csrDevShell.ts +26 -5
- package/server/hmr/devArtifactPruner.ts +77 -0
- package/server/hmr/devHmrController.ts +69 -44
- package/server/hmr/ssrDevShim.ts +7 -1
- package/server/routeTreeBuilder.ts +14 -39
- package/server/rscWorker.tsx +7 -1
- package/server/rscWorkerHost.ts +126 -40
- package/server/webRouter.ts +25 -19
- package/types/client/csrTypes.d.ts +2 -0
- package/types/common/RouteLayering.d.ts +28 -0
- package/types/common/hmrPhase.d.ts +2 -0
- package/types/common/index.d.ts +1 -0
- 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 +2 -0
- package/types/server/hmr/devArtifactPruner.d.ts +9 -0
- package/types/server/hmr/devHmrController.d.ts +0 -2
- package/types/server/rscWorkerHost.d.ts +5 -4
- package/types/server/webRouter.d.ts +4 -2
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeActivity.kt +3 -1
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeBridge.kt +19 -0
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeShell.swift +12 -2
- package/vendor/@akanjs/native/packages/core/src/runtime.ts +26 -5
- package/vendor/@akanjs/native/packages/desktop/src/host.ts +5 -3
- package/webkit/CsrRouteTable.ts +15 -35
- package/webkit/bootCsr.tsx +11 -8
package/client/csrTypes.ts
CHANGED
|
@@ -174,6 +174,8 @@ export interface Route {
|
|
|
174
174
|
path: string;
|
|
175
175
|
renderPage?: RouteRender;
|
|
176
176
|
renderLayout?: RouteRender;
|
|
177
|
+
/** `renderLayout` is a generated `__root_layout`: a root boundary the page generator found. */
|
|
178
|
+
isRootLayout?: boolean;
|
|
177
179
|
/** Synthetic layout render from a `_overrides.tsx` at this node; wraps the subtree in a UI-override provider. */
|
|
178
180
|
renderOverrides?: RouteRender;
|
|
179
181
|
pageIncludesOwnLayout?: boolean;
|
package/client/device.ts
CHANGED
|
@@ -15,14 +15,13 @@ let debugSeq = 0;
|
|
|
15
15
|
|
|
16
16
|
const isFrameDebugEnabled = () => {
|
|
17
17
|
if (typeof window === "undefined") return false;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
);
|
|
18
|
+
if (new URLSearchParams(window.location.search).get("akanFrameDebug") === "1") return true;
|
|
19
|
+
try {
|
|
20
|
+
return window.localStorage.getItem("akan:debug:frame") === "1";
|
|
21
|
+
} catch {
|
|
22
|
+
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
26
25
|
};
|
|
27
26
|
|
|
28
27
|
export function debugFrame(event: string, payload: DebugPayload = {}) {
|
|
@@ -33,7 +32,8 @@ export function debugFrame(event: string, payload: DebugPayload = {}) {
|
|
|
33
32
|
now: Math.round(performance.now()),
|
|
34
33
|
...payload,
|
|
35
34
|
};
|
|
36
|
-
|
|
35
|
+
|
|
36
|
+
console.debug(`[akan:frame:${debugSessionId}:${debugSeq}] ${event}`, details);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
interface DeviceInitOption {
|
package/common/Logger.ts
CHANGED
|
@@ -77,6 +77,15 @@ const colorizeMap: { [key in LogLevel]: (text: string) => string } = {
|
|
|
77
77
|
error: clc.red,
|
|
78
78
|
};
|
|
79
79
|
|
|
80
|
+
const consoleMethodMap = {
|
|
81
|
+
trace: "debug",
|
|
82
|
+
verbose: "debug",
|
|
83
|
+
debug: "debug",
|
|
84
|
+
info: "info",
|
|
85
|
+
warn: "warn",
|
|
86
|
+
error: "error",
|
|
87
|
+
} as const satisfies { [key in LogLevel]: "debug" | "info" | "warn" | "error" };
|
|
88
|
+
|
|
80
89
|
const ansiPattern = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "g");
|
|
81
90
|
|
|
82
91
|
const redactedAttrKey = /password|passwd|token|jwt|authorization|cookie|secret|api[-_]?key|private[-_]?key/i;
|
|
@@ -332,7 +341,22 @@ export class Logger {
|
|
|
332
341
|
if (!Logger.consoleOutput) return;
|
|
333
342
|
if (typeof window === "undefined")
|
|
334
343
|
(process[entry.stream] as unknown as NodeJS.WriteStream | undefined)?.write(entry.message);
|
|
335
|
-
else
|
|
344
|
+
else Logger.#writeBrowserConsole(entry.record, entry.message);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
static #writeBrowserConsole(record: LogRecord, rendered: string) {
|
|
348
|
+
const level = record.level ?? "info";
|
|
349
|
+
const context = record.context ? `[${record.context}] ` : "";
|
|
350
|
+
const attrs = record.attrs ? ` ${Logger.formatAttrs(record.attrs)}` : "";
|
|
351
|
+
const text = Logger.#inNativeShell()
|
|
352
|
+
? `[${record.name}] ${context}${record.message}${attrs}`
|
|
353
|
+
: rendered.replace(/\n+$/, "");
|
|
354
|
+
|
|
355
|
+
console[consoleMethodMap[level]](text);
|
|
356
|
+
}
|
|
357
|
+
static #inNativeShell() {
|
|
358
|
+
const platform = (globalThis as { __AKAN_NATIVE__?: { platform?: string } }).__AKAN_NATIVE__?.platform;
|
|
359
|
+
return platform !== undefined && platform !== "web";
|
|
336
360
|
}
|
|
337
361
|
static #write(name: string | undefined, message: string, context: string, logLevel: LogLevel, attrs?: LogAttrs) {
|
|
338
362
|
const stream = logLevel === "error" ? "stderr" : "stdout";
|
|
@@ -367,7 +391,7 @@ export class Logger {
|
|
|
367
391
|
if (!Logger.consoleOutput) return;
|
|
368
392
|
if (typeof window === "undefined" && method !== "console" && (process as unknown as NodeJS.Process | undefined))
|
|
369
393
|
process[stream].write(msg);
|
|
370
|
-
else console[outputStream === "error" ? "error" : "log"](msg.trim());
|
|
394
|
+
else console[outputStream === "error" ? "error" : Logger.#inNativeShell() ? "debug" : "log"](msg.trim());
|
|
371
395
|
}
|
|
372
396
|
static {
|
|
373
397
|
if (envValue("AKAN_PUBLIC_LOG_LEVEL") === "log" && typeof window === "undefined")
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
interface LayeredRoute<Render> {
|
|
2
|
+
/** The tree segment: `/`, `/:lang`, `/(group)`, `/foo`. */
|
|
3
|
+
path: string;
|
|
4
|
+
renderLayout?: Render;
|
|
5
|
+
/** A generated `__root_layout`: one of the root boundaries the page generator found. */
|
|
6
|
+
isRootLayout?: boolean;
|
|
7
|
+
renderOverrides?: Render;
|
|
8
|
+
/** `false` for a named page file, which sits beside its directory's layout rather than under it. */
|
|
9
|
+
pageIncludesOwnLayout?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RouteLayer<Render> {
|
|
13
|
+
/** The URL pattern down to this node; a route group adds nothing to it. */
|
|
14
|
+
path: string;
|
|
15
|
+
pathSegments: string[];
|
|
16
|
+
isRoot: boolean;
|
|
17
|
+
/** The root layout above or at this node, overrides left out: once there is one, nothing below is a root. */
|
|
18
|
+
rootLayouts: Render[];
|
|
19
|
+
renderRootLayouts: Render[];
|
|
20
|
+
renderLayouts: Render[];
|
|
21
|
+
/** What a page file at this node renders inside: without this node's layout for a named page file. */
|
|
22
|
+
pageRenderRootLayouts: Render[];
|
|
23
|
+
pageRenderLayouts: Render[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class RouteLayering {
|
|
27
|
+
static readonly #group = /^\/\(.*\)$/;
|
|
28
|
+
|
|
29
|
+
static of<Render>(route: LayeredRoute<Render>, parent: RouteLayer<Render> | null): RouteLayer<Render> {
|
|
30
|
+
const parentSegments = parent?.pathSegments ?? [];
|
|
31
|
+
const parentRootLayouts = parent?.rootLayouts ?? [];
|
|
32
|
+
const parentRootRenders = parent?.renderRootLayouts ?? [];
|
|
33
|
+
const parentLayouts = parent?.renderLayouts ?? [];
|
|
34
|
+
const segment = RouteLayering.#group.test(route.path) ? "" : route.path;
|
|
35
|
+
const path = parentSegments.filter((part) => part !== "/").join("") + segment;
|
|
36
|
+
const isRoot = parentRootLayouts.length === 0 && (route.isRootLayout === true || !route.renderLayout);
|
|
37
|
+
const overrideRenders = route.renderOverrides ? [route.renderOverrides] : [];
|
|
38
|
+
|
|
39
|
+
const nodeRenders = [...overrideRenders, ...(route.renderLayout ? [route.renderLayout] : [])];
|
|
40
|
+
const pageNodeRenders = route.pageIncludesOwnLayout === false ? overrideRenders : nodeRenders;
|
|
41
|
+
return {
|
|
42
|
+
path,
|
|
43
|
+
pathSegments: [...parentSegments, ...(segment ? [segment] : [])],
|
|
44
|
+
isRoot,
|
|
45
|
+
rootLayouts: isRoot && route.renderLayout ? [...parentRootLayouts, route.renderLayout] : parentRootLayouts,
|
|
46
|
+
renderRootLayouts: isRoot ? [...parentRootRenders, ...nodeRenders] : parentRootRenders,
|
|
47
|
+
renderLayouts: isRoot ? parentLayouts : [...parentLayouts, ...nodeRenders],
|
|
48
|
+
pageRenderRootLayouts: isRoot ? [...parentRootRenders, ...pageNodeRenders] : parentRootRenders,
|
|
49
|
+
pageRenderLayouts: isRoot ? parentLayouts : [...parentLayouts, ...pageNodeRenders],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
package/common/hmrPhase.ts
CHANGED
|
@@ -5,6 +5,7 @@ declare global {
|
|
|
5
5
|
var __AKAN_HMR_PHASE__: AkanHmrPhase | undefined;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
/** @deprecated Always `null`: the dev client that set the phase applies modules through its registry now. */
|
|
8
9
|
export function getAkanHmrPhase(): AkanHmrPhase {
|
|
9
10
|
if (typeof globalThis === "undefined") return null;
|
|
10
11
|
const phase = globalThis.__AKAN_HMR_PHASE__;
|
|
@@ -12,6 +13,7 @@ export function getAkanHmrPhase(): AkanHmrPhase {
|
|
|
12
13
|
return null;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
/** @deprecated Always `false`, for the reason `getAkanHmrPhase` is always `null`. */
|
|
15
17
|
export function isAkanHmrApplying(): boolean {
|
|
16
18
|
return getAkanHmrPhase() !== null;
|
|
17
19
|
}
|
package/common/index.ts
CHANGED
|
@@ -128,6 +128,7 @@ export {
|
|
|
128
128
|
export { deepObjectify, objectify, plainFieldsOf } from "./objectify";
|
|
129
129
|
export { pathGet, pathGetLoose, pathSet, toPathSegments } from "./objectPath";
|
|
130
130
|
export { formatPhone, isPhoneNumber } from "./phone";
|
|
131
|
+
export { type RouteLayer, RouteLayering } from "./RouteLayering";
|
|
131
132
|
export { randomPick, randomPicks } from "./randomPick";
|
|
132
133
|
export { hostFromRequest, isJsonContentType, originFromRequest } from "./requestOrigin";
|
|
133
134
|
export { RestClient, type RestClientOptions, type RestRequestOptions } from "./restClient";
|
package/package.json
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DevBuildStatus } from "./ipcTypes";
|
|
2
|
+
|
|
3
|
+
export class DevBuildRecovery {
|
|
4
|
+
|
|
5
|
+
static recovers(previous: DevBuildStatus | undefined, status: DevBuildStatus): boolean {
|
|
6
|
+
if (!previous || previous.ok || !status.ok) return false;
|
|
7
|
+
if (status.phase === "route" && status.scope !== undefined && previous.scope !== undefined)
|
|
8
|
+
return status.scope === previous.scope && status.generation >= previous.generation;
|
|
9
|
+
if (status.generation !== previous.generation) return status.generation > previous.generation;
|
|
10
|
+
return status.phase === "backend";
|
|
11
|
+
}
|
|
12
|
+
}
|
package/server/artifact/index.ts
CHANGED
package/server/di/diLifecycle.ts
CHANGED
|
@@ -118,7 +118,7 @@ export class DiLifecycle {
|
|
|
118
118
|
constructor({ env, modules = [], disableModules = [], disableLibs = [] }: DiLifecycleProps, ...libs: AkanLib[]) {
|
|
119
119
|
this.#env = env;
|
|
120
120
|
const databaseMode = DiLifecycle.#databaseMode();
|
|
121
|
-
this.logger.
|
|
121
|
+
this.logger.debug(`Database mode: ${DatabaseModes.describe(databaseMode)}`);
|
|
122
122
|
|
|
123
123
|
this.#predefinedAdaptor = { ...getPredefinedAdaptor(databaseMode) };
|
|
124
124
|
this.#libs = libs;
|
|
@@ -52,12 +52,12 @@ export const HMR_CLIENT_SCRIPT = `(function(){
|
|
|
52
52
|
function connect(){
|
|
53
53
|
try { socket = new WebSocket(url); }
|
|
54
54
|
catch(e){ console.error("[akan-hmr] ws init failed", e); schedule(); return; }
|
|
55
|
-
socket.addEventListener("open", function(){ attempts = 0; });
|
|
56
55
|
socket.addEventListener("message", function(ev){
|
|
57
56
|
var msg;
|
|
58
57
|
try { msg = JSON.parse(ev.data); } catch (e){ return; }
|
|
59
58
|
if (!msg || typeof msg.type !== "string") return;
|
|
60
59
|
if (msg.type === "hello") {
|
|
60
|
+
attempts = 0;
|
|
61
61
|
var dropped = Array.isArray(msg.failingPhases) && dropBuildErrorsExcept(msg.failingPhases);
|
|
62
62
|
if (dropped && systemPage) {
|
|
63
63
|
reloadForUpdate("the build that failed to render this page recovered");
|
|
@@ -387,7 +387,7 @@ export const HMR_CLIENT_SCRIPT = `(function(){
|
|
|
387
387
|
var current = buildErrorStates[phase];
|
|
388
388
|
if (!current) return;
|
|
389
389
|
var generation = typeof msg.generation === "number" ? msg.generation : 0;
|
|
390
|
-
var sameGeneration = phase === "backend" || phase === "
|
|
390
|
+
var sameGeneration = phase === "backend" || phase === "route";
|
|
391
391
|
var recovered = sameGeneration ? generation >= current.generation : generation > current.generation;
|
|
392
392
|
if (!recovered) return;
|
|
393
393
|
delete buildErrorStates[phase];
|
|
@@ -405,6 +405,12 @@ export const HMR_CLIENT_SCRIPT = `(function(){
|
|
|
405
405
|
return dropped;
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
+
function clearRenderError(){
|
|
409
|
+
if (!buildErrorStates.build) return;
|
|
410
|
+
delete buildErrorStates.build;
|
|
411
|
+
showBuildRecovered({});
|
|
412
|
+
}
|
|
413
|
+
|
|
408
414
|
function showBuildRecovered(msg){
|
|
409
415
|
if (hasBuildErrors()) {
|
|
410
416
|
renderBuildErrorOverlay();
|
|
@@ -462,9 +468,12 @@ export const HMR_CLIENT_SCRIPT = `(function(){
|
|
|
462
468
|
return prefix ? prefix + "\\n" + msg.message : msg.message;
|
|
463
469
|
}
|
|
464
470
|
|
|
471
|
+
var rscRefreshSeq = 0;
|
|
472
|
+
var rscRefreshedSeq = 0;
|
|
465
473
|
function refreshRsc(msg){
|
|
466
474
|
var receivedAt = Date.now();
|
|
467
|
-
var
|
|
475
|
+
var seq = ++rscRefreshSeq;
|
|
476
|
+
var refresh = function(){ if (seq === rscRefreshSeq) doRefreshRsc(msg, receivedAt, seq); };
|
|
468
477
|
var settle = function(){
|
|
469
478
|
return self.__akan && typeof self.__akan.whenSettled === "function" ? self.__akan.whenSettled() : null;
|
|
470
479
|
};
|
|
@@ -477,7 +486,7 @@ export const HMR_CLIENT_SCRIPT = `(function(){
|
|
|
477
486
|
settled.then(refresh, refresh);
|
|
478
487
|
}
|
|
479
488
|
|
|
480
|
-
function doRefreshRsc(msg, receivedAt){
|
|
489
|
+
function doRefreshRsc(msg, receivedAt, seq){
|
|
481
490
|
var started = performance.now();
|
|
482
491
|
var overlayToken = beginHmrOverlay("Refreshing page...");
|
|
483
492
|
try { self.__AKAN_RSC_CLEAR_CACHE__ && self.__AKAN_RSC_CLEAR_CACHE__(); } catch(e){}
|
|
@@ -488,9 +497,13 @@ export const HMR_CLIENT_SCRIPT = `(function(){
|
|
|
488
497
|
return;
|
|
489
498
|
}
|
|
490
499
|
Promise.resolve(self.__AKAN_RSC_REFRESH__({ buildId: msg.buildId })).then(function(){
|
|
491
|
-
|
|
500
|
+
if (!(seq < rscRefreshedSeq)) {
|
|
501
|
+
rscRefreshedSeq = seq;
|
|
502
|
+
lastBuildId = msg.buildId;
|
|
503
|
+
}
|
|
492
504
|
recordTrace("rsc-refresh", msg, receivedAt, Date.now());
|
|
493
505
|
endHmrOverlay(overlayToken);
|
|
506
|
+
clearRenderError();
|
|
494
507
|
console.debug && console.debug("[akan-hmr] RSC refreshed", {
|
|
495
508
|
buildId: msg.buildId,
|
|
496
509
|
generation: msg.generation,
|
|
@@ -207,21 +207,25 @@ export const installCsrDevRuntime = (host: CsrDevRuntimeHost): void => {
|
|
|
207
207
|
const entry = script?.dataset.akanCsrEntry;
|
|
208
208
|
if (!entry) throw new Error("[akan-csr] the app script carries no data-akan-csr-entry");
|
|
209
209
|
this.#generation = generation;
|
|
210
|
-
this.#
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
210
|
+
this.#run(entry, () => {
|
|
211
|
+
const runtime = this.#load(refresh).exports as RefreshRuntime;
|
|
212
|
+
|
|
213
|
+
runtime.injectIntoGlobalHook(host);
|
|
214
|
+
this.#refresh = runtime;
|
|
215
|
+
});
|
|
214
216
|
}
|
|
215
217
|
|
|
216
218
|
startLibrary({ generation, refresh, bootstrap, vendorFile, epoch }: LibraryStart) {
|
|
217
219
|
this.#library = { vendorFile, epoch };
|
|
218
220
|
this.#generation = generation;
|
|
219
|
-
this.#
|
|
220
|
-
|
|
221
|
+
this.#run(bootstrap, () => {
|
|
222
|
+
this.#refresh = this.#load(refresh).exports as RefreshRuntime;
|
|
223
|
+
});
|
|
221
224
|
}
|
|
222
225
|
|
|
223
|
-
#run(entry: string) {
|
|
226
|
+
#run(entry: string, loadRefresh: () => void) {
|
|
224
227
|
try {
|
|
228
|
+
loadRefresh();
|
|
225
229
|
this.#load(entry);
|
|
226
230
|
} catch (error) {
|
|
227
231
|
this.#startFailed = true;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { resolveStaticPath } from "../staticPath";
|
|
3
4
|
import {
|
|
@@ -38,6 +39,8 @@ export class CsrDevShell {
|
|
|
38
39
|
readonly #appWaitMs: number;
|
|
39
40
|
readonly #appPollMs: number;
|
|
40
41
|
readonly #bootWaitMs: number;
|
|
42
|
+
|
|
43
|
+
readonly #sourceMaps = new Map<string, { stamp: string; body: string }>();
|
|
41
44
|
|
|
42
45
|
constructor(
|
|
43
46
|
artifactDir: string,
|
|
@@ -109,7 +112,8 @@ ${stylesheet} </head>
|
|
|
109
112
|
const deadline = Date.now() + this.#appWaitMs;
|
|
110
113
|
for (;;) {
|
|
111
114
|
const manifest = await this.readManifest();
|
|
112
|
-
if (!manifest ||
|
|
115
|
+
if (!manifest || manifest.generation < generation || appGenerationOf(manifest) >= generation) return;
|
|
116
|
+
if (Date.now() >= deadline || signal?.aborted) return;
|
|
113
117
|
await Bun.sleep(this.#appPollMs);
|
|
114
118
|
}
|
|
115
119
|
}
|
|
@@ -135,9 +139,26 @@ ${stylesheet} </head>
|
|
|
135
139
|
|
|
136
140
|
async #serveSourceMap(name: string): Promise<Response> {
|
|
137
141
|
const generatedFile = name.slice(0, -".map".length);
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
142
|
+
const layoutPath = path.join(this.#dir, `${generatedFile}.layout.json`);
|
|
143
|
+
const stat = await fs.stat(layoutPath).catch(() => null);
|
|
144
|
+
if (!stat) return new Response("Not Found", { status: 404 });
|
|
145
|
+
const stamp = `${stat.ino}:${stat.size}:${stat.mtimeMs}`;
|
|
146
|
+
const cached = this.#sourceMaps.get(generatedFile);
|
|
147
|
+
const body = cached?.stamp === stamp ? cached.body : await this.#mergeSourceMap(generatedFile, layoutPath);
|
|
148
|
+
if (!body) return new Response("Not Found", { status: 404 });
|
|
149
|
+
this.#sourceMaps.delete(generatedFile);
|
|
150
|
+
this.#sourceMaps.set(generatedFile, { stamp, body });
|
|
151
|
+
for (const oldest of this.#sourceMaps.keys()) {
|
|
152
|
+
if (this.#sourceMaps.size <= 4) break;
|
|
153
|
+
this.#sourceMaps.delete(oldest);
|
|
154
|
+
}
|
|
155
|
+
return new Response(body, { headers: CsrDevShell.#headers("application/json", "no-store") });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async #mergeSourceMap(generatedFile: string, layoutPath: string): Promise<string | null> {
|
|
159
|
+
const layoutText = await fs.readFile(layoutPath, "utf8").catch(() => null);
|
|
160
|
+
if (!layoutText) return null;
|
|
161
|
+
const layout = JSON.parse(layoutText) as CsrDevLayout;
|
|
141
162
|
const sections = await Promise.all(
|
|
142
163
|
layout.modules.map(async ([id, line]) => {
|
|
143
164
|
const file = Bun.file(path.join(this.#dir, csrDevModuleFile(id, ".js.map")));
|
|
@@ -149,7 +170,7 @@ ${stylesheet} </head>
|
|
|
149
170
|
sections.filter((section) => section !== null),
|
|
150
171
|
layout.lineCount,
|
|
151
172
|
);
|
|
152
|
-
return
|
|
173
|
+
return JSON.stringify(map);
|
|
153
174
|
}
|
|
154
175
|
|
|
155
176
|
static #js(body: string | Blob, cacheControl: string): Response {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Logger } from "akanjs/common";
|
|
4
|
+
import type { CssAsset } from "../types";
|
|
5
|
+
|
|
6
|
+
export class DevArtifactPruner {
|
|
7
|
+
|
|
8
|
+
static readonly #defaultGraceMs = 60_000;
|
|
9
|
+
static readonly #pagesFile = /^pages-[^/\\]+\.js$/;
|
|
10
|
+
readonly #logger = new Logger("DevArtifactPruner");
|
|
11
|
+
readonly #artifactDir: string;
|
|
12
|
+
readonly #graceMs: number;
|
|
13
|
+
#pruning: Promise<void> = Promise.resolve();
|
|
14
|
+
|
|
15
|
+
constructor(artifactDir: string, { graceMs = DevArtifactPruner.#defaultGraceMs }: { graceMs?: number } = {}) {
|
|
16
|
+
this.#artifactDir = path.resolve(artifactDir);
|
|
17
|
+
this.#graceMs = graceMs;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
prunePages(runningPath: string): Promise<void> {
|
|
21
|
+
return this.#queue(async () => {
|
|
22
|
+
const running = await fs.stat(runningPath).catch(() => null);
|
|
23
|
+
if (!running) return;
|
|
24
|
+
const keep = new Set([path.resolve(runningPath), ...(await this.#baseArtifact()).pages]);
|
|
25
|
+
const serverDir = path.join(this.#artifactDir, "server");
|
|
26
|
+
const before = Math.min(running.mtimeMs, Date.now() - this.#graceMs);
|
|
27
|
+
await this.#removeOlder(serverDir, (name) => DevArtifactPruner.#pagesFile.test(name), keep, before);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
pruneStyles(cssAssets: Record<string, CssAsset>): Promise<void> {
|
|
32
|
+
return this.#queue(async () => {
|
|
33
|
+
const current = Object.values(cssAssets).map((asset) => path.join(this.#artifactDir, asset.cssRelPath));
|
|
34
|
+
const keep = new Set([...current, ...(await this.#baseArtifact()).styles]);
|
|
35
|
+
const stylesDir = path.join(this.#artifactDir, "styles");
|
|
36
|
+
await this.#removeOlder(stylesDir, (name) => name.endsWith(".css"), keep, Date.now() - this.#graceMs);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#queue(prune: () => Promise<void>): Promise<void> {
|
|
41
|
+
this.#pruning = this.#pruning.then(prune).catch((error: unknown) => {
|
|
42
|
+
this.#logger.verbose(`[dev-artifact] prune skipped: ${String(error)}`);
|
|
43
|
+
});
|
|
44
|
+
return this.#pruning;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async #removeOlder(dir: string, matches: (name: string) => boolean, keep: Set<string>, before: number) {
|
|
48
|
+
const names = await fs.readdir(dir).catch(() => [] as string[]);
|
|
49
|
+
let removed = 0;
|
|
50
|
+
for (const name of names) {
|
|
51
|
+
const file = path.join(dir, name);
|
|
52
|
+
if (!matches(name) || keep.has(file)) continue;
|
|
53
|
+
const stat = await fs.stat(file).catch(() => null);
|
|
54
|
+
if (!stat || stat.mtimeMs >= before) continue;
|
|
55
|
+
|
|
56
|
+
if (
|
|
57
|
+
await fs.rm(file, { force: true }).then(
|
|
58
|
+
() => true,
|
|
59
|
+
() => false,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
removed += 1;
|
|
63
|
+
}
|
|
64
|
+
if (removed > 0) this.#logger.verbose(`[dev-artifact] removed ${removed} superseded file(s) from ${dir}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async #baseArtifact(): Promise<{ pages: string[]; styles: string[] }> {
|
|
68
|
+
const text = await fs.readFile(path.join(this.#artifactDir, "base-artifact.json"), "utf8").catch(() => null);
|
|
69
|
+
const artifact = text
|
|
70
|
+
? (JSON.parse(text) as { pagesBundlePath?: string; cssAssets?: Record<string, CssAsset> })
|
|
71
|
+
: null;
|
|
72
|
+
return {
|
|
73
|
+
pages: artifact?.pagesBundlePath ? [path.resolve(this.#artifactDir, artifact.pagesBundlePath)] : [],
|
|
74
|
+
styles: Object.values(artifact?.cssAssets ?? {}).map((asset) => path.join(this.#artifactDir, asset.cssRelPath)),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|