@ossido-labs/ossido-router 0.1.1 → 0.1.3
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/dist/esm/components/RouteMatch.js.map +1 -1
- package/dist/esm/components/RouterContext.js +2 -1
- package/dist/esm/components/RouterContext.js.map +1 -1
- package/dist/esm/components/RouterContextProvider.js +1 -1
- package/dist/esm/hooks/useRoute.js +1 -1
- package/dist/esm/utils/from-url-to-parsed-location.js +3 -1
- package/dist/esm/utils/from-url-to-parsed-location.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RouteMatch.js","names":[],"sources":["../../../src/components/RouteMatch.tsx"],"sourcesContent":["import type { JSX } from 'react';\nimport { memo, Suspense, useMemo } from 'react';\n\nimport {
|
|
1
|
+
{"version":3,"file":"RouteMatch.js","names":[],"sources":["../../../src/components/RouteMatch.tsx"],"sourcesContent":["import type { JSX } from 'react';\nimport { memo, Suspense, useMemo } from 'react';\n\nimport {\n DefaultLoading,\n DefaultError,\n DevErrorReporter,\n} from '@ossido-labs/ossido-ui';\n\nimport type { Mode } from '../types';\nimport type { Route } from '../route';\nimport { buildResourceKey, readLayoutData } from '../data/resourceCache';\n\nimport { useRouterContext } from './RouterContext';\nimport { CriticalCss } from './CriticalCss';\nimport { RouteDataLoader } from './RouteDataLoader';\nimport { OssidoErrorBoundary } from './OssidoErrorBoundary';\n\ninterface RouteMatchProps {\n route: Route;\n mode?: Mode;\n}\n\n/**\n * Renders the matched route: its parent layouts wrap an error boundary +\n * `<Suspense>` boundary around the data-reading leaf. The boundary is keyed by\n * the data-resource key so a navigation (including same-route param changes)\n * remounts it and shows the loading fallback, while the layouts persist.\n */\nexport const RouteMatch = ({ route, mode }: RouteMatchProps): JSX.Element => {\n const { location, navigationId, retry } = useRouterContext();\n\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const routes = useMemo(() => loadParentComponents(route), [route.id]);\n\n const resourceKey = buildResourceKey(navigationId, location);\n\n const LoadingComponent = route.options.loadingComponent ?? DefaultLoading;\n // In dev the boundary reports the error to the shared store (rendered by the\n // floating DevErrorOverlayHost); production falls back to a detail-free page\n // so source/stack are never leaked to end users.\n const ErrorComponent =\n route.options.errorComponent ??\n (mode === 'Dev' ? DevErrorReporter : DefaultError);\n\n return (\n <TraverseRootComponents routes={routes} mode={mode}>\n {/* The leaf route's critical CSS must stay OUTSIDE the <Suspense> below:\n React 19 ties a boundary's reveal to any `precedence` stylesheet\n rendered inside it, which would push the streamed initial content into\n an out-of-order late chunk (empty shell paints first — a flash on\n every cold load). Rendered here it still hoists into the shell's\n <head> as render-blocking, but the boundary streams inline. */}\n <CriticalCss routeFilePath={route.filePath} mode={mode} />\n {/* The <Suspense> is not keyed by the resource key, so navigation\n re-renders (rather than remounts) the subtree. With the destination's\n data + code prefetched by `updateLocation`, that lets the new page\n appear in place with no fallback flash. The error boundary resets via\n `resetKey` instead of a key change. */}\n <Suspense fallback={<LoadingComponent />}>\n <OssidoErrorBoundary\n resetKey={resourceKey}\n fallback={ErrorComponent}\n onReset={retry}\n >\n <RouteDataLoader\n route={route}\n resourceKey={resourceKey}\n location={location}\n />\n </OssidoErrorBoundary>\n </Suspense>\n </TraverseRootComponents>\n );\n};\n\ninterface TraverseRootComponentsProps {\n routes: Array<Route>;\n children?: React.ReactNode;\n index?: number;\n mode?: Mode;\n}\n\n/**\n * Renders the layout (`layout`) components that wrap the selected route.\n * Layouts receive only `children` now — they live OUTSIDE the keyed Suspense\n * boundary, so they persist across navigation while only the page area shows\n * the loading fallback. Memoized so navigation does not re-render layouts.\n */\nconst TraverseRootComponents = memo(\n ({\n routes,\n index = 0,\n mode,\n children,\n }: TraverseRootComponentsProps): React.JSX.Element => {\n if (routes.length > index) {\n const route = routes[index] as Route;\n const Parent = route.component;\n\n // Fallback to the route id if the filePath is not defined\n // as is the case for the root route\n const routeFilePath = route.filePath || route.id;\n\n // A `layout.rs` handler's data is seeded (by SSR or the page's data fetch)\n // and read synchronously here, then spread as the layout's props alongside\n // `children`. Layouts without a data handler receive only `children`.\n const layoutProps =\n route.options.hasHandler && route.options.dataKey\n ? readLayoutData(route.options.dataKey)\n : undefined;\n\n return (\n <Parent {...layoutProps}>\n <CriticalCss routeFilePath={routeFilePath} mode={mode} />\n <TraverseRootComponents routes={routes} index={index + 1} mode={mode}>\n {children}\n </TraverseRootComponents>\n </Parent>\n );\n }\n\n return <>{children}</>;\n },\n);\nTraverseRootComponents.displayName = 'TraverseRootComponents';\n\nconst loadParentComponents = (\n route: Route,\n loader: Array<Route> = [],\n): Array<Route> => {\n const parentComponent = route.options.getParentRoute?.() as Route;\n\n loader.push(parentComponent);\n\n if (!parentComponent.isRoot) {\n return loadParentComponents(parentComponent, loader);\n }\n\n return loader.reverse();\n};\n"],"mappings":";;;;;;;;;;;;;;;;AA6BA,MAAa,cAAc,EAAE,OAAO,WAAyC;CAC3E,MAAM,EAAE,UAAU,cAAc,UAAU,iBAAiB;CAG3D,MAAM,SAAS,cAAc,qBAAqB,KAAK,GAAG,CAAC,MAAM,EAAE,CAAC;CAEpE,MAAM,cAAc,iBAAiB,cAAc,QAAQ;CAE3D,MAAM,mBAAmB,MAAM,QAAQ,oBAAoB;CAI3D,MAAM,iBACJ,MAAM,QAAQ,mBACb,SAAS,QAAQ,mBAAmB;CAEvC,OACE,qBAAC,wBAAD;EAAgC;EAAc;YAA9C,CAOE,oBAAC,aAAD;GAAa,eAAe,MAAM;GAAgB;EAAO,IAMzD,oBAAC,UAAD;GAAU,UAAU,oBAAC,kBAAD,CAAmB;aACrC,oBAAC,qBAAD;IACE,UAAU;IACV,UAAU;IACV,SAAS;cAET,oBAAC,iBAAD;KACS;KACM;KACH;IACX;GACkB;EACb,EACY;;AAE5B;;;;;;;AAeA,MAAM,yBAAyB,MAC5B,EACC,QACA,QAAQ,GACR,MACA,eACoD;CACpD,IAAI,OAAO,SAAS,OAAO;EACzB,MAAM,QAAQ,OAAO;EACrB,MAAM,SAAS,MAAM;EAIrB,MAAM,gBAAgB,MAAM,YAAY,MAAM;EAK9C,MAAM,cACJ,MAAM,QAAQ,cAAc,MAAM,QAAQ,UACtC,eAAe,MAAM,QAAQ,OAAO,IACpC;EAEN,OACE,qBAAC,QAAD;GAAQ,GAAI;aAAZ,CACE,oBAAC,aAAD;IAA4B;IAAqB;GAAO,IACxD,oBAAC,wBAAD;IAAgC;IAAQ,OAAO,QAAQ;IAAS;IAC7D;GACqB,EAClB;;CAEZ;CAEA,OAAO,gCAAG,SAAW;AACvB,CACF;AACA,uBAAuB,cAAc;AAErC,MAAM,wBACJ,OACA,SAAuB,CAAC,MACP;CACjB,MAAM,kBAAkB,MAAM,QAAQ,iBAAiB;CAEvD,OAAO,KAAK,eAAe;CAE3B,IAAI,CAAC,gBAAgB,QACnB,OAAO,qBAAqB,iBAAiB,MAAM;CAGrD,OAAO,OAAO,QAAQ;AACxB"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sanitizePathname } from "../utils/match-route.js";
|
|
1
2
|
import { createContext, useContext } from "react";
|
|
2
3
|
|
|
3
4
|
//#region src/components/RouterContext.tsx
|
|
@@ -13,7 +14,7 @@ function getInitialLocation(serverPayloadLocation) {
|
|
|
13
14
|
};
|
|
14
15
|
const { pathname, hash, href, search } = window.location;
|
|
15
16
|
return {
|
|
16
|
-
pathname,
|
|
17
|
+
pathname: sanitizePathname(pathname),
|
|
17
18
|
hash,
|
|
18
19
|
href,
|
|
19
20
|
searchStr: search,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RouterContext.js","names":[],"sources":["../../../src/components/RouterContext.tsx"],"sourcesContent":["import { createContext, useContext } from 'react';\n\nimport type { Router } from '../router';\nimport type { ServerInitialLocation } from '../types';\n\n/** How to reflect a committed navigation in the browser (history + scroll). */\nexport interface NavigationCommitOptions {\n history?: { type: 'pushState' | 'replaceState'; path: string };\n scroll?: boolean;\n /**\n * Override the app's `viewTransitions` config for this navigation: `true`\n * forces a view transition, `false` skips it. Defaults to the config value.\n */\n viewTransition?: boolean;\n}\n\nconst isServerSide = typeof window === 'undefined';\n\nexport interface ParsedLocation {\n href: string;\n pathname: string;\n search: Record<string, string>;\n searchStr: string;\n hash: string;\n}\n\nexport interface RouterContextValue {\n router: Router;\n location: ParsedLocation;\n /**\n * Incremented on every navigation (and on error retry / a manual\n * `refetchProps`). Combined with the\n * pathname it forms the data-resource key, so each navigation gets a fresh\n * resource (a refetch).\n *\n * Initialized to `0` identically on server and client and never changed\n * during the initial mount — otherwise the boundary would remount and flash\n * the loading fallback over server-rendered content (hydration mismatch).\n */\n navigationId: number;\n /**\n * Start a navigation to `loc`. Unless the destination has a `loading.tsx`,\n * its data is prefetched first and the navigation is only committed once the\n * data is ready — so the current page stays until then (no blank flash), even\n * across a layout change. `options` carries the browser history/scroll update\n * to apply at commit time.\n */\n updateLocation: (\n loc: ParsedLocation,\n options?: NavigationCommitOptions,\n ) => void;\n /**\n * Re-run the current route's data load (a refetch). Used by the error\n * boundary `reset` and by `useRouter().refetchProps`.\n */\n retry: () => void;\n}\n\nexport const RouterContext = createContext({} as RouterContextValue);\n\nexport function getInitialLocation(\n serverPayloadLocation: ServerInitialLocation,\n): ParsedLocation {\n if (isServerSide) {\n return {\n pathname: serverPayloadLocation.pathname || '',\n hash: '',\n href: serverPayloadLocation.href || '',\n searchStr: serverPayloadLocation.searchStr || '',\n search: Object.fromEntries(\n new URLSearchParams(serverPayloadLocation.searchStr),\n ),\n };\n }\n\n const { pathname, hash, href, search } = window.location;\n return {\n pathname,\n hash,\n href,\n searchStr: search,\n search: Object.fromEntries(new URLSearchParams(search)),\n };\n}\n\n/**\n * @warning THIS SHOULD NOT BE EXPOSED TO USERLAND\n */\nexport function useRouterContext(): RouterContextValue {\n return useContext(RouterContext);\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"RouterContext.js","names":[],"sources":["../../../src/components/RouterContext.tsx"],"sourcesContent":["import { createContext, useContext } from 'react';\n\nimport type { Router } from '../router';\nimport type { ServerInitialLocation } from '../types';\nimport { sanitizePathname } from '../utils/match-route';\n\n/** How to reflect a committed navigation in the browser (history + scroll). */\nexport interface NavigationCommitOptions {\n history?: { type: 'pushState' | 'replaceState'; path: string };\n scroll?: boolean;\n /**\n * Override the app's `viewTransitions` config for this navigation: `true`\n * forces a view transition, `false` skips it. Defaults to the config value.\n */\n viewTransition?: boolean;\n}\n\nconst isServerSide = typeof window === 'undefined';\n\nexport interface ParsedLocation {\n href: string;\n pathname: string;\n search: Record<string, string>;\n searchStr: string;\n hash: string;\n}\n\nexport interface RouterContextValue {\n router: Router;\n location: ParsedLocation;\n /**\n * Incremented on every navigation (and on error retry / a manual\n * `refetchProps`). Combined with the\n * pathname it forms the data-resource key, so each navigation gets a fresh\n * resource (a refetch).\n *\n * Initialized to `0` identically on server and client and never changed\n * during the initial mount — otherwise the boundary would remount and flash\n * the loading fallback over server-rendered content (hydration mismatch).\n */\n navigationId: number;\n /**\n * Start a navigation to `loc`. Unless the destination has a `loading.tsx`,\n * its data is prefetched first and the navigation is only committed once the\n * data is ready — so the current page stays until then (no blank flash), even\n * across a layout change. `options` carries the browser history/scroll update\n * to apply at commit time.\n */\n updateLocation: (\n loc: ParsedLocation,\n options?: NavigationCommitOptions,\n ) => void;\n /**\n * Re-run the current route's data load (a refetch). Used by the error\n * boundary `reset` and by `useRouter().refetchProps`.\n */\n retry: () => void;\n}\n\nexport const RouterContext = createContext({} as RouterContextValue);\n\nexport function getInitialLocation(\n serverPayloadLocation: ServerInitialLocation,\n): ParsedLocation {\n if (isServerSide) {\n return {\n pathname: serverPayloadLocation.pathname || '',\n hash: '',\n href: serverPayloadLocation.href || '',\n searchStr: serverPayloadLocation.searchStr || '',\n search: Object.fromEntries(\n new URLSearchParams(serverPayloadLocation.searchStr),\n ),\n };\n }\n\n const { pathname, hash, href, search } = window.location;\n return {\n // Match the server, which renders with the sanitized pathname. Static\n // export serves directory URLs with a trailing slash (`/docs/x/`), so the\n // raw `window.location.pathname` would differ from the server's `/docs/x`\n // and break hydration for route-aware UI (e.g. active nav links).\n pathname: sanitizePathname(pathname),\n hash,\n href,\n searchStr: search,\n search: Object.fromEntries(new URLSearchParams(search)),\n };\n}\n\n/**\n * @warning THIS SHOULD NOT BE EXPOSED TO USERLAND\n */\nexport function useRouterContext(): RouterContextValue {\n return useContext(RouterContext);\n}\n"],"mappings":";;;;AAiBA,MAAM,eAAe,OAAO,WAAW;AA0CvC,MAAa,gBAAgB,cAAc,CAAC,CAAuB;AAEnE,SAAgB,mBACd,uBACgB;CAChB,IAAI,cACF,OAAO;EACL,UAAU,sBAAsB,YAAY;EAC5C,MAAM;EACN,MAAM,sBAAsB,QAAQ;EACpC,WAAW,sBAAsB,aAAa;EAC9C,QAAQ,OAAO,YACb,IAAI,gBAAgB,sBAAsB,SAAS,CACrD;CACF;CAGF,MAAM,EAAE,UAAU,MAAM,MAAM,WAAW,OAAO;CAChD,OAAO;EAKL,UAAU,iBAAiB,QAAQ;EACnC;EACA;EACA,WAAW;EACX,QAAQ,OAAO,YAAY,IAAI,gBAAgB,MAAM,CAAC;CACxD;AACF;;;;AAKA,SAAgB,mBAAuC;CACrD,OAAO,WAAW,aAAa;AACjC"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { buildResourceKey, getOrCreateResource } from "../data/resourceCache.js";
|
|
2
|
+
import { matchRoute } from "../utils/match-route.js";
|
|
2
3
|
import { RouterContext, getInitialLocation } from "./RouterContext.js";
|
|
3
4
|
import { fromUrlToParsedLocation } from "../utils/from-url-to-parsed-location.js";
|
|
4
|
-
import { matchRoute } from "../utils/match-route.js";
|
|
5
5
|
import { VIEW_TRANSITIONS_ENABLED, runCommit } from "../utils/view-transition.js";
|
|
6
6
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
7
7
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { sanitizePathname } from "./match-route.js";
|
|
2
|
+
|
|
1
3
|
//#region src/utils/from-url-to-parsed-location.ts
|
|
2
4
|
function fromUrlToParsedLocation(href) {
|
|
3
5
|
const location = new URL(href, window.location.origin);
|
|
4
6
|
return {
|
|
5
7
|
href: location.href,
|
|
6
|
-
pathname: location.pathname,
|
|
8
|
+
pathname: sanitizePathname(location.pathname),
|
|
7
9
|
search: Object.fromEntries(location.searchParams),
|
|
8
10
|
searchStr: location.search,
|
|
9
11
|
hash: location.hash
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"from-url-to-parsed-location.js","names":[],"sources":["../../../src/utils/from-url-to-parsed-location.ts"],"sourcesContent":["import type { ParsedLocation } from '../components/RouterContext';\n\nexport function fromUrlToParsedLocation(href: string): ParsedLocation {\n const location = new URL(href, window.location.origin);\n return {\n href: location.href,\n pathname: location.pathname,\n search: Object.fromEntries(location.searchParams),\n searchStr: location.search,\n hash: location.hash,\n };\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"from-url-to-parsed-location.js","names":[],"sources":["../../../src/utils/from-url-to-parsed-location.ts"],"sourcesContent":["import type { ParsedLocation } from '../components/RouterContext';\nimport { sanitizePathname } from './match-route';\n\nexport function fromUrlToParsedLocation(href: string): ParsedLocation {\n const location = new URL(href, window.location.origin);\n return {\n href: location.href,\n // Canonical pathname (no trailing slash), matching the server payload so\n // `useRouter().pathname` is consistent across SSR, hydration, and navigation.\n pathname: sanitizePathname(location.pathname),\n search: Object.fromEntries(location.searchParams),\n searchStr: location.search,\n hash: location.hash,\n };\n}\n"],"mappings":";;;AAGA,SAAgB,wBAAwB,MAA8B;CACpE,MAAM,WAAW,IAAI,IAAI,MAAM,OAAO,SAAS,MAAM;CACrD,OAAO;EACL,MAAM,SAAS;EAGf,UAAU,iBAAiB,SAAS,QAAQ;EAC5C,QAAQ,OAAO,YAAY,SAAS,YAAY;EAChD,WAAW,SAAS;EACpB,MAAM,SAAS;CACjB;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossido-labs/ossido-router",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"react-intersection-observer": "^9.13.0",
|
|
49
|
-
"@ossido-labs/ossido-ui": "0.1.
|
|
49
|
+
"@ossido-labs/ossido-ui": "0.1.3"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@testing-library/react": "16.3.0",
|