From 65c4b7a602eb3df011d122b4b831cb5ed5fd0839 Mon Sep 17 00:00:00 2001 From: atellmer Date: Fri, 12 Apr 2024 13:33:12 +0500 Subject: [PATCH 1/9] fix resolver --- .../src/create-routes/create-routes.ts | 65 +++++++++++++++++-- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/packages/web-router/src/create-routes/create-routes.ts b/packages/web-router/src/create-routes/create-routes.ts index 4bbf9af1..7125b354 100644 --- a/packages/web-router/src/create-routes/create-routes.ts +++ b/packages/web-router/src/create-routes/create-routes.ts @@ -1,6 +1,15 @@ import { type DarkElement, type ComponentFactory, type SlotProps, keyBy, detectIsString } from '@dark-engine/core'; -import { pipe, splitBySlash, normalizePath, trimSlashes, detectIsParam, getParamName, sort } from '../utils'; +import { + pipe, + splitBySlash, + normalizePath, + trimSlashes, + detectIsParam, + getParamName, + sort, + reduceSlashes, +} from '../utils'; import { SLASH_MARK, WILDCARD_MARK, ROOT_MARK } from '../constants'; import { CurrentPathContext } from '../context'; import type { Routes, RouteDescriptor, PathMatchStrategy, Params } from './types'; @@ -67,6 +76,8 @@ class Route { } } +const trim = (x: string) => trimSlashes(reduceSlashes(x.replaceAll(ROOT_MARK, SLASH_MARK))); + function createRoutes(routes: Routes, prefix = SLASH_MARK, parent: Route = null): Array { const $routes: Array = []; @@ -77,11 +88,11 @@ function createRoutes(routes: Routes, prefix = SLASH_MARK, parent: Route = null) } if (!parent) { - const map = keyBy($routes, x => x.path, true) as Record; + const map = keyBy($routes, x => trim(x.path), true) as Record; for (const $route of $routes) { if ($route.redirectTo) { - $route.redirectTo.route = map[$route.redirectTo.path] || null; + $route.redirectTo.route = map[trim($route.redirectTo.path)] || null; } } } @@ -263,10 +274,52 @@ function getParamsMap(path: string, route: Route): Params { return map; } -function resolveRoute(path: string, routes: Array) { - const activeRoute = resolve(path, routes); +function resolve2(url: string, routes: Array): Route | null { + const a = splitBySlash(url); + + let [route] = routes.filter(x => { + const path = reduceSlashes(x.path.replaceAll(ROOT_MARK, SLASH_MARK)); + const b = splitBySlash(path); + const max = Math.max(a.length, b.length); + + for (let i = 0; i < max; i++) { + const part1 = a[i]; + const part2 = b[i]; + + if (detectIsParam(part2)) continue; + if (part1 !== part2) return false; + } + + return true; + }); + + if (route && route.redirectTo) { + route = redirect()(route); + } + + if (!route) { + let wild = wildcard(url, routes)(route); + + if (wild && wild.redirectTo) { + wild = redirect()(wild); + } + + route = wild; + } + + route = root()(route); + + if (route && route.redirectTo) { + route = redirect()(route); + } + + return route; +} + +function resolveRoute(url: string, routes: Array) { + const activeRoute = resolve2(url, routes); const slot = activeRoute ? activeRoute.render() : null; - const params = activeRoute ? getParamsMap(path, activeRoute) : null; + const params = activeRoute ? getParamsMap(url, activeRoute) : null; const value = { activeRoute, slot, params }; return value; From 4bea41bcc1b307c6068f3ae67dc8e52e920bfa0d Mon Sep 17 00:00:00 2001 From: atellmer Date: Fri, 12 Apr 2024 16:03:02 +0500 Subject: [PATCH 2/9] fix of resolving paths in the router --- packages/core/src/fiber/fiber.ts | 11 +- packages/core/src/workloop/workloop.ts | 1 - .../src/create-routes/create-routes.spec.tsx | 30 +-- .../src/create-routes/create-routes.ts | 208 ++++++------------ packages/web-router/src/index.ts | 2 +- .../src/router-link/router-link.spec.tsx | 4 + .../web-router/src/router/router.spec.tsx | 12 +- packages/web-router/src/router/router.tsx | 25 +-- .../web-router/src/use-match/use-match.ts | 10 +- 9 files changed, 114 insertions(+), 189 deletions(-) diff --git a/packages/core/src/fiber/fiber.ts b/packages/core/src/fiber/fiber.ts index 67944197..cccc4c95 100644 --- a/packages/core/src/fiber/fiber.ts +++ b/packages/core/src/fiber/fiber.ts @@ -2,7 +2,7 @@ import { detectIsTagVirtualNode, detectIsPlainVirtualNode, detectAreSameComponen import { type Instance, type Callback, type TimerId } from '../shared'; import { type Context, type ContextProviderValue } from '../context'; import { detectIsComponent } from '../component'; -import { detectIsFunction } from '../utils'; +import { detectIsFunction, error } from '../utils'; import { type Atom } from '../atom'; import { $$scope } from '../scope'; @@ -72,11 +72,14 @@ class Fiber { } } - setError(error: Error) { + setError(err: Error) { if (detectIsFunction(this.catch)) { - this.catch(error); + this.catch(err); + error(err); } else if (this.parent) { - this.parent.setError(error); + this.parent.setError(err); + } else { + throw err; } } diff --git a/packages/core/src/workloop/workloop.ts b/packages/core/src/workloop/workloop.ts index bc24204a..de12f1ac 100644 --- a/packages/core/src/workloop/workloop.ts +++ b/packages/core/src/workloop/workloop.ts @@ -381,7 +381,6 @@ function mount(fiber: Fiber, prev: Fiber, $scope: Scope) { if (err instanceof Promise) throw err; component.children = []; fiber.setError(err); - error(err); } } else if (detectIsVirtualNodeFactory(inst)) { inst = inst(); diff --git a/packages/web-router/src/create-routes/create-routes.spec.tsx b/packages/web-router/src/create-routes/create-routes.spec.tsx index 79e47423..3f3d8949 100644 --- a/packages/web-router/src/create-routes/create-routes.spec.tsx +++ b/packages/web-router/src/create-routes/create-routes.spec.tsx @@ -3,7 +3,6 @@ import { resetBrowserHistory } from '@test-utils'; import { Routes } from './types'; import { createRoutes, resolve } from './create-routes'; -import { ROOT_MARK } from '../constants'; afterEach(() => { resetBrowserHistory(); @@ -52,13 +51,14 @@ describe('@web-router/create-routes', () => { ]; const $routes = createRoutes(routes); - expect(resolve('/', $routes)).toBe(null); - expect(resolve('', $routes)).toBe(null); - expect(resolve('/xxx', $routes)).toBe(null); + expect(() => resolve('/', $routes)).toThrowError(); + expect(() => resolve('', $routes)).toThrowError(); + expect(() => resolve('/xxx', $routes)).toThrowError(); expect(resolve('/second', $routes).path).toBe('second'); - expect(resolve('/second/1', $routes)).toBe(null); - expect(resolve('/first/1/xxx', $routes)).toBe(null); - expect(resolve('/some/broken/url', $routes)).toBe(null); + expect(() => resolve('/second/1', $routes)).toThrowError(); + expect(() => resolve('/first/1/xxx', $routes)).toThrowError(); + expect(() => resolve('/some/broken/url', $routes)).toThrowError(); + expect(resolve('/first', $routes).path).toBe('first'); }); test('can match nested routes correctly', () => { @@ -92,7 +92,7 @@ describe('@web-router/create-routes', () => { expect(resolve('/second', $routes).path).toBe('second'); expect(resolve('/second/a', $routes).path).toBe('second/a'); expect(resolve('/second/b', $routes).path).toBe('second/b'); - expect(resolve('/second/b/some/broken/route', $routes)).toBe(null); + expect(() => resolve('/second/b/some/broken/route', $routes)).toThrowError(); expect(resolve('/third', $routes).path).toBe('third'); }); @@ -280,9 +280,9 @@ describe('@web-router/create-routes', () => { ]; const $routes = createRoutes(routes); - expect(resolve('/', $routes).path).toBe(`${ROOT_MARK}`); - expect(resolve('', $routes).path).toBe(`${ROOT_MARK}`); - expect(resolve('/broken', $routes).path).toBe(`${ROOT_MARK}`); + expect(resolve('/', $routes).path).toBe(''); + expect(resolve('', $routes).path).toBe(''); + expect(resolve('/broken', $routes).path).toBe(''); expect(resolve('/second', $routes).path).toBe(`second`); expect(resolve('/third', $routes).path).toBe(`third`); }); @@ -724,13 +724,13 @@ describe('@web-router/create-routes', () => { ]; const $routes = createRoutes(routes); - expect(resolve('/', $routes).path).toBe(`first/${ROOT_MARK}`); - expect(resolve('/first', $routes).path).toBe(`first/${ROOT_MARK}`); + expect(resolve('/', $routes).path).toBe(`first`); + expect(resolve('/first', $routes).path).toBe(`first`); expect(resolve('/first/nested', $routes).path).toBe('first/nested'); expect(resolve('/first/666', $routes).path).toBe(`first/:id`); - expect(resolve('/first/666/broken', $routes).path).toBe(`first/${ROOT_MARK}`); + expect(resolve('/first/666/broken', $routes).path).toBe(`first`); expect(resolve('/second', $routes).path).toBe('second'); expect(resolve('/third/', $routes).path).toBe('third'); - expect(resolve('/broken/url', $routes).path).toBe(`first/${ROOT_MARK}`); + expect(resolve('/broken/url', $routes).path).toBe(`first`); }); }); diff --git a/packages/web-router/src/create-routes/create-routes.ts b/packages/web-router/src/create-routes/create-routes.ts index 7125b354..27b65ddd 100644 --- a/packages/web-router/src/create-routes/create-routes.ts +++ b/packages/web-router/src/create-routes/create-routes.ts @@ -1,15 +1,6 @@ import { type DarkElement, type ComponentFactory, type SlotProps, keyBy, detectIsString } from '@dark-engine/core'; -import { - pipe, - splitBySlash, - normalizePath, - trimSlashes, - detectIsParam, - getParamName, - sort, - reduceSlashes, -} from '../utils'; +import { pipe, splitBySlash, normalizePath, trimSlashes, detectIsParam, getParamName, sort, join } from '../utils'; import { SLASH_MARK, WILDCARD_MARK, ROOT_MARK } from '../constants'; import { CurrentPathContext } from '../context'; import type { Routes, RouteDescriptor, PathMatchStrategy, Params } from './types'; @@ -42,7 +33,7 @@ class Route { this.parent = parent; this.children = createRoutes(children, prefixedPath, this); this.level = parent ? parent.level + 1 : 0; - this.marker = rootPath; + this.marker = rootPath === '' ? ROOT_MARK : rootPath; this.redirectTo = detectIsString(redirectTo) ? { path: createPrefixedPath(pathMatch, prefix, createRootPath(redirectTo)), @@ -57,7 +48,7 @@ class Route { } getPath() { - return this.path.replaceAll(new RegExp(`${ROOT_MARK}${SLASH_MARK}?`, 'g'), ''); + return this.path; } render(): DarkElement { @@ -76,8 +67,6 @@ class Route { } } -const trim = (x: string) => trimSlashes(reduceSlashes(x.replaceAll(ROOT_MARK, SLASH_MARK))); - function createRoutes(routes: Routes, prefix = SLASH_MARK, parent: Route = null): Array { const $routes: Array = []; @@ -88,11 +77,11 @@ function createRoutes(routes: Routes, prefix = SLASH_MARK, parent: Route = null) } if (!parent) { - const map = keyBy($routes, x => trim(x.path), true) as Record; + const map = keyBy($routes, x => x.path, true) as Record; for (const $route of $routes) { if ($route.redirectTo) { - $route.redirectTo.route = map[trim($route.redirectTo.path)] || null; + $route.redirectTo.route = map[$route.redirectTo.path] || null; } } } @@ -100,224 +89,149 @@ function createRoutes(routes: Routes, prefix = SLASH_MARK, parent: Route = null) return $routes; } -function resolve(path: string, routes: Array): Route { - const route = pipe( - match(path, routes), - redirect(), - wildcard(path, routes), - redirect(), - root(), - redirect(), - canRender(), - )(); +function resolve(url: string, routes: Array): Route | null { + const $match = match(url, routes); + const $wildcard = wildcard(url, routes); + const route = pipe($match, redirect, $wildcard, redirect, root, redirect, canRender)(); return route; } -function match(path: string, routes: Array) { - return (): Route => { - const [route] = pipe>( - (routes: Array) => routes.filter(x => detectIsMatchByFirstStrategy(path, x.path)), - (routes: Array) => routes.filter(x => detectIsMatchBySecondStrategy(path, x.path)), - )(routes); +function match(url: string, routes: Array) { + return () => { + const [route] = routes.filter(x => detectIsMatch(url, x.path)); return pick(route); }; } -function redirect() { - return (route: Route): Route => { - if (route?.redirectTo) return redirect()(route.redirectTo.route); - if (route?.parent?.redirectTo) return redirect()(route.parent.redirectTo.route); +function redirect(route: Route) { + if (route?.redirectTo) return redirect(route.redirectTo.route); + if (route?.parent?.redirectTo) return redirect(route.parent.redirectTo.route); - return pick(route); - }; + return pick(route); } function wildcard(path: string, routes: Array) { - return ($route: Route): Route => { - if ($route) return $route; - const [route] = pipe>( + return (route: Route) => { + if (route) return route; + const [$route] = pipe>( (routes: Array) => routes.filter(x => x.marker === WILDCARD_MARK), (routes: Array) => routes.filter(x => detectIsMatchAsWildcard(path, x.path)) || null, (routes: Array) => sort('desc', routes, x => x.level), )(routes); - return pick(route); + return pick($route); }; } -function root() { - return (route: Route): Route => { - const root = route?.children.find(x => x.marker === ROOT_MARK) || route; +function root(route: Route) { + const $route = route?.children.find(x => x.marker === ROOT_MARK); - return pick(root); - }; -} - -function canRender() { - return (route: Route): Route => { - if (route?.component) return route; + if ($route?.children.length > 0) return root($route); - if (process.env.NODE_ENV !== 'test') { - throw new Error('[web-router]: the route was not found or it has no component!'); - } + return pick($route || route); +} - return null; - }; +function canRender(route: Route) { + if (route?.component) return route; + throw new Error('[web-router]: the route was not found or it has no component!'); } const pick = (route: Route): Route | null => route || null; -function detectIsMatchByFirstStrategy(urlPath: string, routePath: string): boolean { +function detectIsMatch(url: string, path: string) { const matcher = createMatcher({ - space: (_, b) => b, - skip: ({ isRoot, isParam }) => isRoot || isParam, - }); - - return matcher(urlPath, routePath); -} - -function detectIsMatchBySecondStrategy(urlPath: string, routePath: string): boolean { - const matcher = createMatcher({ - space: a => a, + space: (a, b) => Math.max(a.length, b.length), skip: ({ isParam }) => isParam, }); - return matcher(urlPath, routePath); + return matcher(url, path); } -function detectIsMatchAsWildcard(urlPath: string, routePath: string): boolean { +function detectIsMatchAsWildcard(url: string, path: string) { const matcher = createMatcher({ - space: (_, b) => b, - skip: ({ isRoot, isParam, isWildcard }) => isRoot || isParam || isWildcard, + space: (_, b) => b.length, + skip: ({ isParam, isWildcard }) => isParam || isWildcard, }); - return matcher(urlPath, routePath); + return matcher(url, path); } type CreateMatcherOptions = { - space: (a: Array, b: Array) => Array; + space: (a: Array, b: Array) => number; skip: (options: SkipOptions) => boolean; }; type SkipOptions = { - isRoot: boolean; isWildcard: boolean; isParam: boolean; }; function createMatcher(options: CreateMatcherOptions) { const { space, skip } = options; - return (urlPath: string, routePath: string) => { - const sUrlPath = splitBySlash(urlPath); - const sRoutePath = splitBySlash(routePath); - for (let i = 0; i < space(sUrlPath, sRoutePath).length; i++) { - const segment = sRoutePath[i]; - const isRoot = segment === ROOT_MARK; + return (url: string, path: string) => { + const [a, b] = split(url, path); + + for (let i = 0; i < space(a, b); i++) { + const segment = b[i]; const isWildcard = segment === WILDCARD_MARK; const isParam = detectIsParam(segment); - if (segment !== sUrlPath[i] && !skip({ isRoot, isWildcard, isParam })) return false; + if (segment !== a[i] && !skip({ isWildcard, isParam })) return false; } return true; }; } -function mergePathes(urlPath: string, routePath: string) { - const sUrl = splitBySlash(urlPath); - const sRoute = splitBySlash(routePath); +function mergePaths(url: string, path: string) { + const [a, b] = split(url, path); const parts: Array = []; - for (let i = 0; i < sRoute.length; i++) { - const isParam = detectIsParam(sRoute[i]); + for (let i = 0; i < b.length; i++) { + const isParam = detectIsParam(b[i]); if (isParam) { - const param = sUrl[i] || 'null'; + const param = a[i] || 'null'; parts.push(param); } else { - parts.push(sRoute[i]); + parts.push(b[i]); } } - let path = normalizePath(parts.join(SLASH_MARK)); + let $path = normalizePath(parts.join(SLASH_MARK)); - if (path[0] !== SLASH_MARK) { - path = SLASH_MARK + path; + if ($path[0] !== SLASH_MARK) { + $path = join(SLASH_MARK, $path); } - return path; + return $path; } -const createRootPath = (path: string) => (path === SLASH_MARK || path === '' ? ROOT_MARK : path); - function createPrefixedPath(pathMatch: PathMatchStrategy, prefix: string, path: string) { const $prefix = pathMatch === 'prefix' ? normalizePath(prefix) + SLASH_MARK : ''; return trimSlashes(normalizePath($prefix ? `${$prefix}${path}` : path)); } -function getParamsMap(path: string, route: Route): Params { - const sPathname = splitBySlash(path); - const sPath = splitBySlash(route.path); +function getParamsMap(url: string, route: Route): Params { + const [a, b] = split(url, route.path); const map = new Map(); - for (let i = 0; i < sPath.length; i++) { - if (detectIsParam(sPath[i])) { - map.set(getParamName(sPath[i]), sPathname[i]); + for (let i = 0; i < b.length; i++) { + if (detectIsParam(b[i])) { + map.set(getParamName(b[i]), a[i]); } } return map; } -function resolve2(url: string, routes: Array): Route | null { - const a = splitBySlash(url); - - let [route] = routes.filter(x => { - const path = reduceSlashes(x.path.replaceAll(ROOT_MARK, SLASH_MARK)); - const b = splitBySlash(path); - const max = Math.max(a.length, b.length); - - for (let i = 0; i < max; i++) { - const part1 = a[i]; - const part2 = b[i]; - - if (detectIsParam(part2)) continue; - if (part1 !== part2) return false; - } - - return true; - }); - - if (route && route.redirectTo) { - route = redirect()(route); - } - - if (!route) { - let wild = wildcard(url, routes)(route); - - if (wild && wild.redirectTo) { - wild = redirect()(wild); - } - - route = wild; - } - - route = root()(route); - - if (route && route.redirectTo) { - route = redirect()(route); - } - - return route; -} - function resolveRoute(url: string, routes: Array) { - const activeRoute = resolve2(url, routes); + const activeRoute = resolve(url, routes); const slot = activeRoute ? activeRoute.render() : null; const params = activeRoute ? getParamsMap(url, activeRoute) : null; const value = { activeRoute, slot, params }; @@ -325,4 +239,8 @@ function resolveRoute(url: string, routes: Array) { return value; } -export { type Route, createRoutes, resolve, resolveRoute, mergePathes }; +const createRootPath = (path: string) => (path === SLASH_MARK ? '' : path); + +const split = (url: string, path: string) => [splitBySlash(url), splitBySlash(path)]; + +export { type Route, createRoutes, resolve, resolveRoute, mergePaths }; diff --git a/packages/web-router/src/index.ts b/packages/web-router/src/index.ts index 306140d0..5c7ea570 100644 --- a/packages/web-router/src/index.ts +++ b/packages/web-router/src/index.ts @@ -1,5 +1,5 @@ +export { type Routes, type PathMatchStrategy } from './create-routes'; export { type RouterRef, Router } from './router'; -export { type Routes } from './create-routes'; export { useLocation } from './use-location'; export { RouterLink } from './router-link'; export { useHistory } from './use-history'; diff --git a/packages/web-router/src/router-link/router-link.spec.tsx b/packages/web-router/src/router-link/router-link.spec.tsx index 0f92c0db..680a2650 100644 --- a/packages/web-router/src/router-link/router-link.spec.tsx +++ b/packages/web-router/src/router-link/router-link.spec.tsx @@ -42,6 +42,10 @@ describe('@web-router/router-link', () => { path: 'third', component: component(() =>
third
), }, + { + path: '**', + component: component(() => null), + }, ]; const App = component(() => { diff --git a/packages/web-router/src/router/router.spec.tsx b/packages/web-router/src/router/router.spec.tsx index 702ffac7..2c995b76 100644 --- a/packages/web-router/src/router/router.spec.tsx +++ b/packages/web-router/src/router/router.spec.tsx @@ -8,11 +8,11 @@ type AppProps = { url: string; }; -let { host, render: $render } = createBrowserEnv(); +let { host, render: $render, unmount } = createBrowserEnv(); beforeEach(() => { jest.useFakeTimers(); - ({ host, render: $render } = createBrowserEnv()); + ({ host, render: $render, unmount } = createBrowserEnv()); }); afterEach(() => { @@ -73,6 +73,10 @@ describe('@web-router/router', () => { path: 'third', component: component(() =>
third
), }, + { + path: '**', + component: component(() => null), + }, ]; const App = component(({ url }) => { @@ -154,9 +158,9 @@ describe('@web-router/router', () => { render(); expect(host.innerHTML).toBe(`
b
`); - render(); - expect(host.innerHTML).toBe(replacer); + expect(() => render()).toThrowError(); + unmount(); render(); expect(host.innerHTML).toBe(`
third
`); }); diff --git a/packages/web-router/src/router/router.tsx b/packages/web-router/src/router/router.tsx index 5847188b..bccaafda 100644 --- a/packages/web-router/src/router/router.tsx +++ b/packages/web-router/src/router/router.tsx @@ -15,7 +15,7 @@ import { SLASH_MARK, PROTOCOL_MARK, WILDCARD_MARK } from '../constants'; import { normalizePath, join } from '../utils'; import { createRouterHistory } from '../history'; import { type RouterLocation, createRouterLocation } from '../location'; -import { type Routes, createRoutes, resolveRoute, mergePathes } from '../create-routes'; +import { type Routes, createRoutes, resolveRoute, mergePaths } from '../create-routes'; import { type RouterHistoryContextValue, type ActiveRouteContextValue, @@ -38,20 +38,17 @@ export type RouterRef = { const Router = forwardRef( component( - ({ url, baseURL = SLASH_MARK, routes: sourceRoutes, slot }, ref) => { + ({ url: fullURL, baseURL = SLASH_MARK, routes: sourceRoutes, slot }, ref) => { if (useActiveRouteContext()) throw new Error(`[web-router]: the parent active route's context detected!`); - const sourceURL = url || window.location.href; + const sourceURL = fullURL || window.location.href; const [location, setLocation] = useState(() => createRouterLocation(sourceURL)); const history = useMemo(() => createRouterHistory(sourceURL), []); const routes = useMemo(() => createRoutes(sourceRoutes, normalizePath(baseURL)), []); - const { protocol, host, pathname: path, search, hash } = location; - const { activeRoute, slot: $slot, params } = resolveRoute(path, routes); + const { protocol, host, pathname: url, search, hash } = location; + const { activeRoute, slot: $slot, params } = resolveRoute(url, routes); const scope = useMemo(() => ({ location }), []); const historyContext = useMemo(() => ({ history }), []); - const routerContext = useMemo( - () => ({ location, activeRoute, params }), - [path, search, hash], - ); + const routerContext = useMemo(() => ({ location, activeRoute, params }), [location]); scope.location = location; @@ -76,13 +73,13 @@ const Router = forwardRef( useEffect(() => { if (!activeRoute || activeRoute.marker === WILDCARD_MARK) return; - const url = join(path, search, hash); - const $url = join(mergePathes(path, activeRoute.getPath()), search, hash); + const url1 = join(url, search, hash); + const url2 = join(mergePaths(url, activeRoute.getPath()), search, hash); - if (url !== $url) { - history.replace($url); + if (url1 !== url2) { + history.replace(url2); } - }, [path, search, hash]); + }, [url, search, hash]); useImperativeHandle(ref as MutableRef, () => ({ navigateTo: (url: string) => nextTick(() => history.push(url)), diff --git a/packages/web-router/src/use-match/use-match.ts b/packages/web-router/src/use-match/use-match.ts index a8da6248..abf2a468 100644 --- a/packages/web-router/src/use-match/use-match.ts +++ b/packages/web-router/src/use-match/use-match.ts @@ -1,7 +1,7 @@ import { useMemo } from '@dark-engine/core'; import { useActiveRouteContext, useCurrentPathContext, checkContextValue } from '../context'; -import { mergePathes } from '../create-routes'; +import { mergePaths } from '../create-routes'; export type Match = { path: string; @@ -11,12 +11,12 @@ export type Match = { function useMatch() { const activeRoute = useActiveRouteContext(); checkContextValue(activeRoute); - const routePath = useCurrentPathContext(); + const path = useCurrentPathContext(); const { - location: { pathname: urlPath }, + location: { pathname: url }, } = activeRoute; - const url = useMemo(() => (routePath ? mergePathes(urlPath, routePath) : ''), [urlPath, routePath]); - const value: Match = { path: routePath, url }; + const $url = useMemo(() => (path ? mergePaths(url, path) : ''), [url, path]); + const value: Match = { path, url: $url }; return value; } From 670065957c3161e659fb17f0023b5ceb6008ca6b Mon Sep 17 00:00:00 2001 From: atellmer Date: Fri, 12 Apr 2024 16:25:32 +0500 Subject: [PATCH 3/9] fix of router render --- packages/web-router/src/router/router.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/web-router/src/router/router.tsx b/packages/web-router/src/router/router.tsx index bccaafda..b0f6ce4b 100644 --- a/packages/web-router/src/router/router.tsx +++ b/packages/web-router/src/router/router.tsx @@ -62,7 +62,9 @@ const Router = forwardRef( const unsubscribe = history.subscribe(url => { const href = join(protocol, PROTOCOL_MARK, host, url); - setLocation(createRouterLocation(href)); + if (href !== scope.location.url) { + setLocation(createRouterLocation(href)); + } }); return () => { @@ -79,7 +81,7 @@ const Router = forwardRef( if (url1 !== url2) { history.replace(url2); } - }, [url, search, hash]); + }, [location]); useImperativeHandle(ref as MutableRef, () => ({ navigateTo: (url: string) => nextTick(() => history.push(url)), From a70eec9044f4595dca657a159151e336e6c5f3d0 Mon Sep 17 00:00:00 2001 From: atellmer Date: Fri, 12 Apr 2024 20:41:04 +0500 Subject: [PATCH 4/9] replaced async effect for layout effect --- .../src/create-routes/create-routes.spec.tsx | 5 ----- .../src/router-link/router-link.spec.tsx | 4 ---- .../web-router/src/router/router.spec.tsx | 16 +++++----------- packages/web-router/src/router/router.tsx | 3 +-- .../src/use-history/use-history.spec.tsx | 6 ------ .../src/use-location/use-location.spec.tsx | 3 --- .../src/use-match/use-match.spec.tsx | 19 ------------------- .../src/use-params/use-params.spec.tsx | 5 ----- 8 files changed, 6 insertions(+), 55 deletions(-) diff --git a/packages/web-router/src/create-routes/create-routes.spec.tsx b/packages/web-router/src/create-routes/create-routes.spec.tsx index 3f3d8949..acc684e4 100644 --- a/packages/web-router/src/create-routes/create-routes.spec.tsx +++ b/packages/web-router/src/create-routes/create-routes.spec.tsx @@ -1,13 +1,8 @@ import { component } from '@dark-engine/core'; -import { resetBrowserHistory } from '@test-utils'; import { Routes } from './types'; import { createRoutes, resolve } from './create-routes'; -afterEach(() => { - resetBrowserHistory(); -}); - describe('@web-router/create-routes', () => { test('can match simple routes correctly', () => { const routes: Routes = [ diff --git a/packages/web-router/src/router-link/router-link.spec.tsx b/packages/web-router/src/router-link/router-link.spec.tsx index 680a2650..1a6adb13 100644 --- a/packages/web-router/src/router-link/router-link.spec.tsx +++ b/packages/web-router/src/router-link/router-link.spec.tsx @@ -10,7 +10,6 @@ import { RouterLink } from './router-link'; let { host, render } = createBrowserEnv(); beforeEach(() => { - jest.useFakeTimers(); ({ host, render } = createBrowserEnv()); }); @@ -68,7 +67,6 @@ describe('@web-router/router-link', () => { }); render(); - jest.runAllTimers(); expect(host.innerHTML).toBe(content('', replacer)); const link1 = host.querySelector('a[href="/first"]'); @@ -111,7 +109,6 @@ describe('@web-router/router-link', () => { }); render(); - jest.runAllTimers(); expect(host.innerHTML).toBe(`first`); }); @@ -144,7 +141,6 @@ describe('@web-router/router-link', () => { }); render(); - jest.runAllTimers(); expect(host.innerHTML).toBe(`first`); click(host.querySelector('a')); diff --git a/packages/web-router/src/router/router.spec.tsx b/packages/web-router/src/router/router.spec.tsx index 2c995b76..e8aceb0c 100644 --- a/packages/web-router/src/router/router.spec.tsx +++ b/packages/web-router/src/router/router.spec.tsx @@ -1,6 +1,6 @@ import { type DarkElement, type MutableRef, component, useRef } from '@dark-engine/core'; -import { createBrowserEnv, replacer, resetBrowserHistory } from '@test-utils'; +import { createBrowserEnv, replacer, resetBrowserHistory, sleep } from '@test-utils'; import { type Routes } from '../create-routes'; import { type RouterRef, Router } from './router'; @@ -8,22 +8,16 @@ type AppProps = { url: string; }; -let { host, render: $render, unmount } = createBrowserEnv(); +let { host, render, unmount } = createBrowserEnv(); beforeEach(() => { - jest.useFakeTimers(); - ({ host, render: $render, unmount } = createBrowserEnv()); + ({ host, render, unmount } = createBrowserEnv()); }); afterEach(() => { resetBrowserHistory(); }); -const render = (element: DarkElement) => { - $render(element); - jest.runAllTimers(); -}; - describe('@web-router/router', () => { test('can render simple routes correctly', () => { const routes: Routes = [ @@ -1006,7 +1000,7 @@ describe('@web-router/router', () => { expect(host.innerHTML).toBe(`
root
`); }); - test('a history updates correctly with wildcard routing', () => { + test('a history updates correctly with wildcard routing', async () => { let routerRef: MutableRef = null; const routes: Routes = [ { @@ -1044,7 +1038,7 @@ describe('@web-router/router', () => { render(); routerRef.current.navigateTo('/broken/'); - jest.runAllTimers(); + await sleep(0); expect(host.innerHTML).toBe(`
404
`); expect(location.href).toBe('http://localhost/broken/'); }); diff --git a/packages/web-router/src/router/router.tsx b/packages/web-router/src/router/router.tsx index b0f6ce4b..e050e072 100644 --- a/packages/web-router/src/router/router.tsx +++ b/packages/web-router/src/router/router.tsx @@ -3,7 +3,6 @@ import { type MutableRef, component, useMemo, - useEffect, useLayoutEffect, useState, forwardRef, @@ -73,7 +72,7 @@ const Router = forwardRef( }; }, []); - useEffect(() => { + useLayoutEffect(() => { if (!activeRoute || activeRoute.marker === WILDCARD_MARK) return; const url1 = join(url, search, hash); const url2 = join(mergePaths(url, activeRoute.getPath()), search, hash); diff --git a/packages/web-router/src/use-history/use-history.spec.tsx b/packages/web-router/src/use-history/use-history.spec.tsx index 2760fa72..9eeb0f74 100644 --- a/packages/web-router/src/use-history/use-history.spec.tsx +++ b/packages/web-router/src/use-history/use-history.spec.tsx @@ -9,7 +9,6 @@ import { RouterHistory } from '../history'; let { host, render } = createBrowserEnv(); beforeEach(() => { - jest.useFakeTimers(); ({ host, render } = createBrowserEnv()); }); @@ -44,28 +43,23 @@ describe('@web-router/use-history', () => { }); render(); - jest.runAllTimers(); expect(history).toBeInstanceOf(RouterHistory); expect(host.innerHTML).toBe(`
root
`); expect(location.href).toBe('http://localhost/'); history.push('/second/'); - jest.runAllTimers(); expect(host.innerHTML).toBe(`
second
`); expect(location.href).toBe('http://localhost/second'); history.push('/third/'); - jest.runAllTimers(); expect(host.innerHTML).toBe(`
third
`); expect(location.href).toBe('http://localhost/third'); history.push('/second'); - jest.runAllTimers(); expect(host.innerHTML).toBe(`
second
`); expect(location.href).toBe('http://localhost/second'); history.push('/second'); - jest.runAllTimers(); expect(host.innerHTML).toBe(`
second
`); expect(location.href).toBe('http://localhost/second'); }); diff --git a/packages/web-router/src/use-location/use-location.spec.tsx b/packages/web-router/src/use-location/use-location.spec.tsx index c5680837..5a21e1a8 100644 --- a/packages/web-router/src/use-location/use-location.spec.tsx +++ b/packages/web-router/src/use-location/use-location.spec.tsx @@ -11,7 +11,6 @@ import { useLocation } from './use-location'; let { host, render } = createBrowserEnv(); beforeEach(() => { - jest.useFakeTimers(); ({ host, render } = createBrowserEnv()); }); @@ -49,14 +48,12 @@ describe('@web-router/use-location', () => { }); render(); - jest.runAllTimers(); expect(location).toBeInstanceOf(RouterLocation); expect(location.pathname).toBe('/'); expect(location.key).toBeTruthy(); expect(host.innerHTML).toBe(`
root
`); history.push('/second'); - jest.runAllTimers(); expect(host.innerHTML).toBe(`
second
`); expect(location).toBeInstanceOf(RouterLocation); expect(location.pathname).toBe('/second'); diff --git a/packages/web-router/src/use-match/use-match.spec.tsx b/packages/web-router/src/use-match/use-match.spec.tsx index 9eb112bd..be83fd1d 100644 --- a/packages/web-router/src/use-match/use-match.spec.tsx +++ b/packages/web-router/src/use-match/use-match.spec.tsx @@ -10,7 +10,6 @@ import { useMatch, type Match } from './use-match'; let { host, render } = createBrowserEnv(); beforeEach(() => { - jest.useFakeTimers(); ({ host, render } = createBrowserEnv()); }); @@ -47,14 +46,12 @@ describe('@web-router/use-match', () => { }); render(); - jest.runAllTimers(); expect(match).toBeTruthy(); expect(match.path).toBe(''); expect(match.url).toBe(''); expect(host.innerHTML).toMatchInlineSnapshot(`"
root
"`); history.push('/second/10'); - jest.runAllTimers(); expect(host.innerHTML).toMatchInlineSnapshot(`"
second
"`); expect(match).toBeTruthy(); expect(match.path).toBe('second/:id'); @@ -97,21 +94,18 @@ describe('@web-router/use-match', () => { }); render(); - jest.runAllTimers(); expect(match).toBeTruthy(); expect(match.path).toBe(''); expect(match.url).toBe(''); expect(host.innerHTML).toMatchInlineSnapshot(`"
root
"`); history.push('/first'); - jest.runAllTimers(); expect(host.innerHTML).toMatchInlineSnapshot(`"
first
"`); expect(match).toBeTruthy(); expect(match.path).toBe('first'); expect(match.url).toBe('/first'); history.push('/second'); - jest.runAllTimers(); expect(host.innerHTML).toMatchInlineSnapshot(`"
second
"`); expect(match).toBeTruthy(); expect(match.path).toBe('second'); @@ -154,21 +148,18 @@ describe('@web-router/use-match', () => { }); render(); - jest.runAllTimers(); expect(match).toBeTruthy(); expect(match.path).toBe(''); expect(match.url).toBe(''); expect(host.innerHTML).toMatchInlineSnapshot(`"
root
"`); history.push('/first/'); - jest.runAllTimers(); expect(host.innerHTML).toMatchInlineSnapshot(`"
first
"`); expect(match).toBeTruthy(); expect(match.path).toBe('first'); expect(match.url).toBe('/first'); history.push('/second/'); - jest.runAllTimers(); expect(host.innerHTML).toMatchInlineSnapshot(`"
second
"`); expect(match).toBeTruthy(); expect(match.path).toBe('second'); @@ -211,21 +202,18 @@ describe('@web-router/use-match', () => { }); render(); - jest.runAllTimers(); expect(match).toBeTruthy(); expect(match.path).toBe(''); expect(match.url).toBe(''); expect(host.innerHTML).toMatchInlineSnapshot(`"
root
"`); history.push('/first'); - jest.runAllTimers(); expect(host.innerHTML).toMatchInlineSnapshot(`"
first
"`); expect(match).toBeTruthy(); expect(match.path).toBe('first'); expect(match.url).toBe('/first'); history.push('/second'); - jest.runAllTimers(); expect(host.innerHTML).toMatchInlineSnapshot(`"
second
"`); expect(match).toBeTruthy(); expect(match.path).toBe('second'); @@ -274,10 +262,8 @@ describe('@web-router/use-match', () => { }); render(); - jest.runAllTimers(); history.push('/second/child'); - jest.runAllTimers(); expect(host.innerHTML).toMatchInlineSnapshot(`"
second:
child
"`); expect(match1).toBeTruthy(); expect(match1.path).toBe('second'); @@ -340,10 +326,8 @@ describe('@web-router/use-match', () => { }); render(); - jest.runAllTimers(); history.push('/second/child/another'); - jest.runAllTimers(); expect(host.innerHTML).toMatchInlineSnapshot(`"
second:
child:
another
"`); expect(match1).toBeTruthy(); expect(match1.path).toBe('second'); @@ -409,10 +393,8 @@ describe('@web-router/use-match', () => { }); render(); - jest.runAllTimers(); history.push('/second/child/10/another'); - jest.runAllTimers(); expect(host.innerHTML).toMatchInlineSnapshot(`"
second:
child:
another
"`); expect(match1).toBeTruthy(); expect(match1.path).toBe('second'); @@ -425,7 +407,6 @@ describe('@web-router/use-match', () => { expect(match3.url).toBe('/second/child/10/another'); history.push('/second/child/20/another?q=xxx'); - jest.runAllTimers(); expect(host.innerHTML).toMatchInlineSnapshot(`"
second:
child:
another
"`); expect(match1).toBeTruthy(); expect(match1.path).toBe('second'); diff --git a/packages/web-router/src/use-params/use-params.spec.tsx b/packages/web-router/src/use-params/use-params.spec.tsx index cd95607a..a02ddee8 100644 --- a/packages/web-router/src/use-params/use-params.spec.tsx +++ b/packages/web-router/src/use-params/use-params.spec.tsx @@ -10,7 +10,6 @@ import { useParams } from './use-params'; let { host, render } = createBrowserEnv(); beforeEach(() => { - jest.useFakeTimers(); ({ host, render } = createBrowserEnv()); }); @@ -73,19 +72,15 @@ describe('@web-router/use-params', () => { }); render(); - jest.runAllTimers(); expect(host.innerHTML).toBe(`
root
`); history.push('/first/1'); - jest.runAllTimers(); expect(host.innerHTML).toBe(`
first: 1
`); history.push('/second/2'); - jest.runAllTimers(); expect(host.innerHTML).toBe(`
second: 2${replacer}
`); history.push('/second/2/a/3'); - jest.runAllTimers(); expect(host.innerHTML).toBe(`
second: 2
a: 2|3
`); }); }); From 8c2943aefd6472a37bfacf54a3a55e5165242862 Mon Sep 17 00:00:00 2001 From: atellmer Date: Fri, 12 Apr 2024 23:32:04 +0500 Subject: [PATCH 5/9] refactoring --- .../src/create-routes/create-routes.ts | 8 ++-- packages/web-router/src/router/router.tsx | 39 +++++++++++++------ 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/packages/web-router/src/create-routes/create-routes.ts b/packages/web-router/src/create-routes/create-routes.ts index 27b65ddd..507d5c6f 100644 --- a/packages/web-router/src/create-routes/create-routes.ts +++ b/packages/web-router/src/create-routes/create-routes.ts @@ -231,10 +231,10 @@ function getParamsMap(url: string, route: Route): Params { } function resolveRoute(url: string, routes: Array) { - const activeRoute = resolve(url, routes); - const slot = activeRoute ? activeRoute.render() : null; - const params = activeRoute ? getParamsMap(url, activeRoute) : null; - const value = { activeRoute, slot, params }; + const route = resolve(url, routes); + const slot = route.render(); + const params = getParamsMap(url, route); + const value = { activeRoute: route, slot, params }; return value; } diff --git a/packages/web-router/src/router/router.tsx b/packages/web-router/src/router/router.tsx index e050e072..871fc2b6 100644 --- a/packages/web-router/src/router/router.tsx +++ b/packages/web-router/src/router/router.tsx @@ -8,13 +8,14 @@ import { forwardRef, useImperativeHandle, nextTick, + detectIsString, } from '@dark-engine/core'; import { SLASH_MARK, PROTOCOL_MARK, WILDCARD_MARK } from '../constants'; -import { normalizePath, join } from '../utils'; +import { normalizePath, join, parseURL } from '../utils'; import { createRouterHistory } from '../history'; import { type RouterLocation, createRouterLocation } from '../location'; -import { type Routes, createRoutes, resolveRoute, mergePaths } from '../create-routes'; +import { type Routes, type Route, createRoutes, resolveRoute, mergePaths } from '../create-routes'; import { type RouterHistoryContextValue, type ActiveRouteContextValue, @@ -44,14 +45,15 @@ const Router = forwardRef( const history = useMemo(() => createRouterHistory(sourceURL), []); const routes = useMemo(() => createRoutes(sourceRoutes, normalizePath(baseURL)), []); const { protocol, host, pathname: url, search, hash } = location; - const { activeRoute, slot: $slot, params } = resolveRoute(url, routes); - const scope = useMemo(() => ({ location }), []); + const { activeRoute, slot: $slot, params } = useMemo(() => resolveRoute(url, routes), [url]); + const scope = useMemo(() => ({ location, fromOwnEffect: false }), []); const historyContext = useMemo(() => ({ history }), []); const routerContext = useMemo(() => ({ location, activeRoute, params }), [location]); scope.location = location; useLayoutEffect(() => { + if (!detectIsString(fullURL)) return; if (sourceURL !== scope.location.url) { setLocation(createRouterLocation(sourceURL)); } @@ -59,11 +61,20 @@ const Router = forwardRef( useLayoutEffect(() => { const unsubscribe = history.subscribe(url => { - const href = join(protocol, PROTOCOL_MARK, host, url); + const { pathname: url1, search: search1, hash: hash1 } = scope.location; + const { pathname: url2, search: search2, hash: hash2 } = parseURL(url); + const { activeRoute } = resolveRoute(url2, routes); + const prevURL = join(url1, search1, hash1); + const nextURL = merge(url2, activeRoute, search2, hash2); + + if (url !== nextURL || prevURL !== nextURL) { + const href = join(protocol, PROTOCOL_MARK, host, nextURL); - if (href !== scope.location.url) { setLocation(createRouterLocation(href)); + !scope.fromOwnEffect && activeRoute.marker !== WILDCARD_MARK && history.replace(nextURL); } + + scope.fromOwnEffect = false; }); return () => { @@ -72,15 +83,17 @@ const Router = forwardRef( }; }, []); + // ! useLayoutEffect(() => { - if (!activeRoute || activeRoute.marker === WILDCARD_MARK) return; - const url1 = join(url, search, hash); - const url2 = join(mergePaths(url, activeRoute.getPath()), search, hash); + if (activeRoute.marker === WILDCARD_MARK) return; + const prevURL = join(url, search, hash); + const nextURL = merge(url, activeRoute, search, hash); - if (url1 !== url2) { - history.replace(url2); + if (prevURL !== nextURL) { + scope.fromOwnEffect = true; + history.replace(nextURL); } - }, [location]); + }, []); useImperativeHandle(ref as MutableRef, () => ({ navigateTo: (url: string) => nextTick(() => history.push(url)), @@ -97,4 +110,6 @@ const Router = forwardRef( ), ); +const merge = (url: string, route: Route, s: string, h: string) => join(mergePaths(url, route.getPath()), s, h); + export { Router }; From 85591d2fb6b43627e0c3e618984538b482d0ee54 Mon Sep 17 00:00:00 2001 From: atellmer Date: Sat, 13 Apr 2024 11:15:38 +0500 Subject: [PATCH 6/9] refactoring --- packages/web-router/src/context/context.tsx | 2 +- .../src/create-routes/create-routes.ts | 14 ++++++-- packages/web-router/src/router/router.tsx | 34 ++++++++----------- .../src/use-location/use-location.ts | 6 ++-- .../web-router/src/use-match/use-match.ts | 6 ++-- .../web-router/src/use-params/use-params.ts | 6 ++-- 6 files changed, 37 insertions(+), 31 deletions(-) diff --git a/packages/web-router/src/context/context.tsx b/packages/web-router/src/context/context.tsx index 8aed19bf..32f079f0 100644 --- a/packages/web-router/src/context/context.tsx +++ b/packages/web-router/src/context/context.tsx @@ -7,7 +7,7 @@ import { type Route } from '../create-routes'; export type ActiveRouteContextValue = { location: RouterLocation; params: Map; - activeRoute: Route; + route: Route; }; const ActiveRouteContext = createContext(null, { displayName: 'ActiveRoute' }); diff --git a/packages/web-router/src/create-routes/create-routes.ts b/packages/web-router/src/create-routes/create-routes.ts index 507d5c6f..84201414 100644 --- a/packages/web-router/src/create-routes/create-routes.ts +++ b/packages/web-router/src/create-routes/create-routes.ts @@ -230,11 +230,17 @@ function getParamsMap(url: string, route: Route): Params { return map; } +type ResolveRouteValue = { + route: Route; + slot: DarkElement; + params: Params; +}; + function resolveRoute(url: string, routes: Array) { const route = resolve(url, routes); const slot = route.render(); const params = getParamsMap(url, route); - const value = { activeRoute: route, slot, params }; + const value: ResolveRouteValue = { route, slot, params }; return value; } @@ -243,4 +249,8 @@ const createRootPath = (path: string) => (path === SLASH_MARK ? '' : path); const split = (url: string, path: string) => [splitBySlash(url), splitBySlash(path)]; -export { type Route, createRoutes, resolve, resolveRoute, mergePaths }; +const merge = (url: string, route: Route, s: string, h: string) => join(mergePaths(url, route.getPath()), s, h); + +const detectIsWildcard = (route: Route) => route.marker === WILDCARD_MARK; + +export { type Route, createRoutes, resolve, resolveRoute, mergePaths, merge, detectIsWildcard }; diff --git a/packages/web-router/src/router/router.tsx b/packages/web-router/src/router/router.tsx index 871fc2b6..10a0dcbb 100644 --- a/packages/web-router/src/router/router.tsx +++ b/packages/web-router/src/router/router.tsx @@ -11,11 +11,11 @@ import { detectIsString, } from '@dark-engine/core'; -import { SLASH_MARK, PROTOCOL_MARK, WILDCARD_MARK } from '../constants'; +import { type Routes, createRoutes, resolveRoute, merge, detectIsWildcard } from '../create-routes'; +import { type RouterLocation, createRouterLocation } from '../location'; +import { SLASH_MARK, PROTOCOL_MARK } from '../constants'; import { normalizePath, join, parseURL } from '../utils'; import { createRouterHistory } from '../history'; -import { type RouterLocation, createRouterLocation } from '../location'; -import { type Routes, type Route, createRoutes, resolveRoute, mergePaths } from '../create-routes'; import { type RouterHistoryContextValue, type ActiveRouteContextValue, @@ -45,10 +45,10 @@ const Router = forwardRef( const history = useMemo(() => createRouterHistory(sourceURL), []); const routes = useMemo(() => createRoutes(sourceRoutes, normalizePath(baseURL)), []); const { protocol, host, pathname: url, search, hash } = location; - const { activeRoute, slot: $slot, params } = useMemo(() => resolveRoute(url, routes), [url]); - const scope = useMemo(() => ({ location, fromOwnEffect: false }), []); + const { route, slot: $slot, params } = useMemo(() => resolveRoute(url, routes), [url]); + const scope = useMemo(() => ({ location }), []); const historyContext = useMemo(() => ({ history }), []); - const routerContext = useMemo(() => ({ location, activeRoute, params }), [location]); + const routerContext = useMemo(() => ({ location, route, params }), [location]); scope.location = location; @@ -60,21 +60,20 @@ const Router = forwardRef( }, [sourceURL]); useLayoutEffect(() => { - const unsubscribe = history.subscribe(url => { + const unsubscribe = history.subscribe(candidateURL => { const { pathname: url1, search: search1, hash: hash1 } = scope.location; - const { pathname: url2, search: search2, hash: hash2 } = parseURL(url); - const { activeRoute } = resolveRoute(url2, routes); + const { pathname: url2, search: search2, hash: hash2 } = parseURL(candidateURL); + const { route: nextRoute } = resolveRoute(url2, routes); const prevURL = join(url1, search1, hash1); - const nextURL = merge(url2, activeRoute, search2, hash2); + const nextURL = merge(url2, nextRoute, search2, hash2); + const isDifferent = candidateURL !== nextURL; - if (url !== nextURL || prevURL !== nextURL) { + if (isDifferent || prevURL !== nextURL) { const href = join(protocol, PROTOCOL_MARK, host, nextURL); setLocation(createRouterLocation(href)); - !scope.fromOwnEffect && activeRoute.marker !== WILDCARD_MARK && history.replace(nextURL); + isDifferent && !detectIsWildcard(nextRoute) && history.replace(nextURL); } - - scope.fromOwnEffect = false; }); return () => { @@ -85,12 +84,11 @@ const Router = forwardRef( // ! useLayoutEffect(() => { - if (activeRoute.marker === WILDCARD_MARK) return; + if (detectIsWildcard(route)) return; const prevURL = join(url, search, hash); - const nextURL = merge(url, activeRoute, search, hash); + const nextURL = merge(url, route, search, hash); if (prevURL !== nextURL) { - scope.fromOwnEffect = true; history.replace(nextURL); } }, []); @@ -110,6 +108,4 @@ const Router = forwardRef( ), ); -const merge = (url: string, route: Route, s: string, h: string) => join(mergePaths(url, route.getPath()), s, h); - export { Router }; diff --git a/packages/web-router/src/use-location/use-location.ts b/packages/web-router/src/use-location/use-location.ts index 0d37a243..e6a41ce9 100644 --- a/packages/web-router/src/use-location/use-location.ts +++ b/packages/web-router/src/use-location/use-location.ts @@ -1,11 +1,11 @@ import { useActiveRouteContext, checkContextValue } from '../context'; function useLocation() { - const activeRoute = useActiveRouteContext(); + const active = useActiveRouteContext(); - checkContextValue(activeRoute); + checkContextValue(active); - return activeRoute.location; + return active.location; } export { useLocation }; diff --git a/packages/web-router/src/use-match/use-match.ts b/packages/web-router/src/use-match/use-match.ts index abf2a468..af518834 100644 --- a/packages/web-router/src/use-match/use-match.ts +++ b/packages/web-router/src/use-match/use-match.ts @@ -9,12 +9,12 @@ export type Match = { }; function useMatch() { - const activeRoute = useActiveRouteContext(); - checkContextValue(activeRoute); + const active = useActiveRouteContext(); + checkContextValue(active); const path = useCurrentPathContext(); const { location: { pathname: url }, - } = activeRoute; + } = active; const $url = useMemo(() => (path ? mergePaths(url, path) : ''), [url, path]); const value: Match = { path, url: $url }; diff --git a/packages/web-router/src/use-params/use-params.ts b/packages/web-router/src/use-params/use-params.ts index e5afba32..b5757ce4 100644 --- a/packages/web-router/src/use-params/use-params.ts +++ b/packages/web-router/src/use-params/use-params.ts @@ -2,11 +2,11 @@ import { useActiveRouteContext, checkContextValue } from '../context'; import { type Params } from '../create-routes'; function useParams(): Params { - const value = useActiveRouteContext(); + const active = useActiveRouteContext(); - checkContextValue(value); + checkContextValue(active); - return value.params; + return active.params; } export { Params, useParams }; From b06fad2a4582f9b9fd5c4c8f7999365f694f832d Mon Sep 17 00:00:00 2001 From: atellmer Date: Sat, 13 Apr 2024 16:22:25 +0500 Subject: [PATCH 7/9] added some tests for router --- .../src/create-routes/create-routes.spec.tsx | 101 +++++++++++- packages/web-router/src/index.ts | 2 +- .../web-router/src/router/router.spec.tsx | 156 +++++++++++++++++- .../src/use-history/use-history.spec.tsx | 5 + 4 files changed, 261 insertions(+), 3 deletions(-) diff --git a/packages/web-router/src/create-routes/create-routes.spec.tsx b/packages/web-router/src/create-routes/create-routes.spec.tsx index acc684e4..d1bdef21 100644 --- a/packages/web-router/src/create-routes/create-routes.spec.tsx +++ b/packages/web-router/src/create-routes/create-routes.spec.tsx @@ -1,4 +1,4 @@ -import { component } from '@dark-engine/core'; +import { component, Fragment } from '@dark-engine/core'; import { Routes } from './types'; import { createRoutes, resolve } from './create-routes'; @@ -728,4 +728,103 @@ describe('@web-router/create-routes', () => { expect(resolve('/third/', $routes).path).toBe('third'); expect(resolve('/broken/url', $routes).path).toBe(`first`); }); + + test('can resolve nested indexed routes', () => { + // https://github.com/atellmer/dark/issues/53 + const routes: Routes = [ + { + path: '/', + component: Fragment, + children: [ + { + path: '/', + component: Fragment, + }, + { + path: 'contact', + component: Fragment, + }, + { + path: 'de', + component: Fragment, + children: [ + { + path: '/', + component: Fragment, + }, + { + path: 'contact', + component: Fragment, + }, + ], + }, + ], + }, + { + path: '**', + redirectTo: '/', + }, + ]; + const $routes = createRoutes(routes); + + expect(resolve('/', $routes).path).toBe(``); + expect(resolve('/contact', $routes).path).toBe(`contact`); + expect(resolve('/de', $routes).path).toBe('de'); + expect(resolve('/de/contact', $routes).path).toBe(`de/contact`); + expect(resolve('/broken', $routes).path).toBe(``); + expect(resolve('/de/broken', $routes).path).toBe(``); + expect(resolve('/de/contact/broken', $routes).path).toBe(``); + }); + + test('can resolve i18n static routes', () => { + // https://github.com/atellmer/dark/issues/53 + const routes: Routes = [ + ...['en', 'it', 'fr'].map(lang => ({ + path: lang, + component: Fragment, + children: [ + { + path: 'contact', + component: Fragment, + }, + { + path: '**', + pathMatch: 'full', + redirectTo: '/not-found', + }, + ], + })), + { + path: '', + component: Fragment, + children: [ + { + path: 'contact', + component: Fragment, + }, + { + path: 'not-found', + component: Fragment, + }, + ], + }, + { + path: '**', + redirectTo: 'not-found', + }, + ] as Routes; + const $routes = createRoutes(routes); + + expect(resolve('/', $routes).path).toBe(``); + expect(resolve('/contact', $routes).path).toBe(`contact`); + expect(resolve('/en', $routes).path).toBe('en'); + expect(resolve('/en/contact', $routes).path).toBe(`en/contact`); + expect(resolve('/it', $routes).path).toBe('it'); + expect(resolve('/it/contact', $routes).path).toBe(`it/contact`); + expect(resolve('/fr', $routes).path).toBe('fr'); + expect(resolve('/fr/contact', $routes).path).toBe(`fr/contact`); + expect(resolve('/broken', $routes).path).toBe(`not-found`); + expect(resolve('/en/broken', $routes).path).toBe(`not-found`); + expect(resolve('/en/contact/broken', $routes).path).toBe(`not-found`); + }); }); diff --git a/packages/web-router/src/index.ts b/packages/web-router/src/index.ts index 5c7ea570..19c83d09 100644 --- a/packages/web-router/src/index.ts +++ b/packages/web-router/src/index.ts @@ -1,4 +1,4 @@ -export { type Routes, type PathMatchStrategy } from './create-routes'; +export { type Routes } from './create-routes'; export { type RouterRef, Router } from './router'; export { useLocation } from './use-location'; export { RouterLink } from './router-link'; diff --git a/packages/web-router/src/router/router.spec.tsx b/packages/web-router/src/router/router.spec.tsx index e8aceb0c..42f6e5da 100644 --- a/packages/web-router/src/router/router.spec.tsx +++ b/packages/web-router/src/router/router.spec.tsx @@ -1,4 +1,4 @@ -import { type DarkElement, type MutableRef, component, useRef } from '@dark-engine/core'; +import { type DarkElement, type MutableRef, component, useRef, Fragment } from '@dark-engine/core'; import { createBrowserEnv, replacer, resetBrowserHistory, sleep } from '@test-utils'; import { type Routes } from '../create-routes'; @@ -1042,4 +1042,158 @@ describe('@web-router/router', () => { expect(host.innerHTML).toBe(`
404
`); expect(location.href).toBe('http://localhost/broken/'); }); + + test('can render nested indexed routes', () => { + // https://github.com/atellmer/dark/issues/53 + const routes: Routes = [ + { + path: '/', + component: component<{ slot: DarkElement }>(({ slot }) => {slot}), + children: [ + { + path: '/', + component: component(() => /), + }, + { + path: 'contact', + component: component(() => /), + }, + { + path: 'de', + component: Fragment, + children: [ + { + path: '/', + component: component(() => de), + }, + { + path: 'contact', + component: component(() => de), + }, + ], + }, + ], + }, + { + path: '**', + redirectTo: '/', + }, + ]; + + const App = component(({ url }) => { + return ( + + {slot => slot} + + ); + }); + + render(); + expect(host.innerHTML).toMatchInlineSnapshot(`"/"`); + + render(); + expect(host.innerHTML).toMatchInlineSnapshot(`"/"`); + + render(); + expect(host.innerHTML).toMatchInlineSnapshot(`"de"`); + + render(); + expect(host.innerHTML).toMatchInlineSnapshot(`"de"`); + + render(); + expect(host.innerHTML).toMatchInlineSnapshot(`"de"`); + + render(); + expect(host.innerHTML).toMatchInlineSnapshot(`"/"`); + + render(); + expect(host.innerHTML).toMatchInlineSnapshot(`"/"`); + + render(); + expect(host.innerHTML).toMatchInlineSnapshot(`"/"`); + }); + + test('can render i18n static routes', () => { + // https://github.com/atellmer/dark/issues/53 + const routes: Routes = [ + ...['en', 'it', 'fr'].map(lang => ({ + path: lang, + component: component<{ slot: DarkElement }>(({ slot }) => ( + + {lang}:{slot || '/'} + + )), + children: [ + { + path: 'contact', + component: component(() => {lang}), + }, + { + path: '**', + pathMatch: 'full', + redirectTo: '/not-found', + }, + ], + })), + { + path: '', + component: component<{ slot: DarkElement }>(({ slot }) => {slot || '/'}), + children: [ + { + path: 'contact', + component: component(() => /), + }, + { + path: 'not-found', + component: component(() => /), + }, + ], + }, + { + path: '**', + redirectTo: 'not-found', + }, + ] as Routes; + + const App = component(({ url }) => { + return ( + + {slot => slot} + + ); + }); + + render(); + expect(host.innerHTML).toMatchInlineSnapshot(`"/"`); + + render(); + expect(host.innerHTML).toMatchInlineSnapshot(`"/"`); + + render(); + expect(host.innerHTML).toMatchInlineSnapshot(`"en:/"`); + + render(); + expect(host.innerHTML).toMatchInlineSnapshot(`"en:en"`); + + render(); + expect(host.innerHTML).toMatchInlineSnapshot(`"it:/"`); + + render(); + expect(host.innerHTML).toMatchInlineSnapshot(`"it:it"`); + + render(); + expect(host.innerHTML).toMatchInlineSnapshot(`"fr:/"`); + + render(); + expect(host.innerHTML).toMatchInlineSnapshot(`"fr:fr"`); + + render(); + expect(host.innerHTML).toMatchInlineSnapshot(`"/"`); + + render(); + expect(host.innerHTML).toMatchInlineSnapshot(`"/"`); + + render(); + expect(host.innerHTML).toMatchInlineSnapshot(`"/"`); + }); }); diff --git a/packages/web-router/src/use-history/use-history.spec.tsx b/packages/web-router/src/use-history/use-history.spec.tsx index 9eeb0f74..aba2a40d 100644 --- a/packages/web-router/src/use-history/use-history.spec.tsx +++ b/packages/web-router/src/use-history/use-history.spec.tsx @@ -9,6 +9,7 @@ import { RouterHistory } from '../history'; let { host, render } = createBrowserEnv(); beforeEach(() => { + jest.useFakeTimers(); ({ host, render } = createBrowserEnv()); }); @@ -48,18 +49,22 @@ describe('@web-router/use-history', () => { expect(location.href).toBe('http://localhost/'); history.push('/second/'); + jest.runAllTimers(); expect(host.innerHTML).toBe(`
second
`); expect(location.href).toBe('http://localhost/second'); history.push('/third/'); + jest.runAllTimers(); expect(host.innerHTML).toBe(`
third
`); expect(location.href).toBe('http://localhost/third'); history.push('/second'); + jest.runAllTimers(); expect(host.innerHTML).toBe(`
second
`); expect(location.href).toBe('http://localhost/second'); history.push('/second'); + jest.runAllTimers(); expect(host.innerHTML).toBe(`
second
`); expect(location.href).toBe('http://localhost/second'); }); From 8f4c83d0a61da7829ac22ebd8039c8f33ab287a6 Mon Sep 17 00:00:00 2001 From: atellmer Date: Sat, 13 Apr 2024 17:56:51 +0500 Subject: [PATCH 8/9] added the correct way to resolve promises --- packages/core/src/scheduler/scheduler.ts | 40 ++++++++++++++--------- packages/core/src/utils/utils.ts | 3 ++ packages/core/src/workloop/workloop.ts | 18 ++++------ packages/web-router/src/router/router.tsx | 3 +- 4 files changed, 35 insertions(+), 29 deletions(-) diff --git a/packages/core/src/scheduler/scheduler.ts b/packages/core/src/scheduler/scheduler.ts index 0a36a4a2..7f3a73b1 100644 --- a/packages/core/src/scheduler/scheduler.ts +++ b/packages/core/src/scheduler/scheduler.ts @@ -1,11 +1,11 @@ +import { ROOT, HOOK_DELIMETER, YIELD_INTERVAL, TaskPriority } from '../constants'; +import { getTime, detectIsFunction, detectIsPromise, nextTick } from '../utils'; import { type WorkLoop, workLoop, detectIsBusy } from '../workloop'; import { type SetPendingStatus } from '../start-transition'; import { type Callback } from '../shared'; -import { type Fiber } from '../fiber'; -import { ROOT, HOOK_DELIMETER, YIELD_INTERVAL, TaskPriority } from '../constants'; -import { getTime, detectIsFunction, nextTick } from '../utils'; import { EventEmitter } from '../emitter'; import { platform } from '../platform'; +import { type Fiber } from '../fiber'; class MessageChannel extends EventEmitter { port1: MessagePort = null; @@ -170,9 +170,10 @@ class Scheduler { private execute() { const isBusy = detectIsBusy(); - const { high, normal, low } = this.getQueues(); if (!isBusy && !this.isMessageLoopRunning) { + const { high, normal, low } = this.getQueues(); + this.pick(high) || this.pick(normal) || this.pick(low); } } @@ -187,26 +188,33 @@ class Scheduler { } private requestCallback(callback: WorkLoop) { - callback(false); - this.task = null; - this.execute(); + const result = callback(false); + + if (detectIsPromise(result)) { + result.finally(() => { + this.requestCallback(callback); + }); + } else { + this.task = null; + this.execute(); + } } private performWorkUntilDeadline() { if (this.scheduledCallback) { this.deadline = getTime() + YIELD_INTERVAL; - const hasMoreWork = this.scheduledCallback(true); + const result = this.scheduledCallback(true); - if (hasMoreWork) { + if (detectIsPromise(result)) { + result.finally(() => { + this.port.postMessage(null); + }); + } else if (result) { this.port.postMessage(null); } else { - if (hasMoreWork === null) { - setTimeout(() => this.port.postMessage(null)); // has promise - } else { - this.complete(this.task); - this.reset(); - this.execute(); - } + this.complete(this.task); + this.reset(); + this.execute(); } } else { this.isMessageLoopRunning = false; diff --git a/packages/core/src/utils/utils.ts b/packages/core/src/utils/utils.ts index d9130112..d1f80d65 100644 --- a/packages/core/src/utils/utils.ts +++ b/packages/core/src/utils/utils.ts @@ -23,6 +23,8 @@ const detectIsEmpty = (o: any) => detectIsNull(o) || detectIsUndefined(o); const detectIsFalsy = (o: any) => detectIsEmpty(o) || o === false; +const detectIsPromise = (o: any): o is Promise => o instanceof Promise; + const getTime = () => Date.now(); const dummyFn = () => {}; @@ -102,6 +104,7 @@ export { detectIsNull, detectIsEmpty, detectIsFalsy, + detectIsPromise, getTime, dummyFn, trueFn, diff --git a/packages/core/src/workloop/workloop.ts b/packages/core/src/workloop/workloop.ts index de12f1ac..f8414845 100644 --- a/packages/core/src/workloop/workloop.ts +++ b/packages/core/src/workloop/workloop.ts @@ -22,6 +22,7 @@ import { detectIsArray, detectIsFunction, detectIsTextBased, + detectIsPromise, createIndexKey, trueFn, } from '../utils'; @@ -53,12 +54,9 @@ import { type RestoreOptions, scheduler } from '../scheduler'; import { Fragment, detectIsFragment } from '../fragment'; import { unmountFiber } from '../unmount'; -let hasPendingPromise = false; +export type WorkLoop = (isAsync: boolean) => boolean | Promise | null; -export type WorkLoop = (isAsync: boolean) => boolean; - -function workLoop(isAsync: boolean): boolean | null { - if (hasPendingPromise) return null; +function workLoop(isAsync: boolean): boolean | Promise | null { const $scope = $$scope(); const wipFiber = $scope.getWorkInProgress(); let unit = $scope.getNextUnitOfWork(); @@ -78,12 +76,8 @@ function workLoop(isAsync: boolean): boolean | null { commit($scope); } } catch (err) { - if (err instanceof Promise) { - hasPendingPromise = true; - err.finally(() => { - hasPendingPromise = false; - !isAsync && workLoop(false); - }); + if (detectIsPromise(err)) { + return err; } else { const emitter = $scope.getEmitter(); @@ -378,7 +372,7 @@ function mount(fiber: Fiber, prev: Fiber, $scope: Scope) { component.children = result as Array; platform.detectIsPortal(inst) && fiber.markHost(PORTAL_HOST_MASK); } catch (err) { - if (err instanceof Promise) throw err; + if (detectIsPromise(err)) throw err; component.children = []; fiber.setError(err); } diff --git a/packages/web-router/src/router/router.tsx b/packages/web-router/src/router/router.tsx index 10a0dcbb..4c57aeb2 100644 --- a/packages/web-router/src/router/router.tsx +++ b/packages/web-router/src/router/router.tsx @@ -70,8 +70,9 @@ const Router = forwardRef( if (isDifferent || prevURL !== nextURL) { const href = join(protocol, PROTOCOL_MARK, host, nextURL); + const location = createRouterLocation(href); - setLocation(createRouterLocation(href)); + setLocation(location); isDifferent && !detectIsWildcard(nextRoute) && history.replace(nextURL); } }); From 98aff1d3cd38a1e7905dc40a96a929aea42ef1b9 Mon Sep 17 00:00:00 2001 From: atellmer Date: Sat, 13 Apr 2024 18:05:03 +0500 Subject: [PATCH 9/9] readme updated --- packages/styled/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/styled/README.md b/packages/styled/README.md index b4522811..9140fe4b 100644 --- a/packages/styled/README.md +++ b/packages/styled/README.md @@ -476,7 +476,7 @@ const GlobalStyle = createGlobalStyle<{ $light?: boolean }>` ``` ## Theming -The styled offers complete theming support by exporting a `` wrapper component. This component supplies a theme to all its child components through the context API. Consequently, all styled components in the render tree, regardless of their depth, can access the provided theme. +The styled offers complete theming support by exporting a `` wrapper component. This component supplies a theme to all its child components through the `Context API`. Consequently, all styled components in the render tree, regardless of their depth, can access the provided theme. ```tsx type Theme = { @@ -522,7 +522,7 @@ const style = useStyle(styled => ({ ## Server Side Rendering -The styled supports server-side rendering, complemented by stylesheet rehydration. Essentially, each time your application is rendered on the server, a `ServerStyleSheet` can be created and a provider can be added to your component tree, which accepts styles through a context API. +The styled supports server-side rendering, complemented by stylesheet rehydration. Essentially, each time your application is rendered on the server, a `ServerStyleSheet` can be created and a provider can be added to your component tree, which accepts styles through a `Context API`. Please note that `sheet.collectStyles()` already contains the provider and you do not need to do anything additional. ### Rendering to string @@ -535,7 +535,7 @@ const sheet = new ServerStyleSheet(); try { const app = await renderToString(sheet.collectStyles()); const tags = sheet.getStyleTags(); - const mark = '{{%styles%}}' // somewhere in your + const mark = '__styled__' // somewhere in your const page = `${app}`.replace(mark, tags.join('')); res.statusCode = 200;