@rangojs/router 0.0.0-experimental.121 → 0.0.0-experimental.124

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 (120) hide show
  1. package/dist/bin/rango.js +7 -2
  2. package/dist/vite/index.js +47 -6
  3. package/package.json +61 -21
  4. package/skills/cache-guide/SKILL.md +8 -6
  5. package/skills/caching/SKILL.md +148 -1
  6. package/skills/hooks/SKILL.md +38 -27
  7. package/skills/host-router/SKILL.md +16 -2
  8. package/skills/intercept/SKILL.md +4 -2
  9. package/skills/layout/SKILL.md +11 -6
  10. package/skills/loader/SKILL.md +6 -2
  11. package/skills/middleware/SKILL.md +4 -2
  12. package/skills/migrate-nextjs/SKILL.md +38 -16
  13. package/skills/parallel/SKILL.md +9 -4
  14. package/skills/rango/SKILL.md +27 -15
  15. package/skills/route/SKILL.md +4 -2
  16. package/skills/testing/SKILL.md +129 -0
  17. package/skills/testing/bindings.md +89 -0
  18. package/skills/testing/cache-prerender.md +98 -0
  19. package/skills/testing/client-components.md +122 -0
  20. package/skills/testing/e2e-parity.md +125 -0
  21. package/skills/testing/flight.md +89 -0
  22. package/skills/testing/handles.md +129 -0
  23. package/skills/testing/loader.md +128 -0
  24. package/skills/testing/middleware.md +99 -0
  25. package/skills/testing/render-handler.md +118 -0
  26. package/skills/testing/response-routes.md +95 -0
  27. package/skills/testing/reverse-and-types.md +84 -0
  28. package/skills/testing/server-actions.md +107 -0
  29. package/skills/testing/server-tree.md +128 -0
  30. package/skills/testing/setup.md +120 -0
  31. package/skills/use-cache/SKILL.md +9 -7
  32. package/src/browser/action-fence.ts +37 -0
  33. package/src/browser/cookie-name.ts +140 -0
  34. package/src/browser/invalidate-client-cache.ts +52 -0
  35. package/src/browser/navigation-bridge.ts +14 -1
  36. package/src/browser/navigation-client.ts +14 -1
  37. package/src/browser/navigation-store-handle.ts +39 -0
  38. package/src/browser/navigation-store.ts +26 -12
  39. package/src/browser/prefetch/fetch.ts +7 -0
  40. package/src/browser/rango-state.ts +176 -97
  41. package/src/browser/react/index.ts +0 -6
  42. package/src/browser/rsc-router.tsx +12 -4
  43. package/src/browser/server-action-bridge.ts +77 -15
  44. package/src/browser/types.ts +7 -1
  45. package/src/cache/cache-error.ts +104 -0
  46. package/src/cache/cache-policy.ts +95 -1
  47. package/src/cache/cache-runtime.ts +79 -13
  48. package/src/cache/cache-scope.ts +55 -4
  49. package/src/cache/cache-tag.ts +135 -0
  50. package/src/cache/cf/cf-cache-store.ts +2080 -224
  51. package/src/cache/cf/index.ts +15 -1
  52. package/src/cache/document-cache.ts +74 -7
  53. package/src/cache/index.ts +17 -0
  54. package/src/cache/memory-segment-store.ts +164 -14
  55. package/src/cache/tag-invalidation.ts +230 -0
  56. package/src/cache/types.ts +27 -0
  57. package/src/client.rsc.tsx +1 -1
  58. package/src/client.tsx +0 -6
  59. package/src/component-utils.ts +19 -0
  60. package/src/handle.ts +29 -9
  61. package/src/host/testing.ts +43 -14
  62. package/src/index.rsc.ts +29 -1
  63. package/src/index.ts +43 -1
  64. package/src/loader.rsc.ts +24 -3
  65. package/src/loader.ts +16 -2
  66. package/src/prerender.ts +24 -3
  67. package/src/router/basename.ts +14 -0
  68. package/src/router/match-handlers.ts +62 -20
  69. package/src/router/prerender-match.ts +6 -0
  70. package/src/router/router-interfaces.ts +7 -0
  71. package/src/router/router-options.ts +30 -0
  72. package/src/router/segment-resolution/loader-cache.ts +8 -17
  73. package/src/router/state-cookie-name.ts +33 -0
  74. package/src/router/telemetry.ts +99 -0
  75. package/src/router.ts +36 -7
  76. package/src/rsc/handler.ts +13 -1
  77. package/src/rsc/helpers.ts +19 -0
  78. package/src/rsc/progressive-enhancement.ts +2 -0
  79. package/src/rsc/response-route-handler.ts +8 -1
  80. package/src/rsc/rsc-rendering.ts +2 -0
  81. package/src/rsc/types.ts +2 -0
  82. package/src/runtime-env.ts +18 -0
  83. package/src/server/cookie-store.ts +52 -1
  84. package/src/server/request-context.ts +105 -2
  85. package/src/static-handler.ts +25 -3
  86. package/src/testing/cache-status.ts +166 -0
  87. package/src/testing/collect-handle.ts +63 -0
  88. package/src/testing/dispatch.ts +581 -0
  89. package/src/testing/dom.entry.ts +22 -0
  90. package/src/testing/e2e/fixture.ts +188 -0
  91. package/src/testing/e2e/index.ts +149 -0
  92. package/src/testing/e2e/matchers.ts +51 -0
  93. package/src/testing/e2e/page-helpers.ts +272 -0
  94. package/src/testing/e2e/parity.ts +387 -0
  95. package/src/testing/e2e/server.ts +195 -0
  96. package/src/testing/flight-matchers.ts +110 -0
  97. package/src/testing/flight-normalize.ts +38 -0
  98. package/src/testing/flight-runtime.d.ts +57 -0
  99. package/src/testing/flight-tree.ts +682 -0
  100. package/src/testing/flight.entry.ts +52 -0
  101. package/src/testing/flight.ts +234 -0
  102. package/src/testing/generated-routes.ts +223 -0
  103. package/src/testing/index.ts +119 -0
  104. package/src/testing/internal/context.ts +390 -0
  105. package/src/testing/internal/flight-client-globals.ts +30 -0
  106. package/src/testing/internal/seed-vars.ts +80 -0
  107. package/src/testing/render-handler.ts +360 -0
  108. package/src/testing/render-route.tsx +594 -0
  109. package/src/testing/run-loader.ts +474 -0
  110. package/src/testing/run-middleware.ts +231 -0
  111. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  112. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  113. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  114. package/src/testing/vitest-stubs/version.ts +5 -0
  115. package/src/testing/vitest.ts +305 -0
  116. package/src/types/cache-types.ts +13 -4
  117. package/src/types/error-types.ts +5 -1
  118. package/src/types/global-namespace.ts +11 -1
  119. package/src/types/handler-context.ts +16 -5
  120. package/src/browser/react/use-client-cache.ts +0 -58
@@ -16,6 +16,8 @@ import {
16
16
  getRequestContext,
17
17
  _getRequestContext,
18
18
  } from "../server/request-context.js";
19
+ import { recordRequestTags } from "./cache-tag.js";
20
+ import { reportCacheError } from "./cache-error.js";
19
21
  import { serializeSegments, deserializeSegments } from "./segment-codec.js";
20
22
  import {
21
23
  captureHandles,
@@ -28,7 +30,23 @@ import {
28
30
  DEFAULT_ROUTE_TTL,
29
31
  resolveCacheKey,
30
32
  resolveCacheStore,
33
+ resolveTagsOption,
31
34
  } from "./cache-policy.js";
35
+ import type { RequestContext } from "../server/request-context.js";
36
+
37
+ /**
38
+ * Resolve tags for a cache() boundary from its config (static array or
39
+ * function of ctx). Thin wrapper over the shared resolveTagsOption so the
40
+ * cache() DSL and loader caching resolve tags identically.
41
+ * @internal
42
+ */
43
+ export function resolveCacheTags(
44
+ config: PartialCacheOptions | false,
45
+ ctx: RequestContext | undefined,
46
+ ): string[] | undefined {
47
+ if (config === false) return undefined;
48
+ return resolveTagsOption(config.tags, ctx, "CacheScope");
49
+ }
32
50
 
33
51
  function debugCacheLog(message: string): void {
34
52
  if (INTERNAL_RANGO_DEBUG) {
@@ -253,8 +271,32 @@ export class CacheScope {
253
271
 
254
272
  const { data: cached, shouldRevalidate } = result;
255
273
 
256
- // Deserialize segments
257
- const segments = await deserializeSegments(cached.segments);
274
+ // Deserialize segments. A failure means the cached segments are corrupt/
275
+ // partial: evict the entry (self-heal - the re-render re-caches under the
276
+ // same key) and report it as corruption, distinct from a transient infra
277
+ // error (handled by the outer catch).
278
+ let segments: ResolvedSegment[];
279
+ try {
280
+ segments = await deserializeSegments(cached.segments);
281
+ } catch (error) {
282
+ reportCacheError(
283
+ error,
284
+ "cache-corrupt",
285
+ `[CacheScope] ${key}: corrupt cached segments, evicting`,
286
+ );
287
+ await store
288
+ .delete(key)
289
+ .catch((e) =>
290
+ reportCacheError(e, "cache-delete", `[CacheScope] ${key}: evict`),
291
+ );
292
+ return null;
293
+ }
294
+
295
+ // A hit serves content that was tagged at write time, so the document
296
+ // tag union must include this entry's tags for updateTag()/revalidateTag()
297
+ // to invalidate any full-page entry built on top of it. The write path
298
+ // records via cacheRoute (resolveCacheTags); the hit path records here.
299
+ recordRequestTags(cached.tags);
258
300
 
259
301
  // Replay handle data. An empty string means the route pushed no handles —
260
302
  // skip the decode entirely (the common case). Otherwise decode the
@@ -279,7 +321,7 @@ export class CacheScope {
279
321
 
280
322
  return { segments, shouldRevalidate };
281
323
  } catch (error) {
282
- console.error(`[CacheScope] Failed to lookup ${key}:`, error);
324
+ reportCacheError(error, "cache-read", `[CacheScope] lookup ${key}`);
283
325
  return null;
284
326
  }
285
327
  }
@@ -322,6 +364,10 @@ export class CacheScope {
322
364
  // Resolve cache key early (while request context is available)
323
365
  const key = await this.resolveKey(pathname, params, isIntercept);
324
366
 
367
+ // Resolve tags early (while request context is available, before waitUntil)
368
+ const tags = resolveCacheTags(this.config, requestCtx);
369
+ recordRequestTags(tags, requestCtx);
370
+
325
371
  // Check if this is a partial request (navigation) vs document request
326
372
  const isPartial = requestCtx.originalUrl.searchParams.has("_rsc_partial");
327
373
 
@@ -390,6 +436,7 @@ export class CacheScope {
390
436
  segments: serializedSegments,
391
437
  handles: encodedHandles,
392
438
  expiresAt: Date.now() + ttl * 1000,
439
+ tags,
393
440
  };
394
441
 
395
442
  if (INTERNAL_RANGO_DEBUG) {
@@ -407,7 +454,11 @@ export class CacheScope {
407
454
  );
408
455
  }
409
456
  } catch (error) {
410
- console.error(`[CacheScope] Failed to cache ${key}:`, error);
457
+ reportCacheError(
458
+ error,
459
+ "cache-write",
460
+ `[CacheScope] Failed to cache ${key}`,
461
+ );
411
462
  }
412
463
  });
413
464
  }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Cache Tag API
3
+ *
4
+ * Provides cacheTag() for tagging cached entries at runtime inside "use cache"
5
+ * functions. Tags are scoped via AsyncLocalStorage; calling cacheTag() outside
6
+ * a "use cache" execution throws.
7
+ *
8
+ * The runtime (cache-runtime.ts) wraps "use cache" execution in
9
+ * runWithCacheTagScope(), collects the runtime tags, and merges them with the
10
+ * profile/DSL tags before storing.
11
+ */
12
+
13
+ import { AsyncLocalStorage } from "node:async_hooks";
14
+ import {
15
+ _getRequestContext,
16
+ type RequestContext,
17
+ } from "../server/request-context.js";
18
+
19
+ const cacheTagStorage = new AsyncLocalStorage<Set<string>>();
20
+
21
+ /**
22
+ * Normalize a tag for storage.
23
+ *
24
+ * Returns the tag unchanged if usable, or null if it is empty/whitespace-only
25
+ * (dropped consistently in every environment - an empty tag matches nothing).
26
+ *
27
+ * Backend-specific constraints are intentionally NOT enforced here so the tag
28
+ * primitive stays backend-agnostic. In particular, the CFCacheStore
29
+ * encodeURIComponent's tags at serialization time so commas/spaces/non-Latin1
30
+ * characters cannot corrupt the comma-delimited Cloudflare Cache-Tag header or
31
+ * the HTTP marker header (it does not reject them). Keep tags short and
32
+ * low-cardinality: a tag's KV marker key must stay under Cloudflare's 512-byte
33
+ * limit, and a Cache-Tag value under 1024 bytes. The in-memory store has no
34
+ * such limitations.
35
+ *
36
+ * @internal
37
+ */
38
+ export function normalizeTag(tag: string): string | null {
39
+ if (!tag || !tag.trim()) return null;
40
+ return tag;
41
+ }
42
+
43
+ /**
44
+ * Normalize a tag collection: drop empty/whitespace-only tags so the WRITE path
45
+ * matches the invalidate path (updateTag/revalidateTag/cacheTag all normalize).
46
+ * Does not deduplicate - callers that need that wrap with a Set.
47
+ *
48
+ * @internal
49
+ */
50
+ export function normalizeTags(tags: Iterable<string>): string[] {
51
+ const out: string[] = [];
52
+ for (const tag of tags) {
53
+ const normalized = normalizeTag(tag);
54
+ if (normalized !== null) out.push(normalized);
55
+ }
56
+ return out;
57
+ }
58
+
59
+ /**
60
+ * Tag the current "use cache" entry for later invalidation via
61
+ * updateTag() / revalidateTag().
62
+ *
63
+ * Must be called inside a function marked with "use cache".
64
+ * Tags are additive - multiple calls accumulate.
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * async function getProduct(ctx) {
69
+ * "use cache";
70
+ * cacheTag(`product:${ctx.params.id}`, "products");
71
+ * return db.getProduct(ctx.params.id);
72
+ * }
73
+ * ```
74
+ */
75
+ export function cacheTag(...tags: string[]): void {
76
+ const store = cacheTagStorage.getStore();
77
+ if (!store) {
78
+ throw new Error('cacheTag() must be called inside a "use cache" function.');
79
+ }
80
+ for (const tag of tags) {
81
+ const normalized = normalizeTag(tag);
82
+ if (normalized === null) {
83
+ if (process.env.NODE_ENV !== "production") {
84
+ console.warn(`[cacheTag] Ignoring empty or whitespace-only tag.`);
85
+ }
86
+ continue;
87
+ }
88
+ store.add(normalized);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Record `tags` into the request-scoped tag set (ctx._requestTags), the union of
94
+ * every cache tag resolved while producing the response. The document cache reads
95
+ * this after the render settles so a full-page entry is tagged with everything its
96
+ * content used, making it invalidatable by updateTag()/revalidateTag().
97
+ *
98
+ * Called at the tag-resolution sites: "use cache" stores (cache-runtime, both the
99
+ * miss and read/hit paths), loader cache (cache-policy/loader-cache), and segment
100
+ * cache() (cache-scope). Writes the field directly (not via ctx.set()) so it does
101
+ * not trip the cache-scope side-effect guard, mirroring cacheTag() itself.
102
+ *
103
+ * @internal
104
+ */
105
+ export function recordRequestTags(
106
+ tags: Iterable<string> | undefined,
107
+ ctx: RequestContext | undefined = _getRequestContext(),
108
+ ): void {
109
+ if (!tags) return;
110
+ const set = ctx?._requestTags;
111
+ if (!set) return;
112
+ for (const tag of tags) {
113
+ const normalized = normalizeTag(tag);
114
+ if (normalized !== null) set.add(normalized);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Run a function within a cache tag scope. Any cacheTag() calls inside `fn`
120
+ * accumulate into the returned Set.
121
+ *
122
+ * The returned Set is the LIVE reference - the caller must await `result`
123
+ * before reading `tags`, because an async cached function may call cacheTag()
124
+ * after an await boundary.
125
+ *
126
+ * @internal Used by cache-runtime.ts to wrap "use cache" execution.
127
+ */
128
+ export function runWithCacheTagScope<T>(fn: () => T): {
129
+ result: T;
130
+ tags: Set<string>;
131
+ } {
132
+ const tagSet = new Set<string>();
133
+ const result = cacheTagStorage.run(tagSet, fn);
134
+ return { result, tags: tagSet };
135
+ }