@rangojs/router 0.0.0-experimental.110 → 0.0.0-experimental.112

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 (65) hide show
  1. package/dist/bin/rango.js +41 -37
  2. package/dist/vite/index.js +144 -191
  3. package/package.json +17 -14
  4. package/skills/handler-use/SKILL.md +1 -1
  5. package/skills/rango/SKILL.md +20 -0
  6. package/src/browser/action-coordinator.ts +53 -36
  7. package/src/browser/event-controller.ts +42 -66
  8. package/src/browser/navigation-bridge.ts +4 -0
  9. package/src/browser/navigation-client.ts +12 -15
  10. package/src/browser/navigation-store.ts +7 -8
  11. package/src/browser/navigation-transaction.ts +7 -21
  12. package/src/browser/partial-update.ts +8 -16
  13. package/src/browser/react/NavigationProvider.tsx +29 -40
  14. package/src/browser/react/use-params.ts +3 -4
  15. package/src/browser/response-adapter.ts +25 -0
  16. package/src/browser/rsc-router.tsx +16 -2
  17. package/src/browser/server-action-bridge.ts +23 -30
  18. package/src/browser/types.ts +2 -0
  19. package/src/build/generate-manifest.ts +29 -31
  20. package/src/build/generate-route-types.ts +2 -0
  21. package/src/build/route-types/router-processing.ts +37 -9
  22. package/src/build/runtime-discovery.ts +9 -20
  23. package/src/decode-loader-results.ts +36 -0
  24. package/src/errors.ts +29 -0
  25. package/src/index.rsc.ts +1 -0
  26. package/src/index.ts +1 -0
  27. package/src/response-utils.ts +9 -0
  28. package/src/route-content-wrapper.tsx +6 -28
  29. package/src/route-definition/dsl-helpers.ts +231 -259
  30. package/src/route-definition/helper-factories.ts +29 -139
  31. package/src/route-definition/use-item-types.ts +32 -0
  32. package/src/route-types.ts +19 -41
  33. package/src/router/content-negotiation.ts +15 -2
  34. package/src/router/intercept-resolution.ts +4 -18
  35. package/src/router/match-result.ts +32 -30
  36. package/src/router/middleware.ts +46 -78
  37. package/src/router/preview-match.ts +3 -1
  38. package/src/router/request-classification.ts +4 -28
  39. package/src/rsc/handler.ts +20 -65
  40. package/src/rsc/helpers.ts +3 -2
  41. package/src/rsc/origin-guard.ts +28 -10
  42. package/src/rsc/response-route-handler.ts +32 -52
  43. package/src/rsc/rsc-rendering.ts +27 -53
  44. package/src/rsc/runtime-warnings.ts +9 -10
  45. package/src/rsc/server-action.ts +13 -37
  46. package/src/rsc/ssr-setup.ts +16 -0
  47. package/src/segment-system.tsx +5 -39
  48. package/src/server/context.ts +76 -35
  49. package/src/urls/include-helper.ts +10 -53
  50. package/src/urls/index.ts +0 -3
  51. package/src/urls/path-helper.ts +17 -52
  52. package/src/urls/pattern-types.ts +2 -19
  53. package/src/urls/response-types.ts +20 -19
  54. package/src/urls/type-extraction.ts +20 -115
  55. package/src/urls/urls-function.ts +1 -5
  56. package/src/vite/discovery/discover-routers.ts +10 -22
  57. package/src/vite/discovery/route-types-writer.ts +38 -82
  58. package/src/vite/plugins/cjs-to-esm.ts +3 -7
  59. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  60. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  61. package/src/vite/plugins/expose-internal-ids.ts +34 -62
  62. package/src/vite/plugins/version-injector.ts +2 -12
  63. package/src/vite/router-discovery.ts +71 -26
  64. package/src/vite/utils/shared-utils.ts +13 -1
  65. package/src/browser/action-response-classifier.ts +0 -99
@@ -1,5 +1,5 @@
1
1
  import type MagicString from "magic-string";
2
- import { hashId } from "../expose-id-utils.js";
2
+ import { makeStubId } from "../expose-id-utils.js";
3
3
  import type { CreateExportBinding } from "./types.js";
4
4
  import { isExportOnlyFile } from "./export-analysis.js";
5
5
 
@@ -33,7 +33,7 @@ export function generateClientLoaderStubs(
33
33
 
34
34
  for (const binding of bindings) {
35
35
  for (const name of binding.exportNames) {
36
- const loaderId = isBuild ? hashId(filePath, name) : `${filePath}#${name}`;
36
+ const loaderId = makeStubId(filePath, name, isBuild);
37
37
  lines.push(
38
38
  `export const ${name} = { __brand: "loader", $$id: "${loaderId}" };`,
39
39
  );
@@ -54,9 +54,7 @@ export function transformLoaders(
54
54
  for (const binding of bindings) {
55
55
  const exportName = binding.exportNames[0];
56
56
 
57
- const loaderId = isBuild
58
- ? hashId(filePath, exportName)
59
- : `${filePath}#${exportName}`;
57
+ const loaderId = makeStubId(filePath, exportName, isBuild);
60
58
 
61
59
  // Inject $$id as hidden third parameter.
62
60
  // createLoader(fn) -> createLoader(fn, undefined, "id")
@@ -39,7 +39,6 @@ import {
39
39
  transformHandles,
40
40
  transformLocationState,
41
41
  generateWholeFileStubs,
42
- generateExprStubs,
43
42
  stubHandlerExprs,
44
43
  transformHandlerIds,
45
44
  } from "./expose-ids/handler-transform.js";
@@ -424,17 +423,6 @@ ${lazyImports.join(",\n")}
424
423
  if (wholeFile) return wholeFile;
425
424
  }
426
425
 
427
- // --- PrerenderHandler: RSC build module tracking ---
428
- if (hasPrerenderHandlerCode && isRscEnv && isBuild) {
429
- const fnNames = getFnNames(PRERENDER_CONFIG.fnName);
430
- const exportNames = getBindings(code, fnNames).map(
431
- (b) => b.exportNames[0],
432
- );
433
- if (exportNames.length > 0) {
434
- prerenderHandlerModules.set(id, exportNames);
435
- }
436
- }
437
-
438
426
  // --- Inline handler extraction to virtual modules ---
439
427
  // Runs before stubs/tracking so inline calls become imports, then
440
428
  // the existing regex fast path handles both the original file's
@@ -710,14 +698,27 @@ ${lazyImports.join(",\n")}
710
698
  }
711
699
  }
712
700
 
713
- // --- StaticHandler: RSC build module tracking ---
714
- if (hasStaticHandlerCode && isRscEnv && isBuild) {
715
- const fnNames = getFnNames(STATIC_CONFIG.fnName);
716
- const exportNames = getBindings(code, fnNames).map(
717
- (b) => b.exportNames[0],
718
- );
719
- if (exportNames.length > 0) {
720
- staticHandlerModules.set(id, exportNames);
701
+ // RSC build module tracking (prerender + static), consumed via the
702
+ // plugin API for prerender freezing. Export-binding sets are invariant
703
+ // across the inline-extraction loop, so tracking both here is equivalent
704
+ // to the pre-extraction prerender tracking this replaces.
705
+ if (isRscEnv && isBuild) {
706
+ const trackTypes: Array<
707
+ [boolean, HandlerTransformConfig, Map<string, string[]>]
708
+ > = [
709
+ [
710
+ hasPrerenderHandlerCode,
711
+ PRERENDER_CONFIG,
712
+ prerenderHandlerModules,
713
+ ],
714
+ [hasStaticHandlerCode, STATIC_CONFIG, staticHandlerModules],
715
+ ];
716
+ for (const [has, cfg, trackMap] of trackTypes) {
717
+ if (!has) continue;
718
+ const exportNames = getBindings(code, getFnNames(cfg.fnName)).map(
719
+ (b) => b.exportNames[0],
720
+ );
721
+ if (exportNames.length > 0) trackMap.set(id, exportNames);
721
722
  }
722
723
  }
723
724
 
@@ -758,48 +759,19 @@ ${lazyImports.join(",\n")}
758
759
  isBuild,
759
760
  ) || changed;
760
761
  }
761
- if (hasPrerenderHandlerCode) {
762
- const fnNames = getFnNames(PRERENDER_CONFIG.fnName);
763
- const bindings = getBindings(code, fnNames);
764
- if (isRscEnv) {
765
- changed =
766
- transformHandlerIds(
767
- PRERENDER_CONFIG,
768
- bindings,
769
- s,
770
- filePath,
771
- isBuild,
772
- ) || changed;
773
- } else {
774
- // Non-RSC mixed-export file: replace Prerender() calls with stubs
775
- // on the shared MagicString so sourcemaps stay accurate.
776
- changed =
777
- stubHandlerExprs(
778
- PRERENDER_CONFIG,
779
- bindings,
780
- s,
781
- filePath,
782
- isBuild,
783
- ) || changed;
784
- }
785
- }
786
- if (hasStaticHandlerCode) {
787
- const fnNames = getFnNames(STATIC_CONFIG.fnName);
788
- const bindings = getBindings(code, fnNames);
789
- if (isRscEnv) {
790
- changed =
791
- transformHandlerIds(
792
- STATIC_CONFIG,
793
- bindings,
794
- s,
795
- filePath,
796
- isBuild,
797
- ) || changed;
798
- } else {
799
- changed =
800
- stubHandlerExprs(STATIC_CONFIG, bindings, s, filePath, isBuild) ||
801
- changed;
802
- }
762
+ // Prerender + Static share the RSC inject-id vs non-RSC stub dispatch.
763
+ // Call sites are disjoint (distinct fnNames), so loop order is irrelevant.
764
+ const finalHandlerConfigs = [
765
+ hasPrerenderHandlerCode && PRERENDER_CONFIG,
766
+ hasStaticHandlerCode && STATIC_CONFIG,
767
+ ].filter((c): c is HandlerTransformConfig => !!c);
768
+ for (const cfg of finalHandlerConfigs) {
769
+ const bindings = getBindings(code, getFnNames(cfg.fnName));
770
+ changed =
771
+ (isRscEnv
772
+ ? transformHandlerIds(cfg, bindings, s, filePath, isBuild)
773
+ : stubHandlerExprs(cfg, bindings, s, filePath, isBuild)) ||
774
+ changed;
803
775
  }
804
776
 
805
777
  if (!changed) return;
@@ -1,6 +1,7 @@
1
1
  import type { Plugin } from "vite";
2
2
  import { resolve } from "node:path";
3
3
  import * as Vite from "vite";
4
+ import { resolveRscEntryFromConfig } from "../utils/shared-utils.js";
4
5
 
5
6
  /**
6
7
  * Plugin that auto-injects VERSION and routes-manifest into custom entry.rsc files.
@@ -20,18 +21,7 @@ export function createVersionInjectorPlugin(
20
21
 
21
22
  configResolved(config) {
22
23
  let entryPath = rscEntryPath;
23
- // Cloudflare preset: read entry from resolved environment config.
24
- // The @cloudflare/vite-plugin reads wrangler config (toml/json/jsonc)
25
- // and sets optimizeDeps.entries on the RSC environment.
26
- if (!entryPath) {
27
- const rscEnvConfig = (config.environments as any)?.["rsc"];
28
- const entries = rscEnvConfig?.optimizeDeps?.entries;
29
- if (typeof entries === "string") {
30
- entryPath = entries;
31
- } else if (Array.isArray(entries) && entries.length > 0) {
32
- entryPath = entries[0];
33
- }
34
- }
24
+ if (!entryPath) entryPath = resolveRscEntryFromConfig(config);
35
25
  if (entryPath) {
36
26
  resolvedEntryPath = resolve(config.root, entryPath);
37
27
  }
@@ -52,6 +52,7 @@ import {
52
52
  import { postprocessBundle } from "./discovery/bundle-postprocess.js";
53
53
  import { createDiscoveryGate } from "./discovery/gate-state.js";
54
54
  import { resetStagedBuildAssets } from "./utils/prerender-utils.js";
55
+ import { resolveRscEntryFromConfig } from "./utils/shared-utils.js";
55
56
  import {
56
57
  pickForwardedRunnerConfig,
57
58
  selectForwardableResolvePlugins,
@@ -347,17 +348,10 @@ export function createRouterDiscoveryPlugin(
347
348
  if (!s.resolvedEntryPath && opts?.routerPathRef?.path) {
348
349
  s.resolvedEntryPath = opts.routerPathRef.path;
349
350
  }
350
- // Cloudflare preset: read entry from resolved environment config.
351
- // The @cloudflare/vite-plugin reads wrangler config (toml/json/jsonc)
352
- // and sets optimizeDeps.entries on the RSC environment.
351
+ // Cloudflare preset: entry comes from the resolved RSC env config.
353
352
  if (!s.resolvedEntryPath) {
354
- const rscEnvConfig = (config.environments as any)?.["rsc"];
355
- const entries = rscEnvConfig?.optimizeDeps?.entries;
356
- if (typeof entries === "string") {
357
- s.resolvedEntryPath = entries;
358
- } else if (Array.isArray(entries) && entries.length > 0) {
359
- s.resolvedEntryPath = entries[0];
360
- }
353
+ const entry = resolveRscEntryFromConfig(config);
354
+ if (entry) s.resolvedEntryPath = entry;
361
355
  }
362
356
  // Generate combined named-routes.gen.ts from static source parsing.
363
357
  // Runs before the dev server starts so the gen file exists immediately for IDE.
@@ -794,20 +788,15 @@ export function createRouterDiscoveryPlugin(
794
788
  if (s.mergedRouteTrie && serverMod.setRouteTrie) {
795
789
  serverMod.setRouteTrie(s.mergedRouteTrie);
796
790
  }
797
- if (serverMod.setRouterManifest) {
798
- for (const [routerId, manifest] of s.perRouterManifestDataMap) {
799
- serverMod.setRouterManifest(routerId, manifest);
800
- }
801
- }
802
- if (serverMod.setRouterTrie) {
803
- for (const [routerId, trie] of s.perRouterTrieMap) {
804
- serverMod.setRouterTrie(routerId, trie);
805
- }
806
- }
807
- if (serverMod.setRouterPrecomputedEntries) {
808
- for (const [routerId, entries] of s.perRouterPrecomputedMap) {
809
- serverMod.setRouterPrecomputedEntries(routerId, entries);
810
- }
791
+ const perRouterSetters: Array<[Map<string, any>, string]> = [
792
+ [s.perRouterManifestDataMap, "setRouterManifest"],
793
+ [s.perRouterTrieMap, "setRouterTrie"],
794
+ [s.perRouterPrecomputedMap, "setRouterPrecomputedEntries"],
795
+ ];
796
+ for (const [map, fn] of perRouterSetters) {
797
+ const setter = serverMod[fn];
798
+ if (typeof setter !== "function") continue;
799
+ for (const [routerId, value] of map) setter(routerId, value);
811
800
  }
812
801
  };
813
802
 
@@ -1024,6 +1013,16 @@ export function createRouterDiscoveryPlugin(
1024
1013
  );
1025
1014
  s.lastDiscoveryError = null;
1026
1015
  }
1016
+ // Cloudflare dev: on a successful cycle drop the workerd runner's
1017
+ // cached worker-entry chain so the next request re-evaluates
1018
+ // createRouter() with the new routes. Fired here in the work path
1019
+ // (not the caller's .then()) so a queued follow-up cycle that
1020
+ // succeeds after an earlier failed cycle still reloads:
1021
+ // runRefreshCycle recurses queued work without awaiting it, so the
1022
+ // original call already resolved on the failed cycle. A failed
1023
+ // cycle throws above and never reaches here, so a broken edit
1024
+ // never reloads the worker onto bad source.
1025
+ if (rscEnv && !rscEnv.runner) forceCloudflareWorkerReload(rscEnv);
1027
1026
  } catch (err: any) {
1028
1027
  s.lastDiscoveryError = {
1029
1028
  message: err?.message ?? String(err),
@@ -1045,6 +1044,49 @@ export function createRouterDiscoveryPlugin(
1045
1044
  });
1046
1045
  };
1047
1046
 
1047
+ // Cloudflare dev only. workerd serves every request through the
1048
+ // runner-worker singleton, which re-resolves the worker entry per
1049
+ // request via runner.import("virtual:cloudflare/worker-entry"). The
1050
+ // route table lives in the user's createRouter() instance, captured
1051
+ // when that entry chain (entry -> router -> urls) was last evaluated
1052
+ // and then cached in the runner's evaluatedModules. The route-file
1053
+ // watcher refreshes discovery + types on the Node side, but the worker
1054
+ // keeps serving the cached (stale) router: route-definition modules
1055
+ // have no import.meta.hot boundary, so Vite never sends the worker an
1056
+ // HMR update for them and the entry chain is never evicted.
1057
+ //
1058
+ // Fix: after discovery completes, (1) invalidate the worker env's
1059
+ // Node-side module graph, then (2) send a full-reload to the worker.
1060
+ // Step (2) alone is insufficient: the full-reload handler clears the
1061
+ // runner's evaluatedModules and re-imports entrypoints, but each
1062
+ // re-import fetches the module back through this Node-side graph, which
1063
+ // still holds the pre-edit transform of urls.tsx — so createRouter()
1064
+ // rebuilds the stale route table and the new route 404s/hits the
1065
+ // catch-all. Invalidating the graph forces a fresh transform on
1066
+ // re-fetch (the same mechanism refreshTempRscEnv uses for discovery),
1067
+ // so the re-import re-runs createRouter() with the new routes. This is
1068
+ // the programmatic equivalent of the dev-server "r + enter" restart,
1069
+ // scoped to the worker environment instead of tearing down the server.
1070
+ const forceCloudflareWorkerReload = (rscEnv: any) => {
1071
+ if (!rscEnv?.hot) return;
1072
+ try {
1073
+ const graph = rscEnv.moduleGraph;
1074
+ if (graph?.invalidateAll) {
1075
+ graph.invalidateAll();
1076
+ debugDiscovery?.("hmr: invalidated workerd rsc module graph");
1077
+ }
1078
+ rscEnv.hot.send({ type: "full-reload" });
1079
+ debugDiscovery?.(
1080
+ "hmr: forced workerd rsc env reload (full-reload)",
1081
+ );
1082
+ } catch (err: any) {
1083
+ debugDiscovery?.(
1084
+ "hmr: workerd reload failed: %s",
1085
+ err?.message ?? err,
1086
+ );
1087
+ }
1088
+ };
1089
+
1048
1090
  const scheduleRouteRegeneration = () => {
1049
1091
  clearTimeout(routeChangeTimer);
1050
1092
  routeChangeTimer = setTimeout(() => {
@@ -1083,12 +1125,15 @@ export function createRouterDiscoveryPlugin(
1083
1125
  // routes that the static parser cannot resolve. Resolves the
1084
1126
  // discovery gate when complete.
1085
1127
  if (s.perRouterManifests.length > 0) {
1128
+ // The cloudflare workerd reload fires inside refreshRuntimeDiscovery
1129
+ // on the successful cycle (see forceCloudflareWorkerReload call
1130
+ // there) so queued follow-up cycles also trigger it.
1086
1131
  refreshRuntimeDiscovery().catch((err: any) => {
1087
1132
  console.warn(
1088
1133
  `[rango] Runtime re-discovery error: ${err.message}`,
1089
1134
  );
1090
- // Even on error, unblock the gate so workerd's reload
1091
- // doesn't hang indefinitely against the previous manifest.
1135
+ // Even on error, unblock the gate so workerd's reload doesn't
1136
+ // hang indefinitely against the previous manifest.
1092
1137
  resolveDiscoveryGate();
1093
1138
  });
1094
1139
  }
@@ -1,4 +1,4 @@
1
- import type { Plugin } from "vite";
1
+ import type { Plugin, ResolvedConfig } from "vite";
2
2
  import * as Vite from "vite";
3
3
  import { getPublishedPackageName } from "./package-resolution.js";
4
4
  import { performanceTracksOptimizeDepsPlugin } from "../plugins/performance-tracks.js";
@@ -10,6 +10,18 @@ import {
10
10
  VIRTUAL_IDS,
11
11
  } from "../plugins/virtual-entries.js";
12
12
 
13
+ // Cloudflare preset: @cloudflare/vite-plugin sets optimizeDeps.entries (string
14
+ // or array) on the rsc environment. Single source for both the discovery plugin
15
+ // and the version injector so they target the same entry.
16
+ export function resolveRscEntryFromConfig(
17
+ config: ResolvedConfig,
18
+ ): string | undefined {
19
+ const entries = (config.environments as any)?.["rsc"]?.optimizeDeps?.entries;
20
+ if (typeof entries === "string") return entries;
21
+ if (Array.isArray(entries) && entries.length > 0) return entries[0];
22
+ return undefined;
23
+ }
24
+
13
25
  /**
14
26
  * Rolldown plugin to provide the version virtual module during dependency
15
27
  * optimization. Vite 8 optimizes deps with Rolldown (a Rollup-style plugin
@@ -1,99 +0,0 @@
1
- /**
2
- * Discriminated union of post-reconciliation action response scenarios.
3
- *
4
- * Error and full-update-unsupported are handled inline in the bridge
5
- * before reconciliation. This classifier only runs for partial responses
6
- * that have been successfully reconciled.
7
- */
8
- export type ActionScenario =
9
- | {
10
- type: "navigated-away";
11
- historyKeyChanged: boolean;
12
- onInterceptRoute: boolean;
13
- }
14
- | { type: "hmr-missing" }
15
- | { type: "consolidation-needed"; segmentIds: string[] }
16
- | { type: "concurrent-skip"; otherFetchingCount: number }
17
- | { type: "normal" };
18
-
19
- /**
20
- * Pure data inputs for classifying a partial action response.
21
- * All values come from the bridge but no browser APIs or side effects.
22
- */
23
- export interface ClassifierInput {
24
- /** window.location.pathname captured at action start */
25
- actionStartPathname: string;
26
- /** window.location.pathname at classification time */
27
- currentPathname: string;
28
- /** window.history.state?.key captured at action start */
29
- actionStartLocationKey: string | undefined;
30
- /** window.history.state?.key at classification time */
31
- currentLocationKey: string | undefined;
32
- /** Number of segments after reconciliation */
33
- reconciledSegmentCount: number;
34
- /** Number of matched segment IDs from server */
35
- matchedCount: number;
36
- /** Segment IDs needing consolidation (from concurrent action tracking) */
37
- consolidationSegments: string[] | null;
38
- /** Number of other actions still in "fetching" phase */
39
- otherFetchingActionCount: number;
40
- /** Current intercept source URL (null when not on intercept route) */
41
- currentInterceptSource: string | null;
42
- }
43
-
44
- /**
45
- * Classify a partial action response into one of 5 post-reconciliation
46
- * scenarios.
47
- *
48
- * Called after error and full-update cases are handled inline by the bridge.
49
- * The classification order matches the priority chain:
50
- * 1. User navigated away during action
51
- * 2. HMR missing segments (fewer reconciled than matched)
52
- * 3. Consolidation needed (concurrent actions finished)
53
- * 4. Concurrent skip (other actions still fetching)
54
- * 5. Normal (single action, no issues)
55
- *
56
- * This is a pure function with no side effects - the bridge handles
57
- * all UI updates, store mutations, and network requests based on the
58
- * returned scenario.
59
- */
60
- export function classifyActionResponse(input: ClassifierInput): ActionScenario {
61
- // Check if user navigated away during the action
62
- const userNavigatedAway =
63
- input.currentPathname !== input.actionStartPathname ||
64
- input.currentLocationKey !== input.actionStartLocationKey;
65
-
66
- if (userNavigatedAway) {
67
- const historyKeyChanged =
68
- input.currentLocationKey !== input.actionStartLocationKey;
69
- return {
70
- type: "navigated-away",
71
- historyKeyChanged,
72
- onInterceptRoute: input.currentInterceptSource !== null,
73
- };
74
- }
75
-
76
- // HMR resilience: segments missing after reconciliation
77
- if (input.reconciledSegmentCount < input.matchedCount) {
78
- return { type: "hmr-missing" };
79
- }
80
-
81
- // Consolidation needed for concurrent actions
82
- if (input.consolidationSegments && input.consolidationSegments.length > 0) {
83
- return {
84
- type: "consolidation-needed",
85
- segmentIds: input.consolidationSegments,
86
- };
87
- }
88
-
89
- // Other actions still fetching - skip UI update
90
- if (input.otherFetchingActionCount > 0) {
91
- return {
92
- type: "concurrent-skip",
93
- otherFetchingCount: input.otherFetchingActionCount,
94
- };
95
- }
96
-
97
- // Normal single-action completion
98
- return { type: "normal" };
99
- }