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.
Files changed (39) hide show
  1. package/client/csrTypes.ts +2 -0
  2. package/client/device.ts +9 -9
  3. package/common/Logger.ts +26 -2
  4. package/common/RouteLayering.ts +52 -0
  5. package/common/hmrPhase.ts +2 -0
  6. package/common/index.ts +1 -0
  7. package/package.json +1 -1
  8. package/server/artifact/devBuildRecovery.ts +12 -0
  9. package/server/artifact/index.ts +1 -0
  10. package/server/artifact/ipcTypes.ts +2 -0
  11. package/server/di/diLifecycle.ts +1 -1
  12. package/server/hmr/clientScript.ts +18 -5
  13. package/server/hmr/csrDevRuntime.ts +11 -7
  14. package/server/hmr/csrDevShell.ts +26 -5
  15. package/server/hmr/devArtifactPruner.ts +77 -0
  16. package/server/hmr/devHmrController.ts +69 -44
  17. package/server/hmr/ssrDevShim.ts +7 -1
  18. package/server/routeTreeBuilder.ts +14 -39
  19. package/server/rscWorker.tsx +7 -1
  20. package/server/rscWorkerHost.ts +126 -40
  21. package/server/webRouter.ts +25 -19
  22. package/types/client/csrTypes.d.ts +2 -0
  23. package/types/common/RouteLayering.d.ts +28 -0
  24. package/types/common/hmrPhase.d.ts +2 -0
  25. package/types/common/index.d.ts +1 -0
  26. package/types/server/artifact/devBuildRecovery.d.ts +4 -0
  27. package/types/server/artifact/index.d.ts +1 -0
  28. package/types/server/artifact/ipcTypes.d.ts +2 -0
  29. package/types/server/hmr/devArtifactPruner.d.ts +9 -0
  30. package/types/server/hmr/devHmrController.d.ts +0 -2
  31. package/types/server/rscWorkerHost.d.ts +5 -4
  32. package/types/server/webRouter.d.ts +4 -2
  33. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeActivity.kt +3 -1
  34. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeBridge.kt +19 -0
  35. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeShell.swift +12 -2
  36. package/vendor/@akanjs/native/packages/core/src/runtime.ts +26 -5
  37. package/vendor/@akanjs/native/packages/desktop/src/host.ts +5 -3
  38. package/webkit/CsrRouteTable.ts +15 -35
  39. package/webkit/bootCsr.tsx +11 -8
@@ -1,9 +1,11 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { Logger } from "akanjs/common";
4
+ import { CrossSiteGuard } from "../../signal/CrossSiteGuard";
4
5
  import {
5
6
  BuilderRpc,
6
7
  type ClientManifest,
8
+ DevBuildRecovery,
7
9
  type DevBuildStatus,
8
10
  type HmrTrace,
9
11
  RouteClientCache,
@@ -13,6 +15,7 @@ import {
13
15
  import type { RscReloadFailure, RscWorker } from "../rscWorkerHost";
14
16
  import type { RenderState } from "../types";
15
17
  import { CSR_DEV_DIRNAME, CSR_DEV_MANIFEST_FILE, type CsrDevManifest, SSR_DEV_DIRNAME } from "./csrDevManifest";
18
+ import { DevArtifactPruner } from "./devArtifactPruner";
16
19
  import { isAkanRuntimeMetadataFile } from "./runtimeMetadataFile";
17
20
  import { type SsrUpdateMessage, SsrUpdateQueue } from "./ssrUpdateQueue";
18
21
  import { type ChangeKind, type HmrMessage, type HmrWsData, HmrWsHub } from "./wsHub";
@@ -49,11 +52,7 @@ export function devBuildStatusToHmrMessage(
49
52
  files: status.files.length,
50
53
  };
51
54
  }
52
- if (!previous || previous.ok) return null;
53
-
54
- const sameGeneration = status.phase === "backend" || status.phase === "scan" || status.phase === "route";
55
- const recovered = sameGeneration ? status.generation >= previous.generation : status.generation > previous.generation;
56
- if (!recovered) return null;
55
+ if (!DevBuildRecovery.recovers(previous, status)) return null;
57
56
  return {
58
57
  type: "build-status",
59
58
  status: "ok",
@@ -85,6 +84,8 @@ export class DevHmrController {
85
84
  readonly #ssrUpdates = new SsrUpdateQueue((message) => this.#sendSsrUpdate(message));
86
85
  readonly #builderRpc: BuilderRpc;
87
86
  readonly routeCache: RouteClientCache;
87
+ #recheckingRoute = false;
88
+ #recheckAgain: DevBuildStatus | null = null;
88
89
  readonly #recentClientEntries = new Set<string>();
89
90
  /** File to the newest save that invalidated its routes already: that save's pages build must not drop them again. */
90
91
  readonly #earlyInvalidated = new Map<string, number>();
@@ -97,10 +98,12 @@ export class DevHmrController {
97
98
  readonly #buildStatusByPhase = new Map<DevBuildStatus["phase"], DevBuildStatus>();
98
99
  #graphSeeds: string[];
99
100
  #runningBundlePath: string | null;
101
+ readonly #pruner: DevArtifactPruner;
100
102
 
101
103
  constructor({ artifactDir, renderState, rsc, seedIndex, upgradeHmrWs, pagesBundlePath }: DevHmrControllerOptions) {
102
104
  this.#artifactDir = artifactDir;
103
105
  this.#runningBundlePath = pagesBundlePath ? path.resolve(pagesBundlePath) : null;
106
+ this.#pruner = new DevArtifactPruner(artifactDir);
104
107
  this.#renderState = renderState;
105
108
  this.#rsc = rsc;
106
109
  this.#seedIndex = seedIndex;
@@ -155,28 +158,17 @@ export class DevHmrController {
155
158
  }
156
159
 
157
160
  handleWs(req: Request): Response | undefined {
161
+
162
+ try {
163
+ CrossSiteGuard.assertOrigin(req, new URL(req.url), "hmr");
164
+ } catch {
165
+ return new Response("Forbidden", { status: 403 });
166
+ }
158
167
  const client = new URL(req.url).searchParams.get("client") === "csr" ? "csr" : "ssr";
159
168
  if (this.#upgradeHmrWs(req, { kind: "akan-hmr", openedAt: Date.now(), client })) return;
160
169
  return new Response("Failed to upgrade HMR WebSocket", { status: 500 });
161
170
  }
162
171
 
163
- async ensureRoute(url: URL) {
164
- const started = Date.now();
165
- const matched =
166
- RouteSeedIndexStore.match(url.pathname, this.#seedIndex.entries) ??
167
- RouteSeedIndexStore.matchPrefix(url.pathname, this.#seedIndex.entries);
168
- if (matched) await this.routeCache.ensure(matched.entry.routeId, matched.entry.seeds);
169
- this.#logger.verbose(
170
- `[route-cache] ensure pathname=${url.pathname} routeId=${matched?.entry.routeId ?? "(none)"} in ${Date.now() - started}ms`,
171
- );
172
- return this.routeCache.snapshot();
173
- }
174
-
175
- routeIdsForPath(pathname: string): string[] | undefined {
176
- const matched = RouteSeedIndexStore.match(pathname, this.#seedIndex.entries);
177
- return matched ? [matched.entry.routeId] : undefined;
178
- }
179
-
180
172
  #sendSsrUpdate(message: SsrUpdateMessage): void {
181
173
  this.#renderState.ssrGeneration = message.generation;
182
174
  this.#hub.broadcast({ ...message, trace: DevHmrController.#broadcastTrace(message.trace) });
@@ -235,6 +227,7 @@ export class DevHmrController {
235
227
  this.#renderState.cssBytesByUrl = cssBytesByUrl;
236
228
  this.#rsc.updateCssAssets(this.#renderState.cssAssets);
237
229
  this.#hub.broadcast({ type: "css-update", cssAssets: this.#renderState.cssAssets });
230
+ void this.#pruner.pruneStyles(this.#renderState.cssAssets);
238
231
  this.#logger.verbose(
239
232
  `css-update assets=${Object.keys(this.#renderState.cssAssets).length} generation=${css.generation ?? "(unknown)"} files=${css.changedFiles?.length ?? 0} in ${Date.now() - started}ms (ipc)`,
240
233
  );
@@ -266,6 +259,7 @@ export class DevHmrController {
266
259
  const manifest = this.routeCache.snapshot();
267
260
 
268
261
  const running = bundlePath ? path.resolve(bundlePath) : null;
262
+ let tabBuildId = buildId;
269
263
  if (serverTouched === false && !clearAll && running === this.#runningBundlePath) {
270
264
  this.#logger.verbose(
271
265
  `[SSR] pages bundle unchanged for the server; buildId ${this.#renderState.buildId} kept`,
@@ -275,25 +269,29 @@ export class DevHmrController {
275
269
  this.#renderState.buildId = buildId;
276
270
  const reloadStarted = Date.now();
277
271
  try {
278
- this.#runningBundlePath = path.resolve(
279
- await this.#rsc.reload({
280
- clientManifest: manifest.clientManifest,
281
- cssAssets: this.#renderState.cssAssets,
282
- buildId,
283
- pagesBundlePath: bundlePath,
284
- }),
285
- );
272
+ const adopted = await this.#rsc.reload({
273
+ clientManifest: manifest.clientManifest,
274
+ cssAssets: this.#renderState.cssAssets,
275
+ buildId,
276
+ pagesBundlePath: bundlePath,
277
+ });
278
+ this.#adoptBundle(adopted.pagesBundlePath);
279
+
280
+ tabBuildId = adopted.buildId;
286
281
  } catch (error) {
287
282
  const failure = DevHmrController.#reloadFailure(error);
288
283
 
289
284
  if (failure) {
290
285
  this.#renderState.buildId = failure.adopted.buildId;
291
- this.#runningBundlePath = path.resolve(failure.adopted.pagesBundlePath);
286
+ this.#adoptBundle(failure.adopted.pagesBundlePath);
292
287
  } else if (this.#renderState.buildId === buildId) this.#renderState.buildId = previousBuildId;
293
288
 
294
- if (failure?.superseded) this.#ssrUpdates.release(generation);
295
- else this.#failPagesReload(generation, files, error);
296
- return;
289
+ if (failure?.adopted.buildId !== buildId) {
290
+
291
+ if (failure && failure.failed.buildId !== buildId) this.#ssrUpdates.release(generation);
292
+ else this.#failPagesReload(generation, files, error);
293
+ return;
294
+ }
297
295
  }
298
296
  this.#logger.verbose(`[SSR] rsc reload buildId=${buildId} in ${Date.now() - reloadStarted}ms`);
299
297
  }
@@ -301,7 +299,7 @@ export class DevHmrController {
301
299
  const broadcastTrace = DevHmrController.#broadcastTrace(trace);
302
300
  if (shouldReload) this.#ssrUpdates.clear(generation);
303
301
  const released = shouldReload ? { released: 0, reload: false } : this.#ssrUpdates.release(generation);
304
- if (shouldReload) this.#hub.broadcast({ type: "reload", buildId });
302
+ if (shouldReload) this.#hub.broadcast({ type: "reload", buildId: tabBuildId });
305
303
  else if (released.reload)
306
304
  this.#logger.verbose(`[hmr] generation=${generation} released a registry reload; no RSC refresh needed`);
307
305
  else if (serverTouched === false)
@@ -311,7 +309,7 @@ export class DevHmrController {
311
309
  else
312
310
  this.#hub.broadcast({
313
311
  type: "rsc-refresh",
314
- buildId,
312
+ buildId: tabBuildId,
315
313
  generation,
316
314
  changedFiles,
317
315
  routeIds,
@@ -324,6 +322,13 @@ export class DevHmrController {
324
322
  });
325
323
  }
326
324
 
325
+ #adoptBundle(bundlePath: string): void {
326
+ const running = path.resolve(bundlePath);
327
+ if (running === this.#runningBundlePath) return;
328
+ this.#runningBundlePath = running;
329
+ void this.#pruner.prunePages(running);
330
+ }
331
+
327
332
  static #reloadFailure(error: unknown): RscReloadFailure | null {
328
333
  return error instanceof Error && "adopted" in error ? (error as RscReloadFailure) : null;
329
334
  }
@@ -351,6 +356,7 @@ export class DevHmrController {
351
356
  }
352
357
 
353
358
  #recordBuildStatus(status: DevBuildStatus): void {
359
+ this.#recheckFailedRoute(status);
354
360
  const previous = this.#buildStatusByPhase.get(status.phase);
355
361
  const message = devBuildStatusToHmrMessage(status, previous);
356
362
 
@@ -363,6 +369,26 @@ export class DevHmrController {
363
369
  );
364
370
  }
365
371
 
372
+ #recheckFailedRoute(status: DevBuildStatus, { again = false }: { again?: boolean } = {}): void {
373
+ if (!status.ok || (status.phase !== "pages" && status.phase !== "ssr")) return;
374
+ if (this.#recheckingRoute) {
375
+ this.#recheckAgain = status;
376
+ return;
377
+ }
378
+ const failed = this.#buildStatusByPhase.get("route");
379
+ if (!failed || failed.ok || !failed.scope || (!again && failed.generation >= status.generation)) return;
380
+ this.#recheckingRoute = true;
381
+ void this.routeCache
382
+ .ensure(failed.scope, failed.files)
383
+ .catch(() => undefined)
384
+ .finally(() => {
385
+ this.#recheckingRoute = false;
386
+ const next = this.#recheckAgain;
387
+ this.#recheckAgain = null;
388
+ if (next) this.#recheckFailedRoute(next, { again: true });
389
+ });
390
+ }
391
+
366
392
  #createRouteCache() {
367
393
  return new RouteClientCache({
368
394
  buildRoute: async (routeId, { seeds, knownEntries, generation }) => {
@@ -387,17 +413,16 @@ export class DevHmrController {
387
413
  if (delta.newEntries.length === 0 && removedEntries.size === 0) return;
388
414
  this.#rememberClientDeps(routeId, delta.clientDeps, delta.clientDepsByEntry);
389
415
  try {
390
- this.#runningBundlePath = path.resolve(
391
- await this.#rsc.reload({
392
- clientManifest: nextMerged.clientManifest,
393
- cssAssets: this.#renderState.cssAssets,
394
- buildId: this.#renderState.buildId,
395
- }),
396
- );
416
+ const adopted = await this.#rsc.reload({
417
+ clientManifest: nextMerged.clientManifest,
418
+ cssAssets: this.#renderState.cssAssets,
419
+ buildId: this.#renderState.buildId,
420
+ });
421
+ this.#adoptBundle(adopted.pagesBundlePath);
397
422
  } catch (error) {
398
423
 
399
424
  const failure = DevHmrController.#reloadFailure(error);
400
- if (failure) this.#runningBundlePath = path.resolve(failure.adopted.pagesBundlePath);
425
+ if (failure) this.#adoptBundle(failure.adopted.pagesBundlePath);
401
426
  this.#logger.warn(`[SSR] route ${routeId} merged, but the worker did not reload: ${String(error)}`);
402
427
  }
403
428
  this.#logger.verbose(
@@ -52,9 +52,15 @@ export class SsrDevShim {
52
52
  var hello = self.__AKAN_SSR_HELLO_GENERATION__;
53
53
  if (typeof hello === "number") self.__akan.catchUp(hello, c.prefix);
54
54
  }
55
+ function runtime(attempt) {
56
+ return script(c.runtime).catch(function (error) {
57
+ if (attempt >= 3) throw error;
58
+ return wait(500).then(state).then(function () { return runtime(attempt + 1); });
59
+ });
60
+ }
55
61
  function boot() {
56
62
  if (!booting)
57
- booting = self.__AKAN_SSR_BOOT__ = script(c.runtime).then(function () {
63
+ booting = self.__AKAN_SSR_BOOT__ = runtime(0).then(function () {
58
64
  var early = self.__AKAN_SSR_EARLY_UPDATES__ || [];
59
65
  self.__AKAN_SSR_EARLY_UPDATES__ = null;
60
66
  for (var i = 0; i < early.length; i++) self.__akan.hot(early[i]);
@@ -15,8 +15,9 @@ import {
15
15
  getRouteExports,
16
16
  Logger,
17
17
  matchRoutePattern,
18
- parseBasePaths,
19
18
  parseRouteModuleKey,
19
+ type RouteLayer,
20
+ RouteLayering,
20
21
  routeSegmentToTreePath,
21
22
  } from "akanjs/common";
22
23
  import { createElement } from "react";
@@ -54,15 +55,12 @@ export class RouteTreeBuilder {
54
55
  };
55
56
 
56
57
  readonly #context: PagesContext;
57
- readonly #baseLayoutPaths: string[];
58
58
  readonly #routeMap = new Map<string, Route>();
59
59
  readonly #pagePatterns: { key: string; pattern: string }[] = [];
60
60
  readonly #fallbackRoutes: LayoutFallbackRoute[] = [];
61
61
 
62
62
  constructor(context: PagesContext) {
63
63
  this.#context = context;
64
- const basePaths = process.env.AKAN_PUBLIC_BASE_PATHS ? parseBasePaths(process.env.AKAN_PUBLIC_BASE_PATHS) : null;
65
- this.#baseLayoutPaths = ["/", "/:lang", ...(basePaths?.map((bp) => `/:lang/${bp}`) ?? [])];
66
64
  this.#routeMap.set("/", { path: "/", children: new Map() });
67
65
  }
68
66
 
@@ -169,7 +167,7 @@ export class RouteTreeBuilder {
169
167
  targetRouteMap.set(targetPath, {
170
168
  ...(targetRouteMap.get(targetPath) ?? { path: targetPath, children: new Map<string, Route>() }),
171
169
  ...(parsed.kind === "layout"
172
- ? { renderLayout: routeRender }
170
+ ? { renderLayout: routeRender, isRootLayout: parsed.isInternalRootLayout }
173
171
  : {
174
172
  renderPage: routeRender,
175
173
  pageIncludesOwnLayout: parsed.leaf === "_index",
@@ -178,48 +176,27 @@ export class RouteTreeBuilder {
178
176
  } as Route);
179
177
  }
180
178
 
181
- #getPathRoutes(
182
- route: Route,
183
- parentRootLayouts: RouteRender[] = [],
184
- parentLayouts: RouteRender[] = [],
185
- parentPaths: string[] = [],
186
- parentHead?: ResolveHead,
187
- parentRootRenders: RouteRender[] = [],
188
- ): PathRoute[] {
189
- const parentPath = parentPaths.filter((p) => p !== "/").join("");
190
- const currentPathSegment = /^\/\(.*\)$/.test(route.path) ? "" : route.path;
191
- const isRoot = this.#baseLayoutPaths.includes(parentPath + currentPathSegment) && parentRootLayouts.length < 2;
192
- const routePath = parentPath + currentPathSegment;
193
- const pathSegments = [...parentPaths, ...(currentPathSegment ? [currentPathSegment] : [])];
194
- const currentRootLayout = isRoot && route.renderLayout ? route.renderLayout : null;
195
-
196
- const currentOverrideRenders = route.renderOverrides ? [route.renderOverrides] : [];
197
- const nodeRenders = [...currentOverrideRenders, ...(route.renderLayout ? [route.renderLayout] : [])];
198
- const rootLayoutStack = [...parentRootLayouts, ...(currentRootLayout ? [currentRootLayout] : [])];
199
- const renderRootLayouts = isRoot ? [...parentRootRenders, ...nodeRenders] : parentRootRenders;
200
- const renderLayouts = isRoot ? parentLayouts : [...parentLayouts, ...nodeRenders];
179
+ #getPathRoutes(route: Route, parent: RouteLayer<RouteRender> | null = null, parentHead?: ResolveHead): PathRoute[] {
180
+ const layer = RouteLayering.of(route, parent);
201
181
  if (route.renderLayout) {
202
182
  this.#fallbackRoutes.push({
203
- path: routePath,
204
- pathSegments,
205
- renderRootLayouts,
206
- renderLayouts,
183
+ path: layer.path,
184
+ pathSegments: layer.pathSegments,
185
+ renderRootLayouts: layer.renderRootLayouts,
186
+ renderLayouts: layer.renderLayouts,
207
187
  });
208
188
  }
209
189
  const routeHead = RouteTreeBuilder.#composeHeadResolvers(route.renderLayout?.resolveHead, parentHead);
210
- const pageNodeRenders = route.pageIncludesOwnLayout === false ? currentOverrideRenders : nodeRenders;
211
- const pageRenderRootLayouts = isRoot ? [...parentRootRenders, ...pageNodeRenders] : parentRootRenders;
212
- const pageRenderLayouts = isRoot ? parentLayouts : [...parentLayouts, ...pageNodeRenders];
213
190
  const pageHead = route.pageIncludesOwnLayout === false ? parentHead : routeHead;
214
191
  return [
215
192
  ...(route.renderPage
216
193
  ? [
217
194
  {
218
- path: routePath,
219
- pathSegments,
195
+ path: layer.path,
196
+ pathSegments: layer.pathSegments,
220
197
  renderPage: route.renderPage,
221
- renderRootLayouts: pageRenderRootLayouts,
222
- renderLayouts: pageRenderLayouts,
198
+ renderRootLayouts: layer.pageRenderRootLayouts,
199
+ renderLayouts: layer.pageRenderLayouts,
223
200
  resolveHead: RouteTreeBuilder.#composeHeadResolvers(route.renderPage.resolveHead, pageHead),
224
201
  isSpecialRoute: route.isSpecialRoute,
225
202
  pageState: route.pageState ?? defaultPageState,
@@ -227,9 +204,7 @@ export class RouteTreeBuilder {
227
204
  ]
228
205
  : []),
229
206
  ...(route.children.size
230
- ? [...route.children.values()].flatMap((child) =>
231
- this.#getPathRoutes(child, rootLayoutStack, renderLayouts, pathSegments, routeHead, renderRootLayouts),
232
- )
207
+ ? [...route.children.values()].flatMap((child) => this.#getPathRoutes(child, layer, routeHead))
233
208
  : []),
234
209
  ];
235
210
  }
@@ -387,7 +387,12 @@ export class RscRenderer {
387
387
  this.#resultCache.clear();
388
388
  this.#patchResultCache.clear();
389
389
  this.#logger.verbose(`reload complete buildId=${msg.buildId} in ${Date.now() - startedAt}ms`);
390
- this.#send({ type: "reloaded", buildId: msg.buildId, reloadId: msg.reloadId });
390
+ this.#send({
391
+ type: "reloaded",
392
+ buildId: msg.buildId,
393
+ reloadId: msg.reloadId,
394
+ pagesBundlePath: nextPagesBundlePath,
395
+ });
391
396
  } catch (error) {
392
397
  this.#logger.error(
393
398
  `reload failed buildId=${msg.buildId}: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`,
@@ -399,6 +404,7 @@ export class RscRenderer {
399
404
  requestId: "__reload__",
400
405
  buildId: msg.buildId,
401
406
  reloadId: msg.reloadId,
407
+ running: { pagesBundlePath: this.#pagesBundlePath, buildId: this.#pagesBundleBuildId },
402
408
  message: error instanceof Error ? error.message : String(error),
403
409
  });
404
410
  }