@jk2908/solas 0.3.8 → 0.4.0

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 (46) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +66 -6
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +16 -2
  5. package/dist/internal/browser-router/link.d.ts +1 -1
  6. package/dist/internal/browser-router/link.js +1 -1
  7. package/dist/internal/browser-router/router.d.ts +2 -165
  8. package/dist/internal/browser-router/router.js +3 -99
  9. package/dist/internal/browser-router/shared.d.ts +169 -0
  10. package/dist/internal/browser-router/shared.js +71 -0
  11. package/dist/internal/browser-router/use-router.d.ts +1 -1
  12. package/dist/internal/codegen/environments.js +5 -4
  13. package/dist/internal/env/rsc.d.ts +2 -2
  14. package/dist/internal/env/rsc.js +82 -22
  15. package/dist/internal/http-router/create-http-router.d.ts +1 -1
  16. package/dist/internal/http-router/create-http-router.js +4 -2
  17. package/dist/internal/http-router/router.d.ts +4 -14
  18. package/dist/internal/http-router/router.js +32 -59
  19. package/dist/internal/navigation/http-exception.d.ts +4 -4
  20. package/dist/internal/navigation/http-exception.js +4 -5
  21. package/dist/internal/postbuild.d.ts +1 -0
  22. package/dist/{cli/build.js → internal/postbuild.js} +13 -48
  23. package/dist/internal/prerender.d.ts +4 -19
  24. package/dist/internal/prerender.js +8 -98
  25. package/dist/internal/public-files.d.ts +18 -0
  26. package/dist/internal/public-files.js +63 -0
  27. package/dist/internal/resolver.d.ts +23 -23
  28. package/dist/internal/server/actions.d.ts +2 -5
  29. package/dist/internal/server/actions.js +4 -35
  30. package/dist/internal/server/csrf.d.ts +14 -0
  31. package/dist/internal/server/csrf.js +98 -0
  32. package/dist/router.d.ts +1 -0
  33. package/dist/router.js +1 -0
  34. package/dist/solas.d.ts +12 -1
  35. package/dist/solas.js +116 -1
  36. package/dist/types.d.ts +8 -3
  37. package/dist/utils/base-path.d.ts +14 -0
  38. package/dist/utils/base-path.js +85 -0
  39. package/package.json +3 -6
  40. package/dist/cli/build.d.ts +0 -7
  41. package/dist/cli/dev.d.ts +0 -4
  42. package/dist/cli/dev.js +0 -13
  43. package/dist/cli/preview.d.ts +0 -1
  44. package/dist/cli/preview.js +0 -47
  45. package/dist/cli.d.ts +0 -2
  46. package/dist/cli.js +0 -28
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 - 2026-05-11
4
+
5
+ - Added CSRF protection for server actions and `+endpoint` handlers, plus a new `trustedOrigins` config option for tightly scoped cross-origin browser submissions. The checks are proxy-aware and use browser request headers when available.
6
+ - Added Vite `base` support across server routing, prerendering, and browser navigation, so apps mounted under a subpath resolve routes and generated asset URLs correctly.
7
+ - Changed static file handling so copied `public` files are served from the application root, while framework-generated files now live under the reserved `/_solas/*` path.
8
+ - Breaking: removed the `solas` CLI compatibility layer and switched the documented app scripts to Bun-backed Vite commands (`bunx --bun vite dev`, `build`, and `preview`).
9
+ - Moved Solas post-build work into the Vite plugin lifecycle, so prerendering, runtime manifest emission, sitemap generation, and precompression now run after the full app build instead of through an outer CLI wrapper.
10
+ - Added `Solas.Runtime.Manifest` and `Solas.Runtime.loadManifest(...)` for runtime artifact and public-file lookups, while keeping artifact-specific manifest types and helpers under `Prerender.Artifact`. The runtime manifest now lives at `dist/.solas/runtime-manifest.json` instead of under `.solas/ppr`.
11
+ - Stopped serialising stack traces in `HttpExceptionLike`, so server-rendered error payloads no longer include stacks.
12
+
13
+ ## 0.3.9 - 2026-05-07
14
+
15
+ - Split shared `BrowserRouter` navigation types and target-building helpers into a dedicated internal module, so generated environments and type-only imports no longer need to pull through the full browser router runtime.
16
+ - Made the `solas()` plugin config argument optional.
17
+
3
18
  ## 0.3.8 - 2026-04-30
4
19
 
5
20
  - Improved route module type safety for params, metadata, and static params, and ensured HTTP error boundaries receive route params too.
package/README.md CHANGED
@@ -288,6 +288,32 @@ export default defineConfig({
288
288
  })
289
289
  ```
290
290
 
291
+ ### `trustedOrigins`
292
+
293
+ Use `trustedOrigins` to allow specific origins to make cross-origin browser submissions to your app.
294
+
295
+ Default: `[]`
296
+
297
+ Solas protects server actions and `+endpoint` handlers against CSRF.
298
+
299
+ Server actions are always `POST` requests. `+endpoint` handlers are protected on `POST`, `PUT`, `PATCH`, and `DELETE` requests.
300
+
301
+ By default, only same-origin browser requests are allowed. Add a trusted origin when a third-party service needs to submit through the user's browser, such as a payment gateway or identity provider.
302
+
303
+ Each value must be a complete origin including protocol:
304
+
305
+ ```ts
306
+ export default defineConfig({
307
+ plugins: [
308
+ solas({
309
+ trustedOrigins: ['https://payments.example.com', 'https://login.example.com'],
310
+ }),
311
+ ],
312
+ })
313
+ ```
314
+
315
+ Only add origins you completely trust. These origins are treated as allowed browser sources for unsafe requests.
316
+
291
317
  ### `sitemap`
292
318
 
293
319
  Use `sitemap` to generate a `sitemap.xml` at build time.
@@ -372,15 +398,49 @@ Add scripts to your app:
372
398
  ```json
373
399
  {
374
400
  "scripts": {
375
- "dev": "solas dev",
376
- "build": "solas build",
377
- "preview": "solas preview"
401
+ "dev": "bunx --bun vite dev",
402
+ "build": "bunx --bun vite build",
403
+ "preview": "bunx --bun vite preview"
378
404
  }
379
405
  }
380
406
  ```
381
407
 
382
408
  ## Commands
383
409
 
384
- - `solas dev` starts the development server.
385
- - `solas build` creates a production build, prerenders configured routes, and writes compressed assets when enabled.
386
- - `solas preview` serves the built app for local verification.
410
+ - `bunx --bun vite dev` starts the development server.
411
+ - `bunx --bun vite build` creates a production build. Solas finalizes that build by prerendering configured routes, writing the runtime manifest, generating `sitemap.xml` when enabled, and precompressing output when enabled.
412
+ - `bunx --bun vite preview` serves the built app for local verification.
413
+
414
+ ## Security
415
+
416
+ ### CSRF
417
+
418
+ Solas protects server actions and `+endpoint` handlers against CSRF.
419
+
420
+ Server actions are always `POST` requests. `+endpoint` handlers are protected on browser-initiated `POST`, `PUT`, `PATCH`, and `DELETE` requests.
421
+
422
+ By default, browser requests must be same-origin.
423
+
424
+ When available, Solas checks browser provenance using:
425
+
426
+ - `Sec-Fetch-Site`
427
+ - `Origin`
428
+ - `Referer`
429
+
430
+ Solas also considers the effective request origin when your app is behind a proxy by using `X-Forwarded-Host`, `X-Forwarded-Proto`, and `config.url` when present.
431
+
432
+ If you need to allow a trusted third-party browser POST source, configure it explicitly:
433
+
434
+ ```ts
435
+ export default defineConfig({
436
+ plugins: [
437
+ solas({
438
+ trustedOrigins: ['https://payments.example.com'],
439
+ }),
440
+ ],
441
+ })
442
+ ```
443
+
444
+ Requests from non-browser callers that do not send browser provenance headers are allowed by default, so typical server-to-server integrations and webhooks continue to work.
445
+
446
+ Cookie-backed app mutations should keep the default same-origin protection and only use `trustedOrigins` for narrowly scoped integrations that genuinely need cross-origin browser submissions.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type PluginOption } from 'vite';
2
2
  import type { PluginConfig } from './types.js';
3
- declare function solas(c: PluginConfig): PluginOption[];
3
+ declare function solas(c?: PluginConfig): PluginOption[];
4
4
  export default solas;
5
5
  export type * from './solas.d.ts';
6
6
  export { Solas } from './solas.js';
package/dist/index.js CHANGED
@@ -12,17 +12,20 @@ import { writeBrowserEntry, writeRSCEntry, writeSSREntry, } from './internal/cod
12
12
  import { writeManifest } from './internal/codegen/manifest.js';
13
13
  import { writeMaps } from './internal/codegen/maps.js';
14
14
  import { writeTypes } from './internal/codegen/types.js';
15
+ import { postbuild } from './internal/postbuild.js';
16
+ import { collect as collectPublicFiles } from './internal/public-files.js';
15
17
  import { Solas } from './solas.js';
16
18
  const DEFAULT_CONFIG = {
17
19
  precompress: true,
18
20
  prerender: false,
21
+ trustedOrigins: [],
19
22
  trailingSlash: 'never',
20
23
  };
21
24
  function solas(c) {
22
25
  const config = Solas.Config.validate({
23
26
  ...DEFAULT_CONFIG,
24
27
  ...c,
25
- url: c.url ?? process.env.VITE_APP_URL?.toString(),
28
+ url: c?.url ?? process.env.VITE_APP_URL?.toString(),
26
29
  });
27
30
  if (config.logger?.level)
28
31
  Logger.defaultLevel = config.logger.level;
@@ -219,7 +222,11 @@ function solas(c) {
219
222
  }
220
223
  viteConfig.build ??= {};
221
224
  viteConfig.build.outDir = Solas.Config.OUT_DIR;
225
+ // keep framework files under one reserved url prefix
226
+ viteConfig.build.assetsDir = Solas.Config.ASSETS_DIR;
222
227
  viteConfig.build.emptyOutDir = true;
228
+ // let users move the source public folder if they want
229
+ viteConfig.publicDir ??= Solas.Config.PUBLIC_DIR;
223
230
  viteConfig.server ??= {};
224
231
  viteConfig.server.port = config.port ?? viteConfig.server.port ?? 8787;
225
232
  viteConfig.define ??= {};
@@ -293,13 +300,20 @@ function solas(c) {
293
300
  // write build manifest
294
301
  const generatedDir = path.join(process.cwd(), Solas.Config.GENERATED_DIR);
295
302
  await Bun.write(path.join(generatedDir, 'build.json'), JSON.stringify({
303
+ base: resolvedViteConfig?.base ?? '/',
304
+ publicFiles: await collectPublicFiles(resolvedViteConfig?.publicDir),
296
305
  prerenderRoutes: Array.from(buildContext.prerenderRoutes),
297
306
  sitemapRoutes,
298
307
  precompress: config.precompress,
299
308
  trailingSlash: config.trailingSlash,
300
309
  url: config.url,
301
310
  }));
302
- logger.info('[closeBundle]', 'vite build complete');
311
+ },
312
+ buildApp: {
313
+ order: 'post',
314
+ async handler() {
315
+ await postbuild(resolvedViteConfig?.root ?? process.cwd());
316
+ },
303
317
  },
304
318
  };
305
319
  return [
@@ -1,4 +1,4 @@
1
- import { BrowserRouter } from './router.js';
1
+ import { BrowserRouter } from './shared.js';
2
2
  type AnchorProps = React.ComponentPropsWithRef<'a'> & {
3
3
  href: string;
4
4
  };
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useEffect, useRef } from 'react';
4
- import { BrowserRouter } from './router.js';
4
+ import { BrowserRouter } from './shared.js';
5
5
  import { useRouter } from './use-router.js';
6
6
  function guard(path, prefetcher) {
7
7
  const connection = window.navigator.connection;
@@ -1,169 +1,6 @@
1
1
  import type { RscPayload } from '../env/rsc.js';
2
- import { Solas } from '../../solas.js';
3
- export declare namespace BrowserRouter {
4
- export type Params = Record<string, string>;
5
- export type Query = Record<string, string | number | boolean>;
6
- export type Path = keyof Solas.Routes & string;
7
- type Replace = {
8
- replace?: boolean;
9
- };
10
- export type GoOptions = {
11
- replace?: boolean;
12
- query?: Query;
13
- params?: Params;
14
- };
15
- /**
16
- * These targets are used as-is. They are not matched against the route table,
17
- * so this covers normal external URLs and hash-only links
18
- */
19
- export type ExternalTarget = `${string}:${string}` | `//${string}` | `#${string}`;
20
- export function isHashOnlyTarget(target: string): boolean;
21
- export function isExternalTarget(target: string, origin: string): boolean;
22
- /**
23
- * Turn a route pattern into the real path shape a caller can use. In practice,
24
- * every ':param' or '*' part becomes a plain string slot
25
- *
26
- * @example
27
- * ```ts
28
- * // '/p/:id' becomes '/p/${string}'
29
- * // '/test/*' becomes '/test/${string}'
30
- * // '/posts' stays '/posts'
31
- * ```
32
- *
33
- * @example
34
- * ```ts
35
- * type A = ResolvedPath<'/posts/:id'>
36
- * // '/posts/${string}'
37
- * ```
38
- *
39
- * @example
40
- * ```ts
41
- * type B = ResolvedPath<'/docs/*'>
42
- * // '/docs/${string}'
43
- * ```
44
- */
45
- export type ResolvedPath<TPath extends string> = TPath extends `${infer Start}:${string}/${infer Rest}` ? `${Start}${string}/${ResolvedPath<Rest>}` : TPath extends `${infer Start}:${string}` ? `${Start}${string}` : TPath extends `${infer Start}*${infer Rest}` ? `${Start}${string}${ResolvedPath<Rest>}` : TPath;
46
- /**
47
- * Once we have a real path, also allow the usual query-string and hash forms
48
- *
49
- * @example
50
- * ```ts
51
- * type A = TargetSuffix<'/posts/123'>
52
- * // '/posts/123' | '/posts/123?${string}' | '/posts/123#${string}' | '/posts/123?${string}#${string}'
53
- * ```
54
- */
55
- export type TargetSuffix<TPath extends string> = TPath | `${TPath}?${string}` | `${TPath}#${string}` | `${TPath}?${string}#${string}`;
56
- /**
57
- * This is the final string form a caller can navigate to. It can be an external
58
- * URL, or a concrete URL that matches one of the known routes
59
- *
60
- * @example
61
- * ```ts
62
- * type A = Target
63
- * // 'https://example.com'
64
- * // '#intro'
65
- * // '/posts/123'
66
- * // '/posts/123?draft=true'
67
- * ```
68
- */
69
- export type Target = ExternalTarget | TargetSuffix<ResolvedPath<Path>>;
70
- /**
71
- * Extra options for callers who already have a finished target string
72
- *
73
- * @example
74
- * ```ts
75
- * const a: TargetConfig = { query: { page: 2 } }
76
- * // params is rejected here because the path is already complete
77
- * ```
78
- */
79
- type TargetConfig = {
80
- params?: never;
81
- query?: Query;
82
- };
83
- /**
84
- * Extra options for callers who pass a route pattern and params separately.
85
- * If the route definition says that route needs params, this type makes
86
- * those params required. If the route has no params, it rejects them
87
- *
88
- * @example
89
- * ```ts
90
- * // if Solas.Routes['/posts/:id'] is { params: { id: string } }
91
- * type A = PatternConfig<'/posts/:id'>
92
- * // { query?: Query } & { params: { id: string } }
93
- * ```
94
- *
95
- * @example
96
- * ```ts
97
- * // if Solas.Routes['/about'] has no params field
98
- * type B = PatternConfig<'/about'>
99
- * // { query?: Query } & { params?: never }
100
- * ```
101
- */
102
- type PatternConfig<TPath extends Path> = {
103
- query?: Query;
104
- } & (Solas.Routes[TPath] extends {
105
- params: infer TParams extends Params;
106
- } ? {
107
- params: TParams;
108
- } : {
109
- params?: never;
110
- });
111
- /**
112
- * Typed <Link /> props, using `href` instead of the internal `to` name
113
- *
114
- * `query` is always allowed
115
- * `params` are only allowed when `href` is a known route pattern
116
- *
117
- * @example
118
- * ```ts
119
- * const a: LinkProps = { href: '/posts/:id', params: { id: '123' } }
120
- * const b: LinkProps = { href: '/posts/123?draft=true' }
121
- * ```
122
- */
123
- export type LinkProps = ({
124
- href: Target;
125
- } & TargetConfig) | (keyof Solas.Routes extends never ? never : {
126
- [TPath in Path]: {
127
- href: TPath;
128
- } & PatternConfig<TPath>;
129
- }[Path]);
130
- /**
131
- * Typed input for router.go(), using the same route rules as <Link />
132
- *
133
- * @example
134
- * ```ts
135
- * go('/p/post-2')
136
- * go('/?foo=bar', { replace: true })
137
- * ```
138
- *
139
- * @example
140
- * ```ts
141
- * go('/p/:id', { params: { id: 'post-2' }, replace: true })
142
- * ```
143
- *
144
- * The last overload is the fallback for plain `string` values. The
145
- * `string extends TTo` check stops that fallback from taking over
146
- * when TypeScript already knows the caller passed a more specific
147
- * string literal
148
- *
149
- * @example
150
- * ```ts
151
- * declare const dynamicPath: string
152
- * go(dynamicPath, { replace: true })
153
- * ```
154
- */
155
- export type Go = {
156
- <TTo extends Path>(to: TTo, opts?: PatternConfig<TTo> & Replace): Promise<string>;
157
- <TTo extends Target>(to: TTo, opts?: TargetConfig & Replace): Promise<string>;
158
- <TTo extends string>(to: string extends TTo ? TTo : never, opts?: GoOptions): Promise<string>;
159
- };
160
- /**
161
- * Convert a route pattern and params into a real path string. This is used internally
162
- * to implement <Link /> and router.go
163
- */
164
- export function toTarget(path: string, params?: Record<string, string>, query?: BrowserRouter.Query): string;
165
- export {};
166
- }
2
+ import { BrowserRouter } from './shared.js';
3
+ export { BrowserRouter } from './shared.js';
167
4
  export declare const BrowserRouterContext: import("react").Context<{
168
5
  go: BrowserRouter.Go;
169
6
  prefetch: (path: string) => void;
@@ -4,70 +4,9 @@ import { createContext, useCallback, useEffect, useMemo, useRef } from 'react';
4
4
  import { createFromFetch } from '@vitejs/plugin-rsc/browser';
5
5
  import { Logger } from '../../utils/logger.js';
6
6
  import { Solas } from '../../solas.js';
7
- import { Prefetcher } from './../prefetcher.js';
8
- export { BrowserRouter };
9
- var BrowserRouter;
10
- (function (BrowserRouter) {
11
- function isHashOnlyTarget(target) {
12
- return target.startsWith('#');
13
- }
14
- BrowserRouter.isHashOnlyTarget = isHashOnlyTarget;
15
- function isExternalTarget(target, origin) {
16
- if (isHashOnlyTarget(target))
17
- return false;
18
- try {
19
- return new URL(target, origin).origin !== origin;
20
- }
21
- catch {
22
- return false;
23
- }
24
- }
25
- BrowserRouter.isExternalTarget = isExternalTarget;
26
- /**
27
- * Convert a route pattern and params into a real path string. This is used internally
28
- * to implement <Link /> and router.go
29
- */
30
- function toTarget(path, params, query) {
31
- // keep track of which params were consumed by named `:param` slots
32
- const used = new Set();
33
- // replace each named route param with its URL-encoded value
34
- let to = path.replaceAll(/:([A-Za-z0-9_]+)/g, (_, key) => {
35
- const value = params?.[key];
36
- if (value == null) {
37
- throw new Error(`[Link]: missing route param: ${key}`);
38
- }
39
- used.add(key);
40
- return encodeURIComponent(value);
41
- });
42
- if (to.includes('*')) {
43
- // wildcard routes use the one param that was not already matched by a named slot
44
- const remaining = Object.entries(params ?? {}).filter(([key]) => !used.has(key));
45
- if (remaining.length !== 1) {
46
- throw new Error('[Link]: wildcard routes require exactly one unmatched param');
47
- }
48
- // encode each path segment separately so embedded '/' still acts like a path separator
49
- to = to.replace('*', remaining[0][1].split('/').map(encodeURIComponent).join('/'));
50
- }
51
- if (!query)
52
- return to;
53
- // split the URL up so new query params can be merged without losing an existing hash
54
- const hashIndex = to.indexOf('#');
55
- const hash = hashIndex >= 0 ? to.slice(hashIndex) : '';
56
- const pathWithSearch = hashIndex >= 0 ? to.slice(0, hashIndex) : to;
57
- const searchIndex = pathWithSearch.indexOf('?');
58
- const pathname = searchIndex >= 0 ? pathWithSearch.slice(0, searchIndex) : pathWithSearch;
59
- const currentSearch = searchIndex >= 0 ? pathWithSearch.slice(searchIndex + 1) : '';
60
- const search = new URLSearchParams(currentSearch);
61
- // later values win, so passed query props overwrite any existing query string values
62
- for (const [key, value] of Object.entries(query)) {
63
- search.set(key, String(value));
64
- }
65
- const value = search.toString();
66
- // rebuild the URL in the same order: pathname, optional query string, then hash
67
- return `${pathname}${value.length > 0 ? `?${value}` : ''}${hash}`;
68
- }
69
- BrowserRouter.toTarget = toTarget;
70
- })(BrowserRouter || (BrowserRouter = {}));
7
+ import { Prefetcher } from '../prefetcher.js';
8
+ import { BrowserRouter } from './shared.js';
9
+ export { BrowserRouter } from './shared.js';
71
10
  export const BrowserRouterContext = createContext({
72
11
  go: async () => '',
73
12
  prefetch: () => { },
@@ -80,28 +19,16 @@ const DEFAULT_GO_CONFIG = {
80
19
  const logger = new Logger();
81
20
  const prefetcher = new Prefetcher();
82
21
  export function BrowserRouterProvider({ children, setPayload, isNavigating = false, url, }) {
83
- // id to track active navigations
84
22
  const id = useRef(0);
85
- // abort controller for in-flight navigation
86
23
  const controller = useRef(null);
87
- /**
88
- * Navigate to a new route
89
- * @param to the destination url (absolute or relative to origin)
90
- * @param opts navigation options
91
- * @returns the path that was navigated to (relative to origin)
92
- */
93
24
  const go = useCallback(async (to, opts = {}) => {
94
- // increment navigation id to invalidate any in-flight navigations
95
25
  id.current += 1;
96
26
  const navigationId = id.current;
97
- // fallback for abort/error paths
98
27
  const currentPath = window.location.pathname + window.location.search;
99
28
  let path = currentPath;
100
29
  const replace = opts?.replace ?? DEFAULT_GO_CONFIG.replace;
101
30
  controller.current?.abort();
102
31
  controller.current = null;
103
- // distinguish an actual prior prefetch from a cache entry we create
104
- // opportunistically for this navigation
105
32
  let existing = false;
106
33
  try {
107
34
  const target = BrowserRouter.toTarget(to, opts.params, opts.query);
@@ -112,10 +39,7 @@ export function BrowserRouterProvider({ children, setPayload, isNavigating = fal
112
39
  const key = Prefetcher.key(url.toString(), window.location.origin);
113
40
  if (!key)
114
41
  throw new Error('Invalid navigation url');
115
- // switch to the normalised target once the url is valid
116
42
  path = key;
117
- // internal client navigation should update the route immediately, even
118
- // if the subsequent fetch resolves to a 404 or other error state
119
43
  if (path !== currentPath) {
120
44
  if (replace) {
121
45
  window.history.replaceState(null, '', path);
@@ -124,9 +48,6 @@ export function BrowserRouterProvider({ children, setPayload, isNavigating = fal
124
48
  window.history.pushState(null, '', path);
125
49
  }
126
50
  }
127
- // if the target was already prefetched, use the cached response promise
128
- // and set existing to true so we don't remove it from cache
129
- // after navigation
130
51
  let promise = prefetcher.get(path);
131
52
  existing = promise !== undefined;
132
53
  if (!promise) {
@@ -138,27 +59,18 @@ export function BrowserRouterProvider({ children, setPayload, isNavigating = fal
138
59
  });
139
60
  prefetcher.set(path, promise);
140
61
  }
141
- // if another navigation has started since this one, ignore the result
142
- // and return early
143
62
  if (navigationId !== id.current)
144
63
  return path;
145
- // we need both the parsed payload and the final response url because
146
- // redirects can change the canonical path we should store in history
147
64
  const [res, payload] = await Promise.all([
148
65
  promise,
149
66
  createFromFetch(promise),
150
67
  ]);
151
- // use the final response url so client history matches server redirects
152
68
  const resolvedPath = Prefetcher.key(res.url, window.location.origin) ?? path;
153
- // check again if another navigation has started while we were awaiting
154
- // the response
155
69
  if (navigationId !== id.current)
156
70
  return resolvedPath;
157
71
  if (resolvedPath !== path) {
158
72
  window.history.replaceState(null, '', resolvedPath);
159
73
  }
160
- // this state update is already wrapped in a
161
- // transition before being passed as props
162
74
  setPayload?.(payload);
163
75
  window.dispatchEvent(new CustomEvent(Solas.Events.names.NAVIGATION, {
164
76
  detail: { path: resolvedPath },
@@ -180,20 +92,12 @@ export function BrowserRouterProvider({ children, setPayload, isNavigating = fal
180
92
  finally {
181
93
  if (navigationId === id.current)
182
94
  controller.current = null;
183
- // keep entries that were already in the prefetch cache before go() ran. Only remove
184
- // the temporary cache entry go() created for its own in-flight dedupe
185
95
  if (!existing) {
186
- // this fetch was not an intentional prefetch, so do not leave it behind
187
- // as a reusable cache entry after navigation finishes
188
96
  prefetcher.remove(path);
189
97
  }
190
98
  }
191
99
  return path;
192
100
  }, [setPayload]);
193
- /**
194
- * Prefetch a route's RSC payload
195
- * @param path the route path to prefetch (absolute or relative to origin)
196
- */
197
101
  const prefetch = useCallback((path) => {
198
102
  const key = Prefetcher.key(path, window.location.origin);
199
103
  if (!key)