@solidjs/router
Advanced tools
+22
-16
@@ -1,14 +0,22 @@ | ||
| import { createMemo, mergeProps, splitProps } from "solid-js"; | ||
| import { createMemo, merge, omit } from "solid-js"; | ||
| import { useHref, useLocation, useNavigate, useResolvedPath } from "./routing.js"; | ||
| import { normalizePath } from "./utils.js"; | ||
| function toClassName(value) { | ||
| if (!value) | ||
| return ""; | ||
| if (typeof value === "string" || typeof value === "number") | ||
| return String(value); | ||
| if (Array.isArray(value)) | ||
| return value.map(toClassName).filter(Boolean).join(" "); | ||
| if (typeof value === "object") { | ||
| return Object.entries(value) | ||
| .filter(([, enabled]) => enabled) | ||
| .map(([name]) => name) | ||
| .join(" "); | ||
| } | ||
| return ""; | ||
| } | ||
| export function A(props) { | ||
| props = mergeProps({ inactiveClass: "inactive", activeClass: "active" }, props); | ||
| const [, rest] = splitProps(props, [ | ||
| "href", | ||
| "state", | ||
| "class", | ||
| "activeClass", | ||
| "inactiveClass", | ||
| "end" | ||
| ]); | ||
| props = merge({ inactiveClass: "inactive", activeClass: "active" }, props); | ||
| const rest = omit(props, "href", "state", "class", "activeClass", "inactiveClass", "end"); | ||
| const to = useResolvedPath(() => props.href); | ||
@@ -25,8 +33,6 @@ const href = useHref(to); | ||
| }); | ||
| return (<a {...rest} href={href() || props.href} state={JSON.stringify(props.state)} classList={{ | ||
| ...(props.class && { [props.class]: true }), | ||
| [props.inactiveClass]: !isActive()[0], | ||
| [props.activeClass]: isActive()[0], | ||
| ...rest.classList | ||
| }} link aria-current={isActive()[1] ? "page" : undefined}/>); | ||
| const className = createMemo(() => [toClassName(props.class), isActive()[0] ? props.activeClass : props.inactiveClass] | ||
| .filter(Boolean) | ||
| .join(" ")); | ||
| return (<a {...rest} href={href() || props.href} state={JSON.stringify(props.state)} class={className()} link aria-current={isActive()[1] ? "page" : undefined}/>); | ||
| } | ||
@@ -33,0 +39,0 @@ export function Navigate(props) { |
+12
-10
| import { JSX } from "solid-js"; | ||
| import type { Submission, SubmissionStub, NarrowResponse } from "../types.js"; | ||
| import type { Submission, NarrowResponse } from "../types.js"; | ||
| export type Action<T extends Array<any>, U, V = T> = (T extends [FormData | URLSearchParams] | [] ? JSX.SerializableAttributeValue : unknown) & ((...vars: T) => Promise<NarrowResponse<U>>) & { | ||
| url: string; | ||
| with<A extends any[], B extends any[]>(this: (this: any, ...args: [...A, ...B]) => Promise<NarrowResponse<U>>, ...args: A): Action<B, U, V>; | ||
| onSubmit(hook: (...args: V extends Array<any> ? V : T) => void): Action<T, U, V>; | ||
| onSettled(hook: (submission: Submission<V extends Array<any> ? V : T, NarrowResponse<U>>) => void): Action<T, U, V>; | ||
| }; | ||
| type ActionFactory = { | ||
| <T extends Array<any>, U = void>(fn: (...args: T) => Promise<U>, name?: string): Action<T, U>; | ||
| <T extends Array<any>, U = void>(fn: (...args: T) => Promise<U>, options?: { | ||
| name?: string; | ||
| }): Action<T, U>; | ||
| }; | ||
| export declare const actions: Map<string, Action<any, any, any>>; | ||
| export declare function useSubmissions<T extends Array<any>, U, V>(fn: Action<T, U, V>, filter?: (input: V) => boolean): Submission<T, NarrowResponse<U>>[] & { | ||
| pending: boolean; | ||
| }; | ||
| export declare function useSubmission<T extends Array<any>, U, V>(fn: Action<T, U, V>, filter?: (input: V) => boolean): Submission<T, NarrowResponse<U>> | SubmissionStub; | ||
| export declare function useSubmissions<T extends Array<any>, U, V>(fn: Action<T, U, V>, filter?: (input: V) => boolean): Submission<V, NarrowResponse<U>>[]; | ||
| export declare function useAction<T extends Array<any>, U, V>(action: Action<T, U, V>): (...args: Parameters<Action<T, U, V>>) => Promise<NarrowResponse<U>>; | ||
| export declare function action<T extends Array<any>, U = void>(fn: (...args: T) => Promise<U>, name?: string): Action<T, U>; | ||
| export declare function action<T extends Array<any>, U = void>(fn: (...args: T) => Promise<U>, options?: { | ||
| name?: string; | ||
| onComplete?: (s: Submission<T, U>) => void; | ||
| }): Action<T, U>; | ||
| export declare const action: ActionFactory; | ||
| export {}; |
+94
-75
@@ -1,6 +0,9 @@ | ||
| import { $TRACK, createMemo, createSignal, onCleanup, getOwner } from "solid-js"; | ||
| import { isServer } from "solid-js/web"; | ||
| import { $TRACK, action as createSolidAction, createMemo, onCleanup, getOwner } from "solid-js"; | ||
| import { isServer } from "@solidjs/web"; | ||
| import { useRouter } from "../routing.js"; | ||
| import { mockBase, setFunctionName } from "../utils.js"; | ||
| import { cacheKeyOp, hashKey, revalidate, query } from "./query.js"; | ||
| const submitHooksSymbol = Symbol("routerActionSubmitHooks"); | ||
| const settledHooksSymbol = Symbol("routerActionSettledHooks"); | ||
| const invokeSymbol = Symbol("routerActionInvoke"); | ||
| export const actions = /* #__PURE__ */ new Map(); | ||
@@ -14,4 +17,2 @@ export function useSubmissions(fn, filter) { | ||
| return subs(); | ||
| if (property === "pending") | ||
| return subs().some(sub => !sub.result); | ||
| return subs()[property]; | ||
@@ -24,12 +25,2 @@ }, | ||
| } | ||
| export function useSubmission(fn, filter) { | ||
| const submissions = useSubmissions(fn, filter); | ||
| return new Proxy({}, { | ||
| get(_, property) { | ||
| if ((submissions.length === 0 && property === "clear") || property === "retry") | ||
| return () => { }; | ||
| return submissions[submissions.length - 1]?.[property]; | ||
| } | ||
| }); | ||
| } | ||
| export function useAction(action) { | ||
@@ -39,59 +30,55 @@ const r = useRouter(); | ||
| } | ||
| export function action(fn, options = {}) { | ||
| function mutate(...variables) { | ||
| function actionImpl(fn, options = {}) { | ||
| async function invoke(variables, current) { | ||
| const router = this.r; | ||
| const form = this.f; | ||
| const p = (router.singleFlight && fn.withOptions | ||
| const submitHooks = current[submitHooksSymbol]; | ||
| const settledHooks = current[settledHooksSymbol]; | ||
| const runMutation = () => (router.singleFlight && fn.withOptions | ||
| ? fn.withOptions({ headers: { "X-Single-Flight": "true" } }) | ||
| : fn)(...variables); | ||
| const [result, setResult] = createSignal(); | ||
| const run = createSolidAction(async function* (context) { | ||
| context.optimistic?.(); | ||
| try { | ||
| const value = await context.call(); | ||
| yield; | ||
| return { error: false, value }; | ||
| } | ||
| catch (error) { | ||
| yield; | ||
| return { error: true, value: error }; | ||
| } | ||
| }); | ||
| const settled = await settleActionResult(run({ | ||
| call: runMutation, | ||
| optimistic: submitHooks.size | ||
| ? () => { | ||
| for (const hook of submitHooks.values()) | ||
| hook(...variables); | ||
| } | ||
| : undefined | ||
| })); | ||
| const response = await handleResponse(settled.value, settled.error, router.navigatorFactory()); | ||
| if (!response) | ||
| return undefined; | ||
| let submission; | ||
| function handler(error) { | ||
| return async (res) => { | ||
| const result = await handleResponse(res, error, router.navigatorFactory()); | ||
| let retry = null; | ||
| o.onComplete?.({ | ||
| ...submission, | ||
| result: result?.data, | ||
| error: result?.error, | ||
| pending: false, | ||
| retry() { | ||
| return (retry = submission.retry()); | ||
| } | ||
| }); | ||
| if (retry) | ||
| return retry; | ||
| if (!result) | ||
| return submission.clear(); | ||
| setResult(result); | ||
| if (result.error && !form) | ||
| throw result.error; | ||
| return result.data; | ||
| }; | ||
| } | ||
| router.submissions[1](s => [ | ||
| ...s, | ||
| (submission = { | ||
| input: variables, | ||
| url, | ||
| get result() { | ||
| return result()?.data; | ||
| }, | ||
| get error() { | ||
| return result()?.error; | ||
| }, | ||
| get pending() { | ||
| return !result(); | ||
| }, | ||
| clear() { | ||
| router.submissions[1](v => v.filter(i => i !== submission)); | ||
| }, | ||
| retry() { | ||
| setResult(undefined); | ||
| const p = fn(...variables); | ||
| return p.then(handler(), handler(true)); | ||
| } | ||
| }) | ||
| ]); | ||
| return p.then(handler(), handler(true)); | ||
| submission = { | ||
| input: variables, | ||
| url, | ||
| result: response.data, | ||
| error: response.error, | ||
| clear() { | ||
| router.submissions[1](entries => entries.filter(entry => entry !== submission)); | ||
| }, | ||
| retry() { | ||
| submission.clear(); | ||
| return current[invokeSymbol].call({ r: router, f: form }, variables, current); | ||
| } | ||
| }; | ||
| router.submissions[1](entries => [...entries, submission]); | ||
| for (const hook of settledHooks.values()) | ||
| hook(submission); | ||
| if (response.error && !form) | ||
| throw response.error; | ||
| return response.data; | ||
| } | ||
@@ -101,8 +88,12 @@ const o = typeof options === "string" ? { name: options } : options; | ||
| const url = fn.url || (name && `https://action/${name}`) || ""; | ||
| mutate.base = url; | ||
| const wrapped = toAction(invoke, url); | ||
| if (name) | ||
| setFunctionName(mutate, name); | ||
| return toAction(mutate, url); | ||
| setFunctionName(wrapped, name); | ||
| return wrapped; | ||
| } | ||
| function toAction(fn, url) { | ||
| export const action = actionImpl; | ||
| function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map(), settledHooks = new Map()) { | ||
| const fn = function (...args) { | ||
| return invoke.call(this, [...boundArgs, ...args], fn); | ||
| }; | ||
| fn.toString = () => { | ||
@@ -114,11 +105,24 @@ if (!url) | ||
| fn.with = function (...args) { | ||
| const newFn = function (...passedArgs) { | ||
| return fn.call(this, ...args, ...passedArgs); | ||
| }; | ||
| newFn.base = fn.base; | ||
| const uri = new URL(url, mockBase); | ||
| uri.searchParams.set("args", hashKey(args)); | ||
| return toAction(newFn, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search); | ||
| const next = toAction(invoke, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search, [...boundArgs, ...args], base, submitHooks, settledHooks); | ||
| return next; | ||
| }; | ||
| fn.onSubmit = function (hook) { | ||
| const id = Symbol("actionOnSubmitHook"); | ||
| submitHooks.set(id, hook); | ||
| getOwner() && onCleanup(() => submitHooks.delete(id)); | ||
| return this; | ||
| }; | ||
| fn.onSettled = function (hook) { | ||
| const id = Symbol("actionOnSettledHook"); | ||
| settledHooks.set(id, hook); | ||
| getOwner() && onCleanup(() => settledHooks.delete(id)); | ||
| return this; | ||
| }; | ||
| fn.url = url; | ||
| fn.base = base; | ||
| fn[submitHooksSymbol] = submitHooks; | ||
| fn[settledHooksSymbol] = settledHooks; | ||
| fn[invokeSymbol] = invoke; | ||
| if (!isServer) { | ||
@@ -131,2 +135,17 @@ actions.set(url, fn); | ||
| const hashString = (s) => s.split("").reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0); | ||
| async function settleActionResult(result) { | ||
| const value = result; | ||
| if (value && typeof value.then === "function") { | ||
| return result.then(value => value); | ||
| } | ||
| if (value && typeof value.next === "function") { | ||
| const iterator = value; | ||
| let next = await iterator.next(); | ||
| while (!next.done) { | ||
| next = await iterator.next(); | ||
| } | ||
| return next.value; | ||
| } | ||
| return result; | ||
| } | ||
| async function handleResponse(response, error, navigate) { | ||
@@ -133,0 +152,0 @@ let data; |
@@ -1,2 +0,2 @@ | ||
| import { delegateEvents } from "solid-js/web"; | ||
| import { delegateEvents } from "@solidjs/web"; | ||
| import { onCleanup } from "solid-js"; | ||
@@ -3,0 +3,0 @@ import { actions } from "./action.js"; |
@@ -1,4 +0,3 @@ | ||
| export { createAsync, createAsyncStore, type AccessorWithLatest } from "./createAsync.js"; | ||
| export { action, useSubmission, useSubmissions, useAction, type Action } from "./action.js"; | ||
| export { query, revalidate, cache, type CachedFunction } from "./query.js"; | ||
| export { action, useSubmissions, useAction, type Action } from "./action.js"; | ||
| export { query, revalidate, type CachedFunction } from "./query.js"; | ||
| export { redirect, reload, json } from "./response.js"; |
@@ -1,4 +0,3 @@ | ||
| export { createAsync, createAsyncStore } from "./createAsync.js"; | ||
| export { action, useSubmission, useSubmissions, useAction } from "./action.js"; | ||
| export { query, revalidate, cache } from "./query.js"; | ||
| export { action, useSubmissions, useAction } from "./action.js"; | ||
| export { query, revalidate } from "./query.js"; | ||
| export { redirect, reload, json } from "./response.js"; |
@@ -5,3 +5,3 @@ import type { CacheEntry, NarrowResponse } from "../types.js"; | ||
| */ | ||
| export declare function revalidate(key?: string | string[] | void, force?: boolean): Promise<void>; | ||
| export declare function revalidate(key?: string | string[] | void, force?: boolean): void; | ||
| export declare function cacheKeyOp(key: string | string[] | void, fn: (cacheEntry: CacheEntry) => void): void; | ||
@@ -22,4 +22,2 @@ export type CachedFunction<T extends (...args: any) => any> = T extends (...args: infer A) => infer R ? ([] extends { | ||
| } | ||
| /** @deprecated use query instead */ | ||
| export declare const cache: typeof query; | ||
| export declare function hashKey<T extends Array<any>>(args: T): string; |
+10
-16
@@ -1,3 +0,3 @@ | ||
| import { createSignal, getListener, getOwner, onCleanup, sharedConfig, startTransition } from "solid-js"; | ||
| import { getRequestEvent, isServer } from "solid-js/web"; | ||
| import { createSignal, getObserver, getOwner, onCleanup, sharedConfig } from "solid-js"; | ||
| import { getRequestEvent, isServer } from "@solidjs/web"; | ||
| import { useNavigate, getIntent, getInPreloadFn } from "../routing.js"; | ||
@@ -31,8 +31,6 @@ const LocationHeader = "Location"; | ||
| export function revalidate(key, force = true) { | ||
| return startTransition(() => { | ||
| const now = Date.now(); | ||
| cacheKeyOp(key, entry => { | ||
| force && (entry[0] = 0); //force cache miss | ||
| entry[4][1](now); // retrigger live signals | ||
| }); | ||
| const now = Date.now(); | ||
| cacheKeyOp(key, entry => { | ||
| force && (entry[0] = 0); //force cache miss | ||
| entry[4][1](now); // retrigger live signals | ||
| }); | ||
@@ -76,3 +74,3 @@ } | ||
| } | ||
| if (getListener() && !isServer) { | ||
| if (getObserver() && !isServer) { | ||
| tracking = true; | ||
@@ -100,3 +98,3 @@ onCleanup(() => cached[4].count--); | ||
| : handleResponse(false)(cached[1]); | ||
| !isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version | ||
| !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version | ||
| } | ||
@@ -118,3 +116,3 @@ inPreloadFn && "then" in res && res.catch(() => { }); | ||
| cached[3] = intent; | ||
| !isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version | ||
| !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version | ||
| } | ||
@@ -166,5 +164,3 @@ else { | ||
| if (navigate && url.startsWith("/")) | ||
| startTransition(() => { | ||
| navigate(url, { replace: true }); | ||
| }); | ||
| navigate(url, { replace: true }); | ||
| else if (!isServer) | ||
@@ -211,4 +207,2 @@ window.location.href = url; | ||
| query.clear = () => getCache().clear(); | ||
| /** @deprecated use query instead */ | ||
| export const cache = query; | ||
| function matchKey(key, keys) { | ||
@@ -215,0 +209,0 @@ for (let k of keys) { |
+1
-1
@@ -7,2 +7,2 @@ export * from "./routers/index.js"; | ||
| export * from "./data/index.js"; | ||
| export type { Location, LocationChange, SearchParams, MatchFilter, MatchFilters, NavigateOptions, Navigator, OutputMatch, Params, PathMatch, RouteSectionProps, RoutePreloadFunc, RoutePreloadFuncArgs, RouteDefinition, RouteDescription, RouteMatch, RouterIntegration, RouterUtils, SetParams, Submission, BeforeLeaveEventArgs, RouteLoadFunc, RouteLoadFuncArgs, RouterResponseInit, CustomResponse } from "./types.js"; | ||
| export type { Location, LocationChange, SearchParams, MatchFilter, MatchFilters, NavigateOptions, Navigator, OutputMatch, Params, PathMatch, RouteSectionProps, RoutePreloadFunc, RoutePreloadFuncArgs, RouteDefinition, RouteDescription, RouteMatch, RouterIntegration, RouterUtils, SetParams, Submission, BeforeLeaveEventArgs, RouterResponseInit, CustomResponse } from "./types.js"; |
+211
-260
@@ -1,4 +0,3 @@ | ||
| import { isServer, getRequestEvent, createComponent as createComponent$1, memo, delegateEvents, spread, mergeProps as mergeProps$1, template } from 'solid-js/web'; | ||
| import { getOwner, runWithOwner, createMemo, createContext, onCleanup, useContext, untrack, createSignal, createRenderEffect, on, startTransition, resetErrorBoundaries, batch, createComponent, children, mergeProps, Show, createRoot, sharedConfig, getListener, $TRACK, splitProps, createResource, catchError } from 'solid-js'; | ||
| import { createStore, reconcile, unwrap } from 'solid-js/store'; | ||
| import { isServer, getRequestEvent, createComponent as createComponent$1, memo, delegateEvents, spread, mergeProps, template } from '@solidjs/web'; | ||
| import { getOwner, runWithOwner, createMemo, createContext, onCleanup, useContext, untrack, createSignal, flush, createComponent, children, merge, createRoot, sharedConfig, getObserver, $TRACK, action as action$1, omit } from 'solid-js'; | ||
@@ -254,4 +253,11 @@ function createBeforeLeave() { | ||
| const RouteContextObj = createContext(); | ||
| function useOptionalContext(context) { | ||
| try { | ||
| return useContext(context); | ||
| } catch { | ||
| return undefined; | ||
| } | ||
| } | ||
| const useRouter = () => invariant(useContext(RouterContextObj), "<A> and 'use' router primitives can be only used inside a Route."); | ||
| const useRoute = () => useContext(RouteContextObj) || useRouter().base; | ||
| const useRoute = () => useOptionalContext(RouteContextObj) || useRouter().base; | ||
| const useResolvedPath = path => { | ||
@@ -303,4 +309,4 @@ const route = useRoute(); | ||
| /** | ||
| * Retrieves signal that indicates whether the route is currently in a *Transition*. | ||
| * Useful for showing stale/pending state when the route resolution is *Suspended* during concurrent rendering. | ||
| * Retrieves a signal that indicates whether the router is currently processing a navigation. | ||
| * Useful for showing pending navigation state while the next route and its data settle. | ||
| * | ||
@@ -464,3 +470,2 @@ * @example | ||
| preload, | ||
| load, | ||
| children, | ||
@@ -473,3 +478,3 @@ info | ||
| component, | ||
| preload: preload || load, | ||
| preload, | ||
| info | ||
@@ -568,3 +573,3 @@ }; | ||
| const key = () => ""; | ||
| const queryFn = on(search, () => extractSearchParams(url())); | ||
| const queryFn = createMemo(() => extractSearchParams(url())); | ||
| return { | ||
@@ -618,36 +623,31 @@ get pathname() { | ||
| } | ||
| const [isRouting, setIsRouting] = createSignal(false); | ||
| const [isRouting, setIsRouting] = createSignal(false, { | ||
| pureWrite: true | ||
| }); | ||
| // Keep track of last target, so that last call to transition wins | ||
| // Navigate override written from event handlers. | ||
| const [navigateTarget, setNavigateTarget] = createSignal(undefined, { | ||
| pureWrite: true | ||
| }); | ||
| // Keep track of last target, so that last call to navigate wins | ||
| let lastTransitionTarget; | ||
| // Transition the location to a new value | ||
| const transition = (newIntent, newTarget) => { | ||
| if (newTarget.value === reference() && newTarget.state === state()) return; | ||
| if (lastTransitionTarget === undefined) setIsRouting(true); | ||
| intent = newIntent; | ||
| lastTransitionTarget = newTarget; | ||
| startTransition(() => { | ||
| if (lastTransitionTarget !== newTarget) return; | ||
| setReference(lastTransitionTarget.value); | ||
| setState(lastTransitionTarget.state); | ||
| resetErrorBoundaries(); | ||
| if (!isServer) submissions[1](subs => subs.filter(s => s.pending)); | ||
| }).finally(() => { | ||
| if (lastTransitionTarget !== newTarget) return; | ||
| // Batch, in order for isRouting and final source update to happen together | ||
| batch(() => { | ||
| intent = undefined; | ||
| if (newIntent === "navigate") navigateEnd(lastTransitionTarget); | ||
| setIsRouting(false); | ||
| lastTransitionTarget = undefined; | ||
| }); | ||
| }); | ||
| }; | ||
| const [reference, setReference] = createSignal(source().value); | ||
| const [state, setState] = createSignal(source().state); | ||
| // source() remains canonical for native history changes; navigateTarget() | ||
| // temporarily overrides it for in-flight programmatic navigation. | ||
| const reference = createMemo(() => { | ||
| const nav = navigateTarget(); | ||
| if (nav !== undefined) return nav.value; | ||
| return source().value; | ||
| }); | ||
| const state = createMemo(() => { | ||
| const nav = navigateTarget(); | ||
| if (nav !== undefined) return nav.state; | ||
| return source().state; | ||
| }); | ||
| const location = createLocation(reference, state, utils.queryWrapper); | ||
| const referrers = []; | ||
| const submissions = createSignal(isServer ? initFromFlash() : []); | ||
| const submissions = createSignal(isServer ? initFromFlash() : [], { | ||
| pureWrite: true | ||
| }); | ||
| const matches = createMemo(() => { | ||
@@ -676,7 +676,2 @@ if (typeof options.transformUrl === "function") { | ||
| }; | ||
| // Create a native transition, when source updates | ||
| createRenderEffect(on(source, source => transition("native", source), { | ||
| defer: true | ||
| })); | ||
| return { | ||
@@ -748,6 +743,25 @@ base: baseRoute, | ||
| }); | ||
| transition("navigate", { | ||
| const newTarget = { | ||
| value: resolvedTo, | ||
| state: nextState | ||
| }); | ||
| }; | ||
| if (lastTransitionTarget === undefined) { | ||
| setIsRouting(true); | ||
| flush(); | ||
| } | ||
| intent = "navigate"; | ||
| lastTransitionTarget = newTarget; | ||
| if (lastTransitionTarget === newTarget) { | ||
| setNavigateTarget({ | ||
| ...lastTransitionTarget | ||
| }); | ||
| queueMicrotask(() => { | ||
| if (lastTransitionTarget !== newTarget) return; | ||
| intent = undefined; | ||
| navigateEnd(lastTransitionTarget); | ||
| setNavigateTarget(undefined); | ||
| setIsRouting(false); | ||
| lastTransitionTarget = undefined; | ||
| }); | ||
| } | ||
| } | ||
@@ -759,3 +773,3 @@ } | ||
| // Workaround for vite issue (https://github.com/vitejs/vite/issues/3803) | ||
| route = route || useContext(RouteContextObj) || baseRoute; | ||
| route = route || useOptionalContext(RouteContextObj) || baseRoute; | ||
| return (to, options) => navigateFromRoute(route, to, options); | ||
@@ -806,3 +820,8 @@ } | ||
| const e = getRequestEvent(); | ||
| return e && e.router && e.router.submission ? [e.router.submission] : []; | ||
| if (!(e && e.router && e.router.submission)) return []; | ||
| return [{ | ||
| ...e.router.submission, | ||
| clear() {}, | ||
| retry() {} | ||
| }]; | ||
| } | ||
@@ -862,3 +881,3 @@ } | ||
| router.create && router.create(routerState); | ||
| return createComponent$1(RouterContextObj.Provider, { | ||
| return createComponent$1(RouterContextObj, { | ||
| value: routerState, | ||
@@ -872,3 +891,3 @@ get children() { | ||
| get preload() { | ||
| return props.rootPreload || props.rootLoad; | ||
| return props.rootPreload; | ||
| }, | ||
@@ -899,11 +918,5 @@ get children() { | ||
| })); | ||
| return createComponent$1(Show, { | ||
| get when() { | ||
| return props.root; | ||
| }, | ||
| keyed: true, | ||
| get fallback() { | ||
| return props.children; | ||
| }, | ||
| children: Root => createComponent$1(Root, { | ||
| const RootComp = props.root; | ||
| if (RootComp) { | ||
| return createComponent$1(RootComp, { | ||
| params: params, | ||
@@ -917,4 +930,5 @@ location: location, | ||
| } | ||
| }) | ||
| }); | ||
| }); | ||
| } | ||
| return props.children; | ||
| } | ||
@@ -942,7 +956,10 @@ function Routes(props) { | ||
| let root; | ||
| const routeStates = createMemo(on(props.routerState.matches, (nextMatches, prevMatches, prev) => { | ||
| let equal = prevMatches && nextMatches.length === prevMatches.length; | ||
| let prevMatches; | ||
| const routeStates = createMemo(prev => { | ||
| const nextMatches = props.routerState.matches(); | ||
| const previousMatches = prevMatches; | ||
| let equal = previousMatches && nextMatches.length === previousMatches.length; | ||
| const next = []; | ||
| for (let i = 0, len = nextMatches.length; i < len; i++) { | ||
| const prevMatch = prevMatches && prevMatches[i]; | ||
| const prevMatch = previousMatches && previousMatches[i]; | ||
| const nextMatch = nextMatches[i]; | ||
@@ -958,3 +975,3 @@ if (prev && prevMatch && nextMatch.route.key === prevMatch.route.key) { | ||
| disposers[i] = dispose; | ||
| next[i] = createRouteContext(props.routerState, next[i - 1] || props.routerState.base, createOutlet(() => routeStates()[i + 1]), () => { | ||
| next[i] = createRouteContext(props.routerState, next[i - 1] || props.routerState.base, createOutlet(() => routeStates()?.[i + 1]), () => { | ||
| const routeMatches = props.routerState.matches(); | ||
@@ -968,26 +985,28 @@ return routeMatches[i] ?? routeMatches[0]; | ||
| if (prev && equal) { | ||
| prevMatches = nextMatches; | ||
| return prev; | ||
| } | ||
| root = next[0]; | ||
| prevMatches = nextMatches; | ||
| return next; | ||
| })); | ||
| }, undefined); | ||
| return createOutlet(() => routeStates() && root)(); | ||
| } | ||
| const createOutlet = child => { | ||
| return () => createComponent$1(Show, { | ||
| get when() { | ||
| return child(); | ||
| }, | ||
| keyed: true, | ||
| children: child => createComponent$1(RouteContextObj.Provider, { | ||
| value: child, | ||
| get children() { | ||
| return child.outlet(); | ||
| } | ||
| }) | ||
| }); | ||
| return () => { | ||
| const c = child(); | ||
| if (c) { | ||
| return createComponent$1(RouteContextObj, { | ||
| value: c, | ||
| get children() { | ||
| return c.outlet(); | ||
| } | ||
| }); | ||
| } | ||
| return undefined; | ||
| }; | ||
| }; | ||
| const Route = props => { | ||
| const childRoutes = children(() => props.children); | ||
| return mergeProps(props, { | ||
| return merge(props, { | ||
| get children() { | ||
@@ -1098,8 +1117,6 @@ return childRoutes(); | ||
| function revalidate(key, force = true) { | ||
| return startTransition(() => { | ||
| const now = Date.now(); | ||
| cacheKeyOp(key, entry => { | ||
| force && (entry[0] = 0); //force cache miss | ||
| entry[4][1](now); // retrigger live signals | ||
| }); | ||
| const now = Date.now(); | ||
| cacheKeyOp(key, entry => { | ||
| force && (entry[0] = 0); //force cache miss | ||
| entry[4][1](now); // retrigger live signals | ||
| }); | ||
@@ -1140,3 +1157,3 @@ } | ||
| } | ||
| if (getListener() && !isServer) { | ||
| if (getObserver() && !isServer) { | ||
| tracking = true; | ||
@@ -1156,3 +1173,3 @@ onCleanup(() => cached[4].count--); | ||
| res = "then" in cached[1] ? cached[1].then(handleResponse(false), handleResponse(true)) : handleResponse(false)(cached[1]); | ||
| !isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version | ||
| !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version | ||
| } | ||
@@ -1172,3 +1189,3 @@ inPreloadFn && "then" in res && res.catch(() => {}); | ||
| cached[3] = intent; | ||
| !isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version | ||
| !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version | ||
| } else { | ||
@@ -1208,6 +1225,4 @@ cache.set(key, cached = [now, res,, intent, createSignal(now)]); | ||
| // client + server relative redirect | ||
| if (navigate && url.startsWith("/")) startTransition(() => { | ||
| navigate(url, { | ||
| replace: true | ||
| }); | ||
| if (navigate && url.startsWith("/")) navigate(url, { | ||
| replace: true | ||
| });else if (!isServer) window.location.href = url;else if (e) e.response.status = 302; | ||
@@ -1248,5 +1263,2 @@ return; | ||
| query.clear = () => getCache().clear(); | ||
| /** @deprecated use query instead */ | ||
| const cache = query; | ||
| function matchKey(key, keys) { | ||
@@ -1272,2 +1284,5 @@ for (let k of keys) { | ||
| const submitHooksSymbol = Symbol("routerActionSubmitHooks"); | ||
| const settledHooksSymbol = Symbol("routerActionSettledHooks"); | ||
| const invokeSymbol = Symbol("routerActionInvoke"); | ||
| const actions = /* #__PURE__ */new Map(); | ||
@@ -1280,3 +1295,2 @@ function useSubmissions(fn, filter) { | ||
| if (property === $TRACK) return subs(); | ||
| if (property === "pending") return subs().some(sub => !sub.result); | ||
| return subs()[property]; | ||
@@ -1289,11 +1303,2 @@ }, | ||
| } | ||
| function useSubmission(fn, filter) { | ||
| const submissions = useSubmissions(fn, filter); | ||
| return new Proxy({}, { | ||
| get(_, property) { | ||
| if (submissions.length === 0 && property === "clear" || property === "retry") return () => {}; | ||
| return submissions[submissions.length - 1]?.[property]; | ||
| } | ||
| }); | ||
| } | ||
| function useAction(action) { | ||
@@ -1305,7 +1310,9 @@ const r = useRouter(); | ||
| } | ||
| function action(fn, options = {}) { | ||
| function mutate(...variables) { | ||
| function actionImpl(fn, options = {}) { | ||
| async function invoke(variables, current) { | ||
| const router = this.r; | ||
| const form = this.f; | ||
| const p = (router.singleFlight && fn.withOptions ? fn.withOptions({ | ||
| const submitHooks = current[submitHooksSymbol]; | ||
| const settledHooks = current[settledHooksSymbol]; | ||
| const runMutation = () => (router.singleFlight && fn.withOptions ? fn.withOptions({ | ||
| headers: { | ||
@@ -1315,46 +1322,48 @@ "X-Single-Flight": "true" | ||
| }) : fn)(...variables); | ||
| const [result, setResult] = createSignal(); | ||
| const run = action$1(async function* (context) { | ||
| context.optimistic?.(); | ||
| try { | ||
| const value = await context.call(); | ||
| yield; | ||
| return { | ||
| error: false, | ||
| value | ||
| }; | ||
| } catch (error) { | ||
| yield; | ||
| return { | ||
| error: true, | ||
| value: error | ||
| }; | ||
| } | ||
| }); | ||
| const settled = await settleActionResult(run({ | ||
| call: runMutation, | ||
| optimistic: submitHooks.size ? () => { | ||
| for (const hook of submitHooks.values()) hook(...variables); | ||
| } : undefined | ||
| })); | ||
| const response = await handleResponse(settled.value, settled.error, router.navigatorFactory()); | ||
| if (!response) return undefined; | ||
| let submission; | ||
| function handler(error) { | ||
| return async res => { | ||
| const result = await handleResponse(res, error, router.navigatorFactory()); | ||
| let retry = null; | ||
| o.onComplete?.({ | ||
| ...submission, | ||
| result: result?.data, | ||
| error: result?.error, | ||
| pending: false, | ||
| retry() { | ||
| return retry = submission.retry(); | ||
| } | ||
| }); | ||
| if (retry) return retry; | ||
| if (!result) return submission.clear(); | ||
| setResult(result); | ||
| if (result.error && !form) throw result.error; | ||
| return result.data; | ||
| }; | ||
| } | ||
| router.submissions[1](s => [...s, submission = { | ||
| submission = { | ||
| input: variables, | ||
| url, | ||
| get result() { | ||
| return result()?.data; | ||
| }, | ||
| get error() { | ||
| return result()?.error; | ||
| }, | ||
| get pending() { | ||
| return !result(); | ||
| }, | ||
| result: response.data, | ||
| error: response.error, | ||
| clear() { | ||
| router.submissions[1](v => v.filter(i => i !== submission)); | ||
| router.submissions[1](entries => entries.filter(entry => entry !== submission)); | ||
| }, | ||
| retry() { | ||
| setResult(undefined); | ||
| const p = fn(...variables); | ||
| return p.then(handler(), handler(true)); | ||
| submission.clear(); | ||
| return current[invokeSymbol].call({ | ||
| r: router, | ||
| f: form | ||
| }, variables, current); | ||
| } | ||
| }]); | ||
| return p.then(handler(), handler(true)); | ||
| }; | ||
| router.submissions[1](entries => [...entries, submission]); | ||
| for (const hook of settledHooks.values()) hook(submission); | ||
| if (response.error && !form) throw response.error; | ||
| return response.data; | ||
| } | ||
@@ -1366,7 +1375,11 @@ const o = typeof options === "string" ? { | ||
| const url = fn.url || name && `https://action/${name}` || ""; | ||
| mutate.base = url; | ||
| if (name) setFunctionName(mutate, name); | ||
| return toAction(mutate, url); | ||
| const wrapped = toAction(invoke, url); | ||
| if (name) setFunctionName(wrapped, name); | ||
| return wrapped; | ||
| } | ||
| function toAction(fn, url) { | ||
| const action = actionImpl; | ||
| function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map(), settledHooks = new Map()) { | ||
| const fn = function (...args) { | ||
| return invoke.call(this, [...boundArgs, ...args], fn); | ||
| }; | ||
| fn.toString = () => { | ||
@@ -1377,11 +1390,24 @@ if (!url) throw new Error("Client Actions need explicit names if server rendered"); | ||
| fn.with = function (...args) { | ||
| const newFn = function (...passedArgs) { | ||
| return fn.call(this, ...args, ...passedArgs); | ||
| }; | ||
| newFn.base = fn.base; | ||
| const uri = new URL(url, mockBase); | ||
| uri.searchParams.set("args", hashKey(args)); | ||
| return toAction(newFn, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search); | ||
| const next = toAction(invoke, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search, [...boundArgs, ...args], base, submitHooks, settledHooks); | ||
| return next; | ||
| }; | ||
| fn.onSubmit = function (hook) { | ||
| const id = Symbol("actionOnSubmitHook"); | ||
| submitHooks.set(id, hook); | ||
| getOwner() && onCleanup(() => submitHooks.delete(id)); | ||
| return this; | ||
| }; | ||
| fn.onSettled = function (hook) { | ||
| const id = Symbol("actionOnSettledHook"); | ||
| settledHooks.set(id, hook); | ||
| getOwner() && onCleanup(() => settledHooks.delete(id)); | ||
| return this; | ||
| }; | ||
| fn.url = url; | ||
| fn.base = base; | ||
| fn[submitHooksSymbol] = submitHooks; | ||
| fn[settledHooksSymbol] = settledHooks; | ||
| fn[invokeSymbol] = invoke; | ||
| if (!isServer) { | ||
@@ -1394,2 +1420,17 @@ actions.set(url, fn); | ||
| const hashString = s => s.split("").reduce((a, b) => (a << 5) - a + b.charCodeAt(0) | 0, 0); | ||
| async function settleActionResult(result) { | ||
| const value = result; | ||
| if (value && typeof value.then === "function") { | ||
| return result.then(value => value); | ||
| } | ||
| if (value && typeof value.next === "function") { | ||
| const iterator = value; | ||
| let next = await iterator.next(); | ||
| while (!next.done) { | ||
| next = await iterator.next(); | ||
| } | ||
| return next.value; | ||
| } | ||
| return result; | ||
| } | ||
| async function handleResponse(response, error, navigate) { | ||
@@ -1703,8 +1744,17 @@ let data; | ||
| var _tmpl$ = /*#__PURE__*/template(`<a>`); | ||
| function toClassName(value) { | ||
| if (!value) return ""; | ||
| if (typeof value === "string" || typeof value === "number") return String(value); | ||
| if (Array.isArray(value)) return value.map(toClassName).filter(Boolean).join(" "); | ||
| if (typeof value === "object") { | ||
| return Object.entries(value).filter(([, enabled]) => enabled).map(([name]) => name).join(" "); | ||
| } | ||
| return ""; | ||
| } | ||
| function A(props) { | ||
| props = mergeProps({ | ||
| props = merge({ | ||
| inactiveClass: "inactive", | ||
| activeClass: "active" | ||
| }, props); | ||
| const [, rest] = splitProps(props, ["href", "state", "class", "activeClass", "inactiveClass", "end"]); | ||
| const rest = omit(props, "href", "state", "class", "activeClass", "inactiveClass", "end"); | ||
| const to = useResolvedPath(() => props.href); | ||
@@ -1720,5 +1770,6 @@ const href = useHref(to); | ||
| }); | ||
| const className = createMemo(() => [toClassName(props.class), isActive()[0] ? props.activeClass : props.inactiveClass].filter(Boolean).join(" ")); | ||
| return (() => { | ||
| var _el$ = _tmpl$(); | ||
| spread(_el$, mergeProps$1(rest, { | ||
| spread(_el$, mergeProps(rest, { | ||
| get href() { | ||
@@ -1730,13 +1781,6 @@ return href() || props.href; | ||
| }, | ||
| get classList() { | ||
| return { | ||
| ...(props.class && { | ||
| [props.class]: true | ||
| }), | ||
| [props.inactiveClass]: !isActive()[0], | ||
| [props.activeClass]: isActive()[0], | ||
| ...rest.classList | ||
| }; | ||
| get ["class"]() { | ||
| return className(); | ||
| }, | ||
| "link": "", | ||
| "link": true, | ||
| get ["aria-current"]() { | ||
@@ -1767,95 +1811,2 @@ return isActive()[1] ? "page" : undefined; | ||
| /** | ||
| * This is mock of the eventual Solid 2.0 primitive. It is not fully featured. | ||
| */ | ||
| /** | ||
| * As `createAsync` and `createAsyncStore` are wrappers for `createResource`, | ||
| * this type allows to support `latest` field for these primitives. | ||
| * It will be removed in the future. | ||
| */ | ||
| function createAsync(fn, options) { | ||
| let resource; | ||
| let prev = () => !resource || resource.state === "unresolved" ? undefined : resource.latest; | ||
| [resource] = createResource(() => subFetch(fn, catchError(() => untrack(prev), () => undefined)), v => v, options); | ||
| const resultAccessor = () => resource(); | ||
| if (options?.name) setFunctionName(resultAccessor, options.name); | ||
| Object.defineProperty(resultAccessor, "latest", { | ||
| get() { | ||
| return resource.latest; | ||
| } | ||
| }); | ||
| return resultAccessor; | ||
| } | ||
| function createAsyncStore(fn, options = {}) { | ||
| let resource; | ||
| let prev = () => !resource || resource.state === "unresolved" ? undefined : unwrap(resource.latest); | ||
| [resource] = createResource(() => subFetch(fn, catchError(() => untrack(prev), () => undefined)), v => v, { | ||
| ...options, | ||
| storage: init => createDeepSignal(init, options.reconcile) | ||
| }); | ||
| const resultAccessor = () => resource(); | ||
| Object.defineProperty(resultAccessor, "latest", { | ||
| get() { | ||
| return resource.latest; | ||
| } | ||
| }); | ||
| return resultAccessor; | ||
| } | ||
| function createDeepSignal(value, options) { | ||
| const [store, setStore] = createStore({ | ||
| value: structuredClone(value) | ||
| }); | ||
| return [() => store.value, v => { | ||
| typeof v === "function" && (v = v()); | ||
| setStore("value", reconcile(structuredClone(v), options)); | ||
| return store.value; | ||
| }]; | ||
| } | ||
| // mock promise while hydrating to prevent fetching | ||
| class MockPromise { | ||
| static all() { | ||
| return new MockPromise(); | ||
| } | ||
| static allSettled() { | ||
| return new MockPromise(); | ||
| } | ||
| static any() { | ||
| return new MockPromise(); | ||
| } | ||
| static race() { | ||
| return new MockPromise(); | ||
| } | ||
| static reject() { | ||
| return new MockPromise(); | ||
| } | ||
| static resolve() { | ||
| return new MockPromise(); | ||
| } | ||
| catch() { | ||
| return new MockPromise(); | ||
| } | ||
| then() { | ||
| return new MockPromise(); | ||
| } | ||
| finally() { | ||
| return new MockPromise(); | ||
| } | ||
| } | ||
| function subFetch(fn, prev) { | ||
| if (isServer || !sharedConfig.context) return fn(prev); | ||
| const ogFetch = fetch; | ||
| const ogPromise = Promise; | ||
| try { | ||
| window.fetch = () => new MockPromise(); | ||
| Promise = MockPromise; | ||
| return fn(prev); | ||
| } finally { | ||
| window.fetch = ogFetch; | ||
| Promise = ogPromise; | ||
| } | ||
| } | ||
| function redirect(url, init = 302) { | ||
@@ -1914,2 +1865,2 @@ let responseInit; | ||
| export { A, HashRouter, MemoryRouter, Navigate, Route, Router, RouterContextObj as RouterContext, StaticRouter, mergeSearchString as _mergeSearchString, action, cache, createAsync, createAsyncStore, createBeforeLeave, createMemoryHistory, createRouter, json, keepDepth, notifyIfNotBlocked, query, redirect, reload, revalidate, saveCurrentDepth, useAction, useBeforeLeave, useCurrentMatches, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useSearchParams, useSubmission, useSubmissions }; | ||
| export { A, HashRouter, MemoryRouter, Navigate, Route, Router, RouterContextObj as RouterContext, StaticRouter, mergeSearchString as _mergeSearchString, action, createBeforeLeave, createMemoryHistory, createRouter, json, keepDepth, notifyIfNotBlocked, query, redirect, reload, revalidate, saveCurrentDepth, useAction, useBeforeLeave, useCurrentMatches, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useSearchParams, useSubmissions }; |
@@ -1,2 +0,2 @@ | ||
| import { isServer } from "solid-js/web"; | ||
| import { isServer } from "@solidjs/web"; | ||
| export function createBeforeLeave() { | ||
@@ -3,0 +3,0 @@ let listeners = new Set(); |
@@ -13,4 +13,2 @@ import type { Component, JSX } from "solid-js"; | ||
| transformUrl?: (url: string) => string; | ||
| /** @deprecated use rootPreload */ | ||
| rootLoad?: RoutePreloadFunc; | ||
| }; | ||
@@ -25,5 +23,3 @@ export declare const createRouterComponent: (router: RouterIntegration) => (props: BaseRouterProps) => JSX.Element; | ||
| info?: Record<string, any>; | ||
| /** @deprecated use preload */ | ||
| load?: RoutePreloadFunc<T>; | ||
| }; | ||
| export declare const Route: <S extends string, T = unknown>(props: RouteProps<S, T>) => JSX.Element; |
| /*@refresh skip*/ | ||
| import { children, createMemo, createRoot, getOwner, mergeProps, on, Show, untrack } from "solid-js"; | ||
| import { getRequestEvent, isServer } from "solid-js/web"; | ||
| import { children, createMemo, createRoot, getOwner, merge, untrack } from "solid-js"; | ||
| import { getRequestEvent, isServer } from "@solidjs/web"; | ||
| import { createBranches, createRouteContext, createRouterContext, getIntent, getRouteMatches, RouteContextObj, RouterContextObj, setInPreloadFn } from "../routing.js"; | ||
@@ -16,8 +16,8 @@ export const createRouterComponent = (router) => (props) => { | ||
| router.create && router.create(routerState); | ||
| return (<RouterContextObj.Provider value={routerState}> | ||
| <Root routerState={routerState} root={props.root} preload={props.rootPreload || props.rootLoad}> | ||
| return (<RouterContextObj value={routerState}> | ||
| <Root routerState={routerState} root={props.root} preload={props.rootPreload}> | ||
| {(context = getOwner()) && null} | ||
| <Routes routerState={routerState} branches={branches()}/> | ||
| </Root> | ||
| </RouterContextObj.Provider>); | ||
| </RouterContextObj>); | ||
| }; | ||
@@ -33,7 +33,9 @@ function Root(props) { | ||
| })); | ||
| return (<Show when={props.root} keyed fallback={props.children}> | ||
| {Root => (<Root params={params} location={location} data={data()}> | ||
| {props.children} | ||
| </Root>)} | ||
| </Show>); | ||
| const RootComp = props.root; | ||
| if (RootComp) { | ||
| return (<RootComp params={params} location={location} data={data()}> | ||
| {props.children} | ||
| </RootComp>); | ||
| } | ||
| return props.children; | ||
| } | ||
@@ -59,7 +61,10 @@ function Routes(props) { | ||
| let root; | ||
| const routeStates = createMemo(on(props.routerState.matches, (nextMatches, prevMatches, prev) => { | ||
| let equal = prevMatches && nextMatches.length === prevMatches.length; | ||
| let prevMatches; | ||
| const routeStates = createMemo((prev) => { | ||
| const nextMatches = props.routerState.matches(); | ||
| const previousMatches = prevMatches; | ||
| let equal = previousMatches && nextMatches.length === previousMatches.length; | ||
| const next = []; | ||
| for (let i = 0, len = nextMatches.length; i < len; i++) { | ||
| const prevMatch = prevMatches && prevMatches[i]; | ||
| const prevMatch = previousMatches && previousMatches[i]; | ||
| const nextMatch = nextMatches[i]; | ||
@@ -76,3 +81,3 @@ if (prev && prevMatch && nextMatch.route.key === prevMatch.route.key) { | ||
| disposers[i] = dispose; | ||
| next[i] = createRouteContext(props.routerState, next[i - 1] || props.routerState.base, createOutlet(() => routeStates()[i + 1]), () => { | ||
| next[i] = createRouteContext(props.routerState, next[i - 1] || props.routerState.base, createOutlet(() => routeStates()?.[i + 1]), () => { | ||
| const routeMatches = props.routerState.matches(); | ||
@@ -86,17 +91,23 @@ return routeMatches[i] ?? routeMatches[0]; | ||
| if (prev && equal) { | ||
| prevMatches = nextMatches; | ||
| return prev; | ||
| } | ||
| root = next[0]; | ||
| prevMatches = nextMatches; | ||
| return next; | ||
| })); | ||
| }, undefined); | ||
| return createOutlet(() => routeStates() && root)(); | ||
| } | ||
| const createOutlet = (child) => { | ||
| return () => (<Show when={child()} keyed> | ||
| {child => <RouteContextObj.Provider value={child}>{child.outlet()}</RouteContextObj.Provider>} | ||
| </Show>); | ||
| return () => { | ||
| const c = child(); | ||
| if (c) { | ||
| return <RouteContextObj value={c}>{c.outlet()}</RouteContextObj>; | ||
| } | ||
| return undefined; | ||
| }; | ||
| }; | ||
| export const Route = (props) => { | ||
| const childRoutes = children(() => props.children); | ||
| return mergeProps(props, { | ||
| return merge(props, { | ||
| get children() { | ||
@@ -103,0 +114,0 @@ return childRoutes(); |
@@ -1,2 +0,2 @@ | ||
| import { isServer } from "solid-js/web"; | ||
| import { isServer } from "@solidjs/web"; | ||
| import { createRouter, scrollToHash, bindEvent } from "./createRouter.js"; | ||
@@ -3,0 +3,0 @@ import { StaticRouter } from "./StaticRouter.js"; |
@@ -1,2 +0,2 @@ | ||
| import { getRequestEvent } from "solid-js/web"; | ||
| import { getRequestEvent } from "@solidjs/web"; | ||
| import { createRouterComponent } from "./components.jsx"; | ||
@@ -3,0 +3,0 @@ function getPath(url) { |
@@ -42,4 +42,4 @@ import { JSX, Accessor } from "solid-js"; | ||
| /** | ||
| * Retrieves signal that indicates whether the route is currently in a *Transition*. | ||
| * Useful for showing stale/pending state when the route resolution is *Suspended* during concurrent rendering. | ||
| * Retrieves a signal that indicates whether the router is currently processing a navigation. | ||
| * Useful for showing pending navigation state while the next route and its data settle. | ||
| * | ||
@@ -46,0 +46,0 @@ * @example |
+70
-49
@@ -1,4 +0,4 @@ | ||
| import { runWithOwner, batch } from "solid-js"; | ||
| import { createComponent, createContext, createMemo, createRenderEffect, createSignal, on, onCleanup, untrack, useContext, startTransition, resetErrorBoundaries } from "solid-js"; | ||
| import { isServer, getRequestEvent } from "solid-js/web"; | ||
| import { flush, runWithOwner } from "solid-js"; | ||
| import { createComponent, createContext, createMemo, createSignal, onCleanup, untrack, useContext } from "solid-js"; | ||
| import { isServer, getRequestEvent } from "@solidjs/web"; | ||
| import { createBeforeLeave } from "./lifecycle.js"; | ||
@@ -10,5 +10,13 @@ import { mockBase, createMemoObject, extractSearchParams, invariant, resolvePath, createMatcher, joinPaths, scoreRoute, mergeSearchString, expandOptionals } from "./utils.js"; | ||
| export const RouteContextObj = createContext(); | ||
| function useOptionalContext(context) { | ||
| try { | ||
| return useContext(context); | ||
| } | ||
| catch { | ||
| return undefined; | ||
| } | ||
| } | ||
| export const useRouter = () => invariant(useContext(RouterContextObj), "<A> and 'use' router primitives can be only used inside a Route."); | ||
| let TempRoute; | ||
| export const useRoute = () => TempRoute || useContext(RouteContextObj) || useRouter().base; | ||
| export const useRoute = () => TempRoute || useOptionalContext(RouteContextObj) || useRouter().base; | ||
| export const useResolvedPath = (path) => { | ||
@@ -57,4 +65,4 @@ const route = useRoute(); | ||
| /** | ||
| * Retrieves signal that indicates whether the route is currently in a *Transition*. | ||
| * Useful for showing stale/pending state when the route resolution is *Suspended* during concurrent rendering. | ||
| * Retrieves a signal that indicates whether the router is currently processing a navigation. | ||
| * Useful for showing pending navigation state while the next route and its data settle. | ||
| * | ||
@@ -210,3 +218,3 @@ * @example | ||
| export function createRoutes(routeDef, base = "") { | ||
| const { component, preload, load, children, info } = routeDef; | ||
| const { component, preload, children, info } = routeDef; | ||
| const isLeaf = !children || (Array.isArray(children) && !children.length); | ||
@@ -216,3 +224,3 @@ const shared = { | ||
| component, | ||
| preload: preload || load, | ||
| preload, | ||
| info | ||
@@ -316,3 +324,3 @@ }; | ||
| const key = () => ""; | ||
| const queryFn = on(search, () => extractSearchParams(url())); | ||
| const queryFn = createMemo(() => extractSearchParams(url())); | ||
| return { | ||
@@ -360,39 +368,28 @@ get pathname() { | ||
| } | ||
| const [isRouting, setIsRouting] = createSignal(false); | ||
| // Keep track of last target, so that last call to transition wins | ||
| const [isRouting, setIsRouting] = createSignal(false, { pureWrite: true }); | ||
| // Navigate override written from event handlers. | ||
| const [navigateTarget, setNavigateTarget] = createSignal(undefined, { | ||
| pureWrite: true | ||
| }); | ||
| // Keep track of last target, so that last call to navigate wins | ||
| let lastTransitionTarget; | ||
| // Transition the location to a new value | ||
| const transition = (newIntent, newTarget) => { | ||
| if (newTarget.value === reference() && newTarget.state === state()) | ||
| return; | ||
| if (lastTransitionTarget === undefined) | ||
| setIsRouting(true); | ||
| intent = newIntent; | ||
| lastTransitionTarget = newTarget; | ||
| startTransition(() => { | ||
| if (lastTransitionTarget !== newTarget) | ||
| return; | ||
| setReference(lastTransitionTarget.value); | ||
| setState(lastTransitionTarget.state); | ||
| resetErrorBoundaries(); | ||
| if (!isServer) | ||
| submissions[1](subs => subs.filter(s => s.pending)); | ||
| }).finally(() => { | ||
| if (lastTransitionTarget !== newTarget) | ||
| return; | ||
| // Batch, in order for isRouting and final source update to happen together | ||
| batch(() => { | ||
| intent = undefined; | ||
| if (newIntent === "navigate") | ||
| navigateEnd(lastTransitionTarget); | ||
| setIsRouting(false); | ||
| lastTransitionTarget = undefined; | ||
| }); | ||
| }); | ||
| }; | ||
| const [reference, setReference] = createSignal(source().value); | ||
| const [state, setState] = createSignal(source().state); | ||
| // source() remains canonical for native history changes; navigateTarget() | ||
| // temporarily overrides it for in-flight programmatic navigation. | ||
| const reference = createMemo(() => { | ||
| const nav = navigateTarget(); | ||
| if (nav !== undefined) | ||
| return nav.value; | ||
| return source().value; | ||
| }); | ||
| const state = createMemo(() => { | ||
| const nav = navigateTarget(); | ||
| if (nav !== undefined) | ||
| return nav.state; | ||
| return source().state; | ||
| }); | ||
| const location = createLocation(reference, state, utils.queryWrapper); | ||
| const referrers = []; | ||
| const submissions = createSignal(isServer ? initFromFlash() : []); | ||
| const submissions = createSignal(isServer ? initFromFlash() : [], { | ||
| pureWrite: true | ||
| }); | ||
| const matches = createMemo(() => { | ||
@@ -423,4 +420,2 @@ if (typeof options.transformUrl === "function") { | ||
| }; | ||
| // Create a native transition, when source updates | ||
| createRenderEffect(on(source, source => transition("native", source), { defer: true })); | ||
| return { | ||
@@ -480,6 +475,24 @@ base: baseRoute, | ||
| referrers.push({ value: current, replace, scroll, state: state() }); | ||
| transition("navigate", { | ||
| const newTarget = { | ||
| value: resolvedTo, | ||
| state: nextState | ||
| }); | ||
| }; | ||
| if (lastTransitionTarget === undefined) { | ||
| setIsRouting(true); | ||
| flush(); | ||
| } | ||
| intent = "navigate"; | ||
| lastTransitionTarget = newTarget; | ||
| if (lastTransitionTarget === newTarget) { | ||
| setNavigateTarget({ ...lastTransitionTarget }); | ||
| queueMicrotask(() => { | ||
| if (lastTransitionTarget !== newTarget) | ||
| return; | ||
| intent = undefined; | ||
| navigateEnd(lastTransitionTarget); | ||
| setNavigateTarget(undefined); | ||
| setIsRouting(false); | ||
| lastTransitionTarget = undefined; | ||
| }); | ||
| } | ||
| } | ||
@@ -491,3 +504,3 @@ } | ||
| // Workaround for vite issue (https://github.com/vitejs/vite/issues/3803) | ||
| route = route || useContext(RouteContextObj) || baseRoute; | ||
| route = route || useOptionalContext(RouteContextObj) || baseRoute; | ||
| return (to, options) => navigateFromRoute(route, to, options); | ||
@@ -537,3 +550,11 @@ } | ||
| const e = getRequestEvent(); | ||
| return (e && e.router && e.router.submission ? [e.router.submission] : []); | ||
| if (!(e && e.router && e.router.submission)) | ||
| return []; | ||
| return [ | ||
| { | ||
| ...e.router.submission, | ||
| clear() { }, | ||
| retry() { } | ||
| } | ||
| ]; | ||
| } | ||
@@ -540,0 +561,0 @@ } |
+2
-17
| import type { Component, JSX, Signal } from "solid-js"; | ||
| declare module "solid-js/web" { | ||
| declare module "@solidjs/web" { | ||
| interface RequestEvent { | ||
@@ -15,2 +15,3 @@ response: { | ||
| result: any; | ||
| error?: any; | ||
| url: string; | ||
@@ -82,4 +83,2 @@ }; | ||
| info?: Record<string, any>; | ||
| /** @deprecated use preload */ | ||
| load?: RoutePreloadFunc; | ||
| }; | ||
@@ -171,3 +170,2 @@ export type MatchFilter = readonly string[] | RegExp | ((s: string) => boolean); | ||
| readonly error: any; | ||
| readonly pending: boolean; | ||
| readonly url: string; | ||
@@ -177,11 +175,2 @@ clear: () => void; | ||
| }; | ||
| export type SubmissionStub = { | ||
| readonly input: undefined; | ||
| readonly result: undefined; | ||
| readonly error: undefined; | ||
| readonly pending: undefined; | ||
| readonly url: undefined; | ||
| clear: () => void; | ||
| retry: () => void; | ||
| }; | ||
| export interface MaybePreloadableComponent extends Component { | ||
@@ -201,5 +190,1 @@ preload?: () => void; | ||
| }; | ||
| /** @deprecated */ | ||
| export type RouteLoadFunc = RoutePreloadFunc; | ||
| /** @deprecated */ | ||
| export type RouteLoadFuncArgs = RoutePreloadFuncArgs; |
+7
-5
@@ -9,3 +9,3 @@ { | ||
| "license": "MIT", | ||
| "version": "0.16.1", | ||
| "version": "0.17.0-next.0", | ||
| "homepage": "https://github.com/solidjs/solid-router#readme", | ||
@@ -33,2 +33,3 @@ "repository": { | ||
| "devDependencies": { | ||
| "@solidjs/web": "2.0.0-beta.3", | ||
| "@babel/core": "^7.26.0", | ||
@@ -42,14 +43,15 @@ "@babel/preset-typescript": "^7.26.0", | ||
| "@types/node": "^22.10.0", | ||
| "babel-preset-solid": "^1.9.3", | ||
| "babel-preset-solid": "2.0.0-beta.3", | ||
| "jsdom": "^25.0.1", | ||
| "prettier": "^3.4.1", | ||
| "rollup": "^4.27.4", | ||
| "solid-js": "^1.9.3", | ||
| "solid-js": "2.0.0-beta.3", | ||
| "typescript": "^5.7.2", | ||
| "vite": "^6.0.0", | ||
| "vite-plugin-solid": "^2.11.0", | ||
| "vite-plugin-solid": "3.0.0-next.2", | ||
| "vitest": "^2.1.6" | ||
| }, | ||
| "peerDependencies": { | ||
| "solid-js": "^1.8.6" | ||
| "@solidjs/web": "^2.0.0-beta.3", | ||
| "solid-js": "^2.0.0-beta.3" | ||
| }, | ||
@@ -56,0 +58,0 @@ "scripts": { |
+85
-79
@@ -67,3 +67,3 @@ [](https://github.com/solidjs) | ||
| ```jsx | ||
| import { render } from "solid-js/web"; | ||
| import { render } from "@solidjs/web"; | ||
| import { Router } from "@solidjs/router"; | ||
@@ -83,3 +83,3 @@ | ||
| ```jsx | ||
| import { render } from "solid-js/web"; | ||
| import { render } from "@solidjs/web"; | ||
| import { Router, Route } from "@solidjs/router"; | ||
@@ -106,3 +106,3 @@ | ||
| ```jsx | ||
| import { render } from "solid-js/web"; | ||
| import { render } from "@solidjs/web"; | ||
| import { Router, Route } from "@solidjs/router"; | ||
@@ -136,3 +136,3 @@ | ||
| ```jsx | ||
| import { render } from "solid-js/web"; | ||
| import { render } from "@solidjs/web"; | ||
| import { Router, Route } from "@solidjs/router"; | ||
@@ -169,3 +169,3 @@ | ||
| import { lazy } from "solid-js"; | ||
| import { render } from "solid-js/web"; | ||
| import { render } from "@solidjs/web"; | ||
| import { Router, Route } from "@solidjs/router"; | ||
@@ -200,3 +200,3 @@ | ||
| import { lazy } from "solid-js"; | ||
| import { render } from "solid-js/web"; | ||
| import { render } from "@solidjs/web"; | ||
| import { Router, Route } from "@solidjs/router"; | ||
@@ -235,3 +235,3 @@ | ||
| import { lazy } from "solid-js"; | ||
| import { render } from "solid-js/web"; | ||
| import { render } from "@solidjs/web"; | ||
| import { Router, Route } from "@solidjs/router"; | ||
@@ -275,3 +275,3 @@ | ||
| import { lazy } from "solid-js"; | ||
| import { render } from "solid-js/web"; | ||
| import { render } from "@solidjs/web"; | ||
| import { Router, Route } from "@solidjs/router"; | ||
@@ -509,5 +509,6 @@ import type { MatchFilters } from "@solidjs/router"; | ||
| import { getUser } from ... // the query function | ||
| import { createMemo } from "solid-js"; | ||
| export default function User(props) { | ||
| const user = createAsync(() => getUser(props.params.id)); | ||
| const user = createMemo(() => getUser(props.params.id)); | ||
| return <h1>{user().name}</h1>; | ||
@@ -530,31 +531,20 @@ } | ||
| ### `createAsync` | ||
| ### Async reads in Solid 2 | ||
| This is light wrapper over `createResource` that aims to serve as stand-in for a future primitive we intend to bring to Solid core in 2.0. It is a simpler async primitive where the function tracks like `createMemo` and it expects a promise back that it turns into a Signal. Reading it before it is ready causes Suspense/Transitions to trigger. | ||
| On this Solid 2 branch, `query()` results are meant to be consumed directly with Solid primitives like `createMemo` and `createProjection`. | ||
| ```jsx | ||
| const user = createAsync((currentValue) => getUser(params.id)); | ||
| const user = createMemo(() => getUser(params.id)); | ||
| return <h1>{user().name}</h1>; | ||
| ``` | ||
| It also preserves `latest` field from `createResource`. Note that it will be removed in the future. | ||
| For object-shaped data where you want a deeply reactive result, use `createProjection`. | ||
| ```jsx | ||
| const user = createAsync((currentValue) => getUser(params.id)); | ||
| return <h1>{user.latest.name}</h1>; | ||
| const todos = createProjection(() => getTodos(), []); | ||
| ``` | ||
| Using `query` in `createResource` directly won't work properly as the fetcher is not reactive and it won't invalidate properly. | ||
| ### `createAsyncStore` | ||
| Similar to `createAsync` except it uses a deeply reactive store. Perfect for applying fine-grained changes to large model data that updates. | ||
| It also supports `latest` field which will be removed in the future. | ||
| ```jsx | ||
| const todos = createAsyncStore(() => getTodos()); | ||
| ``` | ||
| ### `action` | ||
| Actions are data mutations that can trigger invalidations and further routing. A list of prebuilt response helpers can be found below. | ||
| Router `action()` is the router-aware mutation wrapper for Solid 2. It keeps form submission, redirects, and invalidation wired into the router while letting you compose optimistic UI with Solid's built-in primitives. | ||
@@ -579,2 +569,25 @@ ```jsx | ||
| For optimistic updates, use Solid's optimistic primitives for the rendered state and attach owner-scoped submit hooks to the router action: | ||
| ```jsx | ||
| import { createOptimisticStore } from "solid-js"; | ||
| import { action, query } from "@solidjs/router"; | ||
| const getTodos = query(async () => fetchTodos(), "todos"); | ||
| const [todos, setTodos] = createOptimisticStore(() => getTodos(), []); | ||
| const addTodo = action(async (todo) => { | ||
| await saveTodo(todo); | ||
| return { ok: true, todo }; | ||
| }, "add-todo").onSubmit(todo => { | ||
| setTodos(items => { | ||
| items.push({ ...todo, pending: true }); | ||
| }); | ||
| }); | ||
| ``` | ||
| `myAction.onSubmit(...)` registers a listener for that action in the current reactive owner. Multiple components can register hooks against the same action, and those hooks are automatically removed when their owner is disposed. `myAction.onSettled(...)` works the same way for observing completed submissions. | ||
| The preferred pattern is for actions to return values and let the client interpret the result. Throwing errors is still supported, but `Submission.error` is mainly an escape hatch for that legacy style. | ||
| Sometimes it might be easier to deal with typed data instead of `FormData` and adding additional hidden fields. For that reason Actions have a with method. That works similar to `bind` which applies the arguments in order. | ||
@@ -606,3 +619,3 @@ | ||
| Actions also take a second argument which can be the name or an option object with `name` and `onComplete`. `name` is used to identify SSR actions that aren't server functions (see note below). `onComplete` allows you to configure behavior when `action`s complete. Keep in mind `onComplete` does not work when JavaScript is disabled. | ||
| Actions also take a second argument which can be the name or an option object with `name`. `name` is used to identify SSR actions that aren't server functions (see note below). | ||
@@ -629,5 +642,5 @@ #### Notes on `<form>` implementation and SSR | ||
| ### `useSubmission`/`useSubmissions` | ||
| ### `useSubmissions` | ||
| Are used to injecting the optimistic updates while actions are in flight. They either return a single Submission(latest) or all that match with an optional filter function. | ||
| This returns settled submission records for an action. It is useful for reading completed results, clearing old submissions, retrying a prior submission, or replaying settled errors. It is not the optimistic state layer. | ||
@@ -638,3 +651,3 @@ ```jsx | ||
| readonly result?: U; | ||
| readonly pending: boolean; | ||
| readonly error: any; | ||
| readonly url: string; | ||
@@ -646,5 +659,7 @@ clear: () => void; | ||
| const submissions = useSubmissions(action, (input) => filter(input)); | ||
| const submission = useSubmission(action, (input) => filter(input)); | ||
| const latestSubmission = submissions.at(-1); | ||
| ``` | ||
| Use Solid's `createOptimistic` or `createOptimisticStore` for in-flight UI, and use submissions as the durable settled record layer. | ||
| ### Response Helpers | ||
@@ -688,3 +703,3 @@ | ||
| import { lazy } from "solid-js"; | ||
| import { render } from "solid-js/web"; | ||
| import { render } from "@solidjs/web"; | ||
| import { Router } from "@solidjs/router"; | ||
@@ -732,3 +747,3 @@ | ||
| import { lazy } from "solid-js"; | ||
| import { render } from "solid-js/web"; | ||
| import { render } from "@solidjs/web"; | ||
| import { Router } from "@solidjs/router"; | ||
@@ -771,3 +786,3 @@ | ||
| ```jsx | ||
| import { isServer } from "solid-js/web"; | ||
| import { isServer } from "@solidjs/web"; | ||
| import { Router } from "@solidjs/router"; | ||
@@ -905,3 +920,3 @@ | ||
| Retrieves signal that indicates whether the route is currently in a Transition. Useful for showing stale/pending state when the route resolution is Suspended during concurrent rendering. | ||
| Retrieves a signal that indicates whether the router is currently processing a navigation. Useful for showing pending navigation state while the next route and its data settle. | ||
@@ -912,3 +927,3 @@ ```js | ||
| return ( | ||
| <div classList={{ "grey-out": isRouting() }}> | ||
| <div class={{ "grey-out": isRouting() }}> | ||
| <MyAwesomeContent /> | ||
@@ -926,3 +941,3 @@ </div> | ||
| return <div classList={{ active: Boolean(match()) }} />; | ||
| return <div class={{ active: Boolean(match()) }} />; | ||
| ``` | ||
@@ -982,58 +997,49 @@ | ||
| ## Migrations from 0.9.x | ||
| ## Migration from 0.16.x | ||
| v0.10.0 brings some big changes to support the future of routing including Islands/Partial Hydration hybrid solutions. Most notably there is no Context API available in non-hydrating parts of the application. | ||
| This branch is the Solid 2 migration. Most route configuration stays the same, but the data APIs and recommended async patterns have changed. | ||
| The biggest changes are around removed APIs that need to be replaced. | ||
| ### Async reads move to Solid 2 primitives | ||
| ### `<Outlet>`, `<Routes>`, `useRoutes` | ||
| `createAsync` and `createAsyncStore` are gone. Read query results with Solid 2 primitives like `createMemo`, `createProjection`, `createOptimistic`, and `createOptimisticStore`. | ||
| This is no longer used and instead will use `props.children` passed from into the page components for outlets. This keeps the outlet directly passed from its page and avoids oddness of trying to use context across Islands boundaries. Nested `<Routes>` components inherently cause waterfalls and are `<Outlets>` themselves so they have the same concerns. | ||
| ```jsx | ||
| const user = createMemo(() => getUser(params.id)); | ||
| const [todos, setTodos] = createOptimisticStore(() => getTodos(), []); | ||
| ``` | ||
| Keep in mind no `<Routes>` means the `<Router>` API is different. The `<Router>` acts as the `<Routes>` component and its children can only be `<Route>` components. Your top-level layout should go in the root prop of the router [as shown above](#configure-your-routes) | ||
| ### `query()` stays the source of truth | ||
| ## `element` prop removed from `Route` | ||
| Continue using `query()` for cached reads and invalidation, but consume the results directly through Solid 2's async primitives instead of router-specific wrappers. | ||
| Related without Outlet component it has to be passed in manually. At which point the `element` prop has less value. Removing the second way to define route components to reduce confusion and edge cases. | ||
| ### `action()` lifecycle hooks changed | ||
| ### `data` functions & `useRouteData` | ||
| The action API is now centered around instance methods: | ||
| These have been replaced by a preload mechanism. This allows link hover preloads (as the preload function can be run as much as wanted without worry about reactivity). It support deduping/query APIs which give more control over how things are cached. It also addresses TS issues with getting the right types in the Component without `typeof` checks. | ||
| ```jsx | ||
| const saveTodo = action(async (todo) => { | ||
| await api.saveTodo(todo); | ||
| return { ok: true, todo }; | ||
| }, "save-todo") | ||
| .onSubmit(todo => { | ||
| // optimistic write | ||
| }) | ||
| .onSettled(submission => { | ||
| // observe settled result or retry state | ||
| }); | ||
| ``` | ||
| That being said you can reproduce the old pattern largely by turning off preloads at the router level and then injecting your own Context: | ||
| - Use `onSubmit(...)` for owner-scoped optimistic/pre-submit work. | ||
| - Use `onSettled(...)` for owner-scoped observation of completed submissions. | ||
| - Use returned values for expected application-level results. Thrown errors are still captured on `Submission.error` when something fails unexpectedly. | ||
| ```js | ||
| import { lazy } from "solid-js"; | ||
| import { Route } from "@solidjs/router"; | ||
| ### `useSubmissions()` is the submission API | ||
| const User = lazy(() => import("./pages/users/[id].js")); | ||
| Submissions are now settled history, not in-flight mutation state. Read them through `useSubmissions()` and select the latest entry with `at(-1)` when needed. | ||
| // preload function | ||
| function preloadUser({ params, location }) { | ||
| const [user] = createResource(() => params.id, fetchUser); | ||
| return user; | ||
| } | ||
| // Pass it in the route definition | ||
| <Router preload={false}> | ||
| <Route path="/users/:id" component={User} preload={preloadUser} /> | ||
| </Router>; | ||
| ```jsx | ||
| const submissions = useSubmissions(saveTodo); | ||
| const latestSubmission = submissions.at(-1); | ||
| ``` | ||
| And then in your component taking the page props and putting them in a Context. | ||
| ```js | ||
| function User(props) { | ||
| <UserContext.Provider value={props.data}> | ||
| {/* my component content */} | ||
| </UserContext.Provider>; | ||
| } | ||
| // Somewhere else | ||
| function UserDetails() { | ||
| const user = useContext(UserContext); | ||
| // render stuff | ||
| } | ||
| ``` | ||
| ## SPAs in Deployed Environments | ||
@@ -1040,0 +1046,0 @@ |
| import { type ReconcileOptions } from "solid-js/store"; | ||
| /** | ||
| * As `createAsync` and `createAsyncStore` are wrappers for `createResource`, | ||
| * this type allows to support `latest` field for these primitives. | ||
| * It will be removed in the future. | ||
| */ | ||
| export type AccessorWithLatest<T> = { | ||
| (): T; | ||
| latest: T; | ||
| }; | ||
| export declare function createAsync<T>(fn: (prev: T) => Promise<T>, options: { | ||
| name?: string; | ||
| initialValue: T; | ||
| deferStream?: boolean; | ||
| }): AccessorWithLatest<T>; | ||
| export declare function createAsync<T>(fn: (prev: T | undefined) => Promise<T>, options?: { | ||
| name?: string; | ||
| initialValue?: T; | ||
| deferStream?: boolean; | ||
| }): AccessorWithLatest<T | undefined>; | ||
| export declare function createAsyncStore<T>(fn: (prev: T) => Promise<T>, options: { | ||
| name?: string; | ||
| initialValue: T; | ||
| deferStream?: boolean; | ||
| reconcile?: ReconcileOptions; | ||
| }): AccessorWithLatest<T>; | ||
| export declare function createAsyncStore<T>(fn: (prev: T | undefined) => Promise<T>, options?: { | ||
| name?: string; | ||
| initialValue?: T; | ||
| deferStream?: boolean; | ||
| reconcile?: ReconcileOptions; | ||
| }): AccessorWithLatest<T | undefined>; |
| /** | ||
| * This is mock of the eventual Solid 2.0 primitive. It is not fully featured. | ||
| */ | ||
| import { createResource, sharedConfig, untrack, catchError } from "solid-js"; | ||
| import { createStore, reconcile, unwrap } from "solid-js/store"; | ||
| import { isServer } from "solid-js/web"; | ||
| import { setFunctionName } from "../utils.js"; | ||
| export function createAsync(fn, options) { | ||
| let resource; | ||
| let prev = () => !resource || resource.state === "unresolved" ? undefined : resource.latest; | ||
| [resource] = createResource(() => subFetch(fn, catchError(() => untrack(prev), () => undefined)), v => v, options); | ||
| const resultAccessor = (() => resource()); | ||
| if (options?.name) | ||
| setFunctionName(resultAccessor, options.name); | ||
| Object.defineProperty(resultAccessor, "latest", { | ||
| get() { | ||
| return resource.latest; | ||
| } | ||
| }); | ||
| return resultAccessor; | ||
| } | ||
| export function createAsyncStore(fn, options = {}) { | ||
| let resource; | ||
| let prev = () => !resource || resource.state === "unresolved" | ||
| ? undefined | ||
| : unwrap(resource.latest); | ||
| [resource] = createResource(() => subFetch(fn, catchError(() => untrack(prev), () => undefined)), v => v, { | ||
| ...options, | ||
| storage: (init) => createDeepSignal(init, options.reconcile) | ||
| }); | ||
| const resultAccessor = (() => resource()); | ||
| Object.defineProperty(resultAccessor, "latest", { | ||
| get() { | ||
| return resource.latest; | ||
| } | ||
| }); | ||
| return resultAccessor; | ||
| } | ||
| function createDeepSignal(value, options) { | ||
| const [store, setStore] = createStore({ | ||
| value: structuredClone(value) | ||
| }); | ||
| return [ | ||
| () => store.value, | ||
| (v) => { | ||
| typeof v === "function" && (v = v()); | ||
| setStore("value", reconcile(structuredClone(v), options)); | ||
| return store.value; | ||
| } | ||
| ]; | ||
| } | ||
| // mock promise while hydrating to prevent fetching | ||
| class MockPromise { | ||
| static all() { | ||
| return new MockPromise(); | ||
| } | ||
| static allSettled() { | ||
| return new MockPromise(); | ||
| } | ||
| static any() { | ||
| return new MockPromise(); | ||
| } | ||
| static race() { | ||
| return new MockPromise(); | ||
| } | ||
| static reject() { | ||
| return new MockPromise(); | ||
| } | ||
| static resolve() { | ||
| return new MockPromise(); | ||
| } | ||
| catch() { | ||
| return new MockPromise(); | ||
| } | ||
| then() { | ||
| return new MockPromise(); | ||
| } | ||
| finally() { | ||
| return new MockPromise(); | ||
| } | ||
| } | ||
| function subFetch(fn, prev) { | ||
| if (isServer || !sharedConfig.context) | ||
| return fn(prev); | ||
| const ogFetch = fetch; | ||
| const ogPromise = Promise; | ||
| try { | ||
| window.fetch = () => new MockPromise(); | ||
| Promise = MockPromise; | ||
| return fn(prev); | ||
| } | ||
| finally { | ||
| window.fetch = ogFetch; | ||
| Promise = ogPromise; | ||
| } | ||
| } |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
1045
0.58%3
-66.67%188457
-2.5%2
100%18
5.88%40
-4.76%4156
-3.3%