From c050fbbfaa0c4cea4f6c1390085aef517b65bb85 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Sun, 21 Feb 2021 11:08:43 +0100 Subject: [PATCH 01/21] support route parameters and inject them into the component --- packages/preact-iso/router.js | 45 ++++++++++---- packages/sw-plugin/example/public/index.js | 1 - packages/wmr/demo/public/header.tsx | 1 - packages/wmr/demo/public/index.tsx | 6 +- packages/wmr/demo/public/lib/loc.js | 69 ++++++++++++++++++---- 5 files changed, 96 insertions(+), 26 deletions(-) diff --git a/packages/preact-iso/router.js b/packages/preact-iso/router.js index f943a0730..a96160121 100644 --- a/packages/preact-iso/router.js +++ b/packages/preact-iso/router.js @@ -1,5 +1,5 @@ import { h, createContext, cloneElement } from 'preact'; -import { useContext, useMemo, useReducer, useEffect, useRef } from 'preact/hooks'; +import { useContext, useMemo, useReducer, useEffect, useRef, useState } from 'preact/hooks'; const UPDATE = (state, url, push) => { if (url && url.type === 'click') { @@ -18,14 +18,37 @@ const UPDATE = (state, url, push) => { return url; }; +const exec = (url, route, matches) => { + url = url.trim('/').split('/'); + route = (route || '').trim('/').split('/'); + for (let i=0, val; i= '?') continue; + // rest (+/*) match: + matches[param] = url.slice(i).map(decodeURIComponent).join('/'); + break; + } + + return matches; +} + export function LocationProvider(props) { const [url, route] = useReducer(UPDATE, location.pathname + location.search); + const [matchedRoute, setMatchedRoute] = useState(); const value = useMemo(() => { const u = new URL(url, location.origin); const path = u.pathname.replace(/(.)\/$/g, '$1'); // @ts-ignore-next - return { url, path, query: Object.fromEntries(u.searchParams), route }; + return { url, path, query: Object.fromEntries(u.searchParams), route, setMatchedRoute }; }, [url]); useEffect(() => { @@ -82,15 +105,17 @@ export function Router(props) { } else commit(); }, [url]); - const children = [].concat(...props.children); - - let a = children.filter(c => c.props.path === path); - - if (a.length == 0) a = children.filter(c => c.props.default); - - curChildren.current = a.map((p, i) => cloneElement(p, { path, query })); + let p, d, m; + [].concat(props.children || []).some(vnode => { + const matches = exec(path, vnode.props.path, m = { path, query }); + if (matches) { + return p = cloneElement(vnode, { ...m, ...matches }) + } else { + return vnode.props.default ? d = cloneElement(vnode, m) : 0, undefined + } + }); - return curChildren.current.concat(prevChildren.current || []); + return [curChildren.current = p || d, prevChildren.current]; } Router.Provider = LocationProvider; diff --git a/packages/sw-plugin/example/public/index.js b/packages/sw-plugin/example/public/index.js index 0dfcbf2cc..d0d767878 100644 --- a/packages/sw-plugin/example/public/index.js +++ b/packages/sw-plugin/example/public/index.js @@ -1,4 +1,3 @@ import swURL from 'sw:./sw.js'; -console.log(swURL); navigator.serviceWorker.register(swURL); diff --git a/packages/wmr/demo/public/header.tsx b/packages/wmr/demo/public/header.tsx index c79bc3a21..a7693b11b 100644 --- a/packages/wmr/demo/public/header.tsx +++ b/packages/wmr/demo/public/header.tsx @@ -15,7 +15,6 @@ export default function Header() { ); diff --git a/packages/wmr/demo/public/index.tsx b/packages/wmr/demo/public/index.tsx index 08d5380fb..0152f23cc 100644 --- a/packages/wmr/demo/public/index.tsx +++ b/packages/wmr/demo/public/index.tsx @@ -1,5 +1,5 @@ import { h, render } from 'preact'; -import { Loc, Router } from './lib/loc.js'; +import { LocationProvider, Router } from './lib/loc.js'; import lazy, { ErrorBoundary } from './lib/lazy.js'; import Home from './pages/home.js'; // import About from './pages/about/index.js'; @@ -15,7 +15,7 @@ const Environment = lazy(async () => (await import('./pages/environment/index.js export function App() { return ( - +
@@ -30,7 +30,7 @@ export function App() {
-
+ ); } diff --git a/packages/wmr/demo/public/lib/loc.js b/packages/wmr/demo/public/lib/loc.js index 2f159a4a4..dde4a97c0 100644 --- a/packages/wmr/demo/public/lib/loc.js +++ b/packages/wmr/demo/public/lib/loc.js @@ -5,51 +5,89 @@ const UPDATE = (state, url, push) => { if (url && url.type === 'click') { const link = url.target.closest('a[href]'); if (!link || link.origin != location.origin) return state; + url.preventDefault(); push = true; url = link.href.replace(location.origin, ''); - } else if (typeof url !== 'string') url = location.pathname + location.search; + } else if (typeof url !== 'string') { + url = location.pathname + location.search; + } + if (push === true) history.pushState(null, '', url); else if (push === false) history.replaceState(null, '', url); return url; }; -export function Loc(props) { +const exec = (url, route, matches) => { + url = url.trim('/').split('/'); + route = (route || '').trim('/').split('/'); + for (let i=0, val; i= '?') continue; + // rest (+/*) match: + matches[param] = url.slice(i).map(decodeURIComponent).join('/'); + break; + } + + return matches; +} + +export function LocationProvider(props) { const [url, route] = useReducer(UPDATE, location.pathname + location.search); + const value = useMemo(() => { const u = new URL(url, location.origin); const path = u.pathname.replace(/(.)\/$/g, '$1'); + // @ts-ignore-next return { url, path, query: Object.fromEntries(u.searchParams), route }; }, [url]); + useEffect(() => { addEventListener('click', route); addEventListener('popstate', route); + return () => { removeEventListener('click', route); removeEventListener('popstate', route); }; }); - return h(Loc.ctx.Provider, { value }, props.children); + + // @ts-ignore + return h(LocationProvider.ctx.Provider, { value }, props.children); } export function Router(props) { const [, update] = useReducer(c => c + 1, 0); + const loc = useLoc(); + const { url, path, query } = loc; + const cur = useRef(loc); const prev = useRef(); const curChildren = useRef(); const prevChildren = useRef(); const pending = useRef(); + if (url !== cur.current.url) { pending.current = null; prev.current = cur.current; prevChildren.current = curChildren.current; cur.current = loc; } + this.componentDidCatch = err => { if (err && err.then) pending.current = err; }; + useEffect(() => { let p = pending.current; const commit = () => { @@ -58,20 +96,29 @@ export function Router(props) { prev.current = prevChildren.current = null; update(0); }; + if (p) { if (props.onLoadStart) props.onLoadStart(url); p.then(commit); } else commit(); }, [url]); - const children = [].concat(...props.children); - let a = children.filter(c => c.props.path === path); - if (a.length == 0) a = children.filter(c => c.props.default); - curChildren.current = a.map((p, i) => cloneElement(p, { path, query })); - return curChildren.current.concat(prevChildren.current || []); + + let p, d, m; + [].concat(props.children || []).some(vnode => { + const matches = exec(path, vnode.props.path, m = { path, query }); + if (matches) { + return p = cloneElement(vnode, { ...m, ...matches }) + } else { + return vnode.props.default ? d = cloneElement(vnode, m) : 0, undefined + } + }); + + return [curChildren.current = p || d, prevChildren.current]; } -Loc.Router = Router; +Router.Provider = LocationProvider; -Loc.ctx = createContext(/** @type {{ url: string, path: string, query: object, route }} */ ({})); +LocationProvider.ctx = createContext(/** @type {{ url: string, path: string, query: object, route }} */ ({})); -export const useLoc = () => useContext(Loc.ctx); +export const useLoc = () => useContext(LocationProvider.ctx); +export const useLocation = useLoc; From d64997e11bedcb1ee36ac895f80d894f13a1d25c Mon Sep 17 00:00:00 2001 From: jdecroock Date: Sun, 21 Feb 2021 11:10:11 +0100 Subject: [PATCH 02/21] add changeset --- .changeset/neat-wasps-mix.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/neat-wasps-mix.md diff --git a/.changeset/neat-wasps-mix.md b/.changeset/neat-wasps-mix.md new file mode 100644 index 000000000..6960b9f14 --- /dev/null +++ b/.changeset/neat-wasps-mix.md @@ -0,0 +1,5 @@ +--- +'preact-iso': minor +--- + +Support route params and inject them into the rendered route. From 4ba4352b4b6f2232c2d90a9c3dc40dbb7e21a3c2 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Sun, 21 Feb 2021 11:33:04 +0100 Subject: [PATCH 03/21] cleanup residual variables --- packages/preact-iso/router.js | 5 ++--- packages/wmr/demo/public/header.tsx | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/preact-iso/router.js b/packages/preact-iso/router.js index a96160121..5b5083822 100644 --- a/packages/preact-iso/router.js +++ b/packages/preact-iso/router.js @@ -1,5 +1,5 @@ import { h, createContext, cloneElement } from 'preact'; -import { useContext, useMemo, useReducer, useEffect, useRef, useState } from 'preact/hooks'; +import { useContext, useMemo, useReducer, useEffect, useRef } from 'preact/hooks'; const UPDATE = (state, url, push) => { if (url && url.type === 'click') { @@ -42,13 +42,12 @@ const exec = (url, route, matches) => { export function LocationProvider(props) { const [url, route] = useReducer(UPDATE, location.pathname + location.search); - const [matchedRoute, setMatchedRoute] = useState(); const value = useMemo(() => { const u = new URL(url, location.origin); const path = u.pathname.replace(/(.)\/$/g, '$1'); // @ts-ignore-next - return { url, path, query: Object.fromEntries(u.searchParams), route, setMatchedRoute }; + return { url, path, query: Object.fromEntries(u.searchParams), route }; }, [url]); useEffect(() => { diff --git a/packages/wmr/demo/public/header.tsx b/packages/wmr/demo/public/header.tsx index a7693b11b..c79bc3a21 100644 --- a/packages/wmr/demo/public/header.tsx +++ b/packages/wmr/demo/public/header.tsx @@ -15,6 +15,7 @@ export default function Header() { ); From 7fd2028fa44886fa26fe6c63dd532531ba7ac603 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Sun, 21 Feb 2021 11:33:29 +0100 Subject: [PATCH 04/21] fix lint-staged --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d8f7be3f7..fc79dcec2 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "arrowParens": "avoid" }, "lint-staged": { - "packages/{src,test}/**/*.js": [ + "packages/**/{src,test}/**/*.js": [ "eslint --fix", "prettier --write" ], From c86764087b072ddcecf29bf08e1d1df0c7c3ee5f Mon Sep 17 00:00:00 2001 From: jdecroock Date: Sun, 21 Feb 2021 12:38:36 +0100 Subject: [PATCH 05/21] reduce diff --- package.json | 2 +- packages/sw-plugin/example/public/index.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index fc79dcec2..d8f7be3f7 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "arrowParens": "avoid" }, "lint-staged": { - "packages/**/{src,test}/**/*.js": [ + "packages/{src,test}/**/*.js": [ "eslint --fix", "prettier --write" ], diff --git a/packages/sw-plugin/example/public/index.js b/packages/sw-plugin/example/public/index.js index d0d767878..0dfcbf2cc 100644 --- a/packages/sw-plugin/example/public/index.js +++ b/packages/sw-plugin/example/public/index.js @@ -1,3 +1,4 @@ import swURL from 'sw:./sw.js'; +console.log(swURL); navigator.serviceWorker.register(swURL); From 691acd2b0a7020d580079a009ffd5eec9b6ba8f7 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Sun, 21 Feb 2021 12:42:23 +0100 Subject: [PATCH 06/21] always return undefined --- packages/preact-iso/router.js | 5 +++-- packages/wmr/demo/public/lib/loc.js | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/preact-iso/router.js b/packages/preact-iso/router.js index 5b5083822..f053348ed 100644 --- a/packages/preact-iso/router.js +++ b/packages/preact-iso/router.js @@ -18,7 +18,7 @@ const UPDATE = (state, url, push) => { return url; }; -const exec = (url, route, matches) => { +export const exec = (url, route, matches) => { url = url.trim('/').split('/'); route = (route || '').trim('/').split('/'); for (let i=0, val; i Date: Sun, 21 Feb 2021 12:51:45 +0100 Subject: [PATCH 07/21] add tests --- .github/workflows/main.yml | 9 ++++++--- packages/preact-iso/package.json | 11 +++++++++++ packages/preact-iso/router.js | 2 +- packages/preact-iso/test/match.test.js | 27 ++++++++++++++++++++++++++ packages/wmr/demo/public/lib/loc.js | 3 ++- yarn.lock | 4 ++-- 6 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 packages/preact-iso/test/match.test.js diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 967fc3e9b..2141799a6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,10 +5,10 @@ on: branches: - main paths: - - packages/wmr/** + - packages/{wmr,preact-iso}/** pull_request: paths: - - packages/wmr/** + - packages/{wmr,preact-iso}/** jobs: build: @@ -28,8 +28,11 @@ jobs: run: yarn --frozen-lockfile - name: Build run: yarn workspace wmr build - - name: Test + - name: Test wmr run: yarn workspace wmr test + - name: Test preact-iso + run: yarn workspace preact-iso test + lhci: runs-on: ubuntu-latest steps: diff --git a/packages/preact-iso/package.json b/packages/preact-iso/package.json index f34f1f0d1..d6aac3603 100644 --- a/packages/preact-iso/package.json +++ b/packages/preact-iso/package.json @@ -6,6 +6,14 @@ "module": "./index.js", "types": "./index.d.ts", "type": "module", + "scripts": { + "test": "jest" + }, + "babel": { + "plugins": [ + "@babel/plugin-transform-modules-commonjs" + ] + }, "exports": { ".": "./index.js", "./router": "./router.js", @@ -21,6 +29,9 @@ }, "license": "MIT", "devDependencies": { + "@babel/plugin-transform-modules-commonjs": "7.12.13", + "babel-jest": "^26.1.0", + "jest": "26.6.3", "preact": "^10.5.7", "preact-render-to-string": "^5.1.12" }, diff --git a/packages/preact-iso/router.js b/packages/preact-iso/router.js index f053348ed..2bbcda484 100644 --- a/packages/preact-iso/router.js +++ b/packages/preact-iso/router.js @@ -110,7 +110,7 @@ export function Router(props) { if (matches) { return p = cloneElement(vnode, { ...m, ...matches }) } else { - vnode.props.default && d = cloneElement(vnode, m); + if (vnode.props.default) d = cloneElement(vnode, m); return undefined; } }); diff --git a/packages/preact-iso/test/match.test.js b/packages/preact-iso/test/match.test.js new file mode 100644 index 000000000..124b7f571 --- /dev/null +++ b/packages/preact-iso/test/match.test.js @@ -0,0 +1,27 @@ +import { exec } from '../router.js'; + +describe('match', () => { + it('Base route', () => { + const accurateResult = exec('/', '/', { path: '/' }); + expect(accurateResult).toEqual({ path: '/' }); + + const inaccurateResult = exec('/user/1', '/', { path: '/' }); + expect(inaccurateResult).toEqual(undefined); + }); + + it('Param route', () => { + const accurateResult = exec('/user/2', '/user/:id', { path: '/' }); + expect(accurateResult).toEqual({ path: '/', id: '2' }); + + const inaccurateResult = exec('/', '/user/:id', { path: '/' }); + expect(inaccurateResult).toEqual(undefined); + }); + + it('Optional param route', () => { + const accurateResult = exec('/user', '/user/:id?', { path: '/' }); + expect(accurateResult).toEqual({ path: '/' }); + + const inaccurateResult = exec('/', '/user/:id?', { path: '/' }); + expect(inaccurateResult).toEqual(undefined); + }); +}); diff --git a/packages/wmr/demo/public/lib/loc.js b/packages/wmr/demo/public/lib/loc.js index 2dd47ceec..317dfa761 100644 --- a/packages/wmr/demo/public/lib/loc.js +++ b/packages/wmr/demo/public/lib/loc.js @@ -105,11 +105,12 @@ export function Router(props) { let p, d, m; [].concat(props.children || []).some(vnode => { + console.log(path, vnode.props.path, query); const matches = exec(path, vnode.props.path, m = { path, query }); if (matches) { return p = cloneElement(vnode, { ...m, ...matches }) } else { - vnode.props.default && d = cloneElement(vnode, m); + if (vnode.props.default) d = cloneElement(vnode, m); return undefined; } }); diff --git a/yarn.lock b/yarn.lock index ef577b09c..08763d81b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -607,7 +607,7 @@ "@babel/helper-plugin-utils" "^7.12.13" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@^7.10.4", "@babel/plugin-transform-modules-commonjs@^7.12.13": +"@babel/plugin-transform-modules-commonjs@7.12.13", "@babel/plugin-transform-modules-commonjs@^7.10.4", "@babel/plugin-transform-modules-commonjs@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.12.13.tgz#5043b870a784a8421fa1fd9136a24f294da13e50" integrity sha512-OGQoeVXVi1259HjuoDnsQMlMkT9UkZT9TpXAsqWplS/M0N1g3TJAn/ByOCeQu7mfjc5WpSsRU+jV1Hd89ts0kQ== @@ -5627,7 +5627,7 @@ jest-worker@^26.2.1, jest-worker@^26.6.2: merge-stream "^2.0.0" supports-color "^7.0.0" -jest@^26.1.0: +jest@26.6.3, jest@^26.1.0: version "26.6.3" resolved "https://registry.yarnpkg.com/jest/-/jest-26.6.3.tgz#40e8fdbe48f00dfa1f0ce8121ca74b88ac9148ef" integrity sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q== From 94354c588991bfe920d3b609034c573269cbcaf9 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Sun, 21 Feb 2021 12:51:58 +0100 Subject: [PATCH 08/21] remove log --- packages/wmr/demo/public/lib/loc.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/wmr/demo/public/lib/loc.js b/packages/wmr/demo/public/lib/loc.js index 317dfa761..0268d8b05 100644 --- a/packages/wmr/demo/public/lib/loc.js +++ b/packages/wmr/demo/public/lib/loc.js @@ -105,7 +105,6 @@ export function Router(props) { let p, d, m; [].concat(props.children || []).some(vnode => { - console.log(path, vnode.props.path, query); const matches = exec(path, vnode.props.path, m = { path, query }); if (matches) { return p = cloneElement(vnode, { ...m, ...matches }) From 3c8db426473cda59be85d37aa5ed9e11658d612f Mon Sep 17 00:00:00 2001 From: jdecroock Date: Sun, 21 Feb 2021 12:54:40 +0100 Subject: [PATCH 09/21] update readme --- packages/preact-iso/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/preact-iso/README.md b/packages/preact-iso/README.md index e165f57ff..3fcae85f0 100644 --- a/packages/preact-iso/README.md +++ b/packages/preact-iso/README.md @@ -85,20 +85,22 @@ import { LocationProvider, Router, useLoc } from 'preact-iso/router'; // Asynchronous (throws a promise) const Home = lazy(() => import('./routes/home.js')); const Profile = lazy(() => import('./routes/profile.js')); +const Profiles = lazy(() => import('./routes/profiles.js')); const App = () => ( - + + ); ``` -During prerendering, the generated HTML includes our full `` and `` component output because it waits for the `lazy()`-wrapped `import()` to resolve. +During prerendering, the generated HTML includes our full `` and `` component output because it waits for the `lazy()`-wrapped `import()` to resolve. **Progressive Hydration:** When the app is hydrated on the client, the route (`Home` or `Profile` in this case) suspends. This causes hydration for that part of the page to be deferred until the route's `import()` is resolved, at which point that part of the page automatically finishes hydrating. From f99ee07fff5b86fd01b4c548fa41b8b2e5cc6493 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Sun, 21 Feb 2021 13:15:05 +0100 Subject: [PATCH 10/21] fix indentation --- packages/wmr/demo/public/lib/loc.js | 42 ++++++++++++++--------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/wmr/demo/public/lib/loc.js b/packages/wmr/demo/public/lib/loc.js index 0268d8b05..ea3c8bab1 100644 --- a/packages/wmr/demo/public/lib/loc.js +++ b/packages/wmr/demo/public/lib/loc.js @@ -19,26 +19,26 @@ const UPDATE = (state, url, push) => { }; const exec = (url, route, matches) => { - url = url.trim('/').split('/'); - route = (route || '').trim('/').split('/'); - for (let i=0, val; i= '?') continue; - // rest (+/*) match: - matches[param] = url.slice(i).map(decodeURIComponent).join('/'); - break; + url = url.trim('/').split('/'); + route = (route || '').trim('/').split('/'); + for (let i = 0, val; i < Math.max(url.length, route.length); i++) { + let [, m, param, flag] = (route[i] || '').match(/^(\:?)(.*?)([+*?]?)$/); + val = url[i]; + // segment match: + if (!m && param == val) continue; + // segment mismatch / missing required field: + if (!m || (!val && flag != '?' && flag != '*')) return; + // field match: + matches[param] = val && decodeURIComponent(val); + // normal/optional field: + if (flag >= '?') continue; + // rest (+/*) match: + matches[param] = url.slice(i).map(decodeURIComponent).join('/'); + break; } - return matches; -} + return matches; +}; export function LocationProvider(props) { const [url, route] = useReducer(UPDATE, location.pathname + location.search); @@ -105,16 +105,16 @@ export function Router(props) { let p, d, m; [].concat(props.children || []).some(vnode => { - const matches = exec(path, vnode.props.path, m = { path, query }); + const matches = exec(path, vnode.props.path, (m = { path, query })); if (matches) { - return p = cloneElement(vnode, { ...m, ...matches }) + return (p = cloneElement(vnode, { ...m, ...matches })); } else { if (vnode.props.default) d = cloneElement(vnode, m); return undefined; } }); - return [curChildren.current = p || d, prevChildren.current]; + return [(curChildren.current = p || d), prevChildren.current]; } Router.Provider = LocationProvider; From 9e3da740f59425bb6f55b309f144f699b8e48a63 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Sun, 21 Feb 2021 13:17:42 +0100 Subject: [PATCH 11/21] Update packages/preact-iso/router.js Co-authored-by: Marvin Hagemeister --- packages/preact-iso/router.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/preact-iso/router.js b/packages/preact-iso/router.js index 2bbcda484..e35964ba7 100644 --- a/packages/preact-iso/router.js +++ b/packages/preact-iso/router.js @@ -109,10 +109,9 @@ export function Router(props) { const matches = exec(path, vnode.props.path, m = { path, query }); if (matches) { return p = cloneElement(vnode, { ...m, ...matches }) - } else { - if (vnode.props.default) d = cloneElement(vnode, m); - return undefined; } + + if (vnode.props.default) d = cloneElement(vnode, m); }); return [curChildren.current = p || d, prevChildren.current]; From 8106eccdd946297105cb6d57b0601715b05a6a99 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Sun, 21 Feb 2021 16:15:34 +0100 Subject: [PATCH 12/21] use correct yml syntax --- .github/workflows/main.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2141799a6..930af9710 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,10 +5,12 @@ on: branches: - main paths: - - packages/{wmr,preact-iso}/** + - packages/wmr/** + - packages/preact-iso/** pull_request: paths: - - packages/{wmr,preact-iso}/** + - packages/wmr/** + - packages/preact-iso/** jobs: build: From 58d5c30edaf564a557b07ef5d684fefe63b4a75c Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Sun, 21 Feb 2021 17:31:48 +0100 Subject: [PATCH 13/21] use context for route --- packages/preact-iso/README.md | 2 ++ packages/preact-iso/index.js | 2 +- packages/preact-iso/router.js | 10 ++++++++-- packages/wmr/demo/public/lib/loc.js | 9 ++++++++- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/preact-iso/README.md b/packages/preact-iso/README.md index 3fcae85f0..cd1fbd1b9 100644 --- a/packages/preact-iso/README.md +++ b/packages/preact-iso/README.md @@ -102,6 +102,8 @@ const App = () => ( During prerendering, the generated HTML includes our full `` and `` component output because it waits for the `lazy()`-wrapped `import()` to resolve. +You can use the `useRoute` hook to get information of the route you are currently on. + **Progressive Hydration:** When the app is hydrated on the client, the route (`Home` or `Profile` in this case) suspends. This causes hydration for that part of the page to be deferred until the route's `import()` is resolved, at which point that part of the page automatically finishes hydrating. **Seamless Routing:** Switch switching between routes on the client, the Router is aware of asynchronous dependencies in routes. Instead of clearing the current route and showing a loading spinner while waiting for the next route (or its data), the router preserves the current route in-place until the incoming route has finished loading, then they are swapped. diff --git a/packages/preact-iso/index.js b/packages/preact-iso/index.js index 565ab1b4c..8cefe40b0 100644 --- a/packages/preact-iso/index.js +++ b/packages/preact-iso/index.js @@ -1,4 +1,4 @@ -export { Router, LocationProvider, useLoc, useLocation } from './router.js'; +export { Router, LocationProvider, useLoc, useLocation, useRoute } from './router.js'; export { default as lazy, ErrorBoundary } from './lazy.js'; export { default as hydrate } from './hydrate.js'; diff --git a/packages/preact-iso/router.js b/packages/preact-iso/router.js index e35964ba7..3339cf112 100644 --- a/packages/preact-iso/router.js +++ b/packages/preact-iso/router.js @@ -108,9 +108,13 @@ export function Router(props) { [].concat(props.children || []).some(vnode => { const matches = exec(path, vnode.props.path, m = { path, query }); if (matches) { - return p = cloneElement(vnode, { ...m, ...matches }) + return p = ( + + {cloneElement(vnode, { ...m, ...matches })} + + ); } - + if (vnode.props.default) d = cloneElement(vnode, m); }); @@ -120,6 +124,8 @@ export function Router(props) { Router.Provider = LocationProvider; LocationProvider.ctx = createContext(/** @type {{ url: string, path: string, query: object, route }} */ ({})); +const RouteContext = createContext({}); export const useLoc = () => useContext(LocationProvider.ctx); export const useLocation = useLoc; +export const useRoute = () => useContext(RouteContext); diff --git a/packages/wmr/demo/public/lib/loc.js b/packages/wmr/demo/public/lib/loc.js index ea3c8bab1..5278bcb62 100644 --- a/packages/wmr/demo/public/lib/loc.js +++ b/packages/wmr/demo/public/lib/loc.js @@ -107,7 +107,11 @@ export function Router(props) { [].concat(props.children || []).some(vnode => { const matches = exec(path, vnode.props.path, (m = { path, query })); if (matches) { - return (p = cloneElement(vnode, { ...m, ...matches })); + return p = ( + + {cloneElement(vnode, { ...m, ...matches })} + + ); } else { if (vnode.props.default) d = cloneElement(vnode, m); return undefined; @@ -121,5 +125,8 @@ Router.Provider = LocationProvider; LocationProvider.ctx = createContext(/** @type {{ url: string, path: string, query: object, route }} */ ({})); +const RouteContext = createContext({}); + export const useLoc = () => useContext(LocationProvider.ctx); export const useLocation = useLoc; +export const useRoute = () => useContext(RouteContext); From 0a9bfb4d1a7524d7fba73f32cc67484c76078564 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Sun, 21 Feb 2021 17:43:02 +0100 Subject: [PATCH 14/21] use createElement (avoid jsx) --- packages/preact-iso/router.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/preact-iso/router.js b/packages/preact-iso/router.js index 3339cf112..04ede02ac 100644 --- a/packages/preact-iso/router.js +++ b/packages/preact-iso/router.js @@ -108,10 +108,10 @@ export function Router(props) { [].concat(props.children || []).some(vnode => { const matches = exec(path, vnode.props.path, m = { path, query }); if (matches) { - return p = ( - - {cloneElement(vnode, { ...m, ...matches })} - + return p = h( + RouteContext.Provider, + { value: { ...matches } }, + cloneElement(vnode, { ...m, ...matches }) ); } From c495a2cc16b90bd22131f7a9f3de21dd90819352 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Sun, 21 Feb 2021 17:45:15 +0100 Subject: [PATCH 15/21] Update neat-wasps-mix.md --- .changeset/neat-wasps-mix.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/neat-wasps-mix.md b/.changeset/neat-wasps-mix.md index 6960b9f14..1a61564f3 100644 --- a/.changeset/neat-wasps-mix.md +++ b/.changeset/neat-wasps-mix.md @@ -2,4 +2,4 @@ 'preact-iso': minor --- -Support route params and inject them into the rendered route. +Support route params and inject them into the rendered route, add the `useRoute` hook so we can retrieve route parameters from anywhere in the subtree. From 541aae55cf91f84f27f00335720774fc021d5a9c Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Sun, 21 Feb 2021 17:45:39 +0100 Subject: [PATCH 16/21] remove unused import --- packages/preact-iso/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/preact-iso/index.js b/packages/preact-iso/index.js index 8cefe40b0..565ab1b4c 100644 --- a/packages/preact-iso/index.js +++ b/packages/preact-iso/index.js @@ -1,4 +1,4 @@ -export { Router, LocationProvider, useLoc, useLocation, useRoute } from './router.js'; +export { Router, LocationProvider, useLoc, useLocation } from './router.js'; export { default as lazy, ErrorBoundary } from './lazy.js'; export { default as hydrate } from './hydrate.js'; From 0035701aee7cf4d0c1e8ef899fe0ecd4297dcd48 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Sun, 21 Feb 2021 20:16:05 +0100 Subject: [PATCH 17/21] Update packages/preact-iso/router.js Co-authored-by: Jason Miller --- packages/preact-iso/router.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/preact-iso/router.js b/packages/preact-iso/router.js index 04ede02ac..88dc7da96 100644 --- a/packages/preact-iso/router.js +++ b/packages/preact-iso/router.js @@ -110,8 +110,8 @@ export function Router(props) { if (matches) { return p = h( RouteContext.Provider, - { value: { ...matches } }, - cloneElement(vnode, { ...m, ...matches }) + { value: m }, + cloneElement(vnode, m) ); } From 8da84b2b51ebb9be8fe45fd4a988ec0fbd1bd425 Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Sun, 21 Feb 2021 14:21:38 -0500 Subject: [PATCH 18/21] Use Jest's native ESM support (#357) --- packages/preact-iso/package.json | 9 +-------- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/preact-iso/package.json b/packages/preact-iso/package.json index d6aac3603..df5b1f658 100644 --- a/packages/preact-iso/package.json +++ b/packages/preact-iso/package.json @@ -7,12 +7,7 @@ "types": "./index.d.ts", "type": "module", "scripts": { - "test": "jest" - }, - "babel": { - "plugins": [ - "@babel/plugin-transform-modules-commonjs" - ] + "test": "node --experimental-vm-modules node_modules/.bin/jest" }, "exports": { ".": "./index.js", @@ -29,8 +24,6 @@ }, "license": "MIT", "devDependencies": { - "@babel/plugin-transform-modules-commonjs": "7.12.13", - "babel-jest": "^26.1.0", "jest": "26.6.3", "preact": "^10.5.7", "preact-render-to-string": "^5.1.12" diff --git a/yarn.lock b/yarn.lock index 08763d81b..3d6d634d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -607,7 +607,7 @@ "@babel/helper-plugin-utils" "^7.12.13" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@7.12.13", "@babel/plugin-transform-modules-commonjs@^7.10.4", "@babel/plugin-transform-modules-commonjs@^7.12.13": +"@babel/plugin-transform-modules-commonjs@^7.10.4", "@babel/plugin-transform-modules-commonjs@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.12.13.tgz#5043b870a784a8421fa1fd9136a24f294da13e50" integrity sha512-OGQoeVXVi1259HjuoDnsQMlMkT9UkZT9TpXAsqWplS/M0N1g3TJAn/ByOCeQu7mfjc5WpSsRU+jV1Hd89ts0kQ== From cbcfed55d24c33c63b658cb15971c6c2eefb8d54 Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Sun, 21 Feb 2021 14:25:18 -0500 Subject: [PATCH 19/21] lint fixes --- packages/preact-iso/router.js | 46 ++++++++++++++++------------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/packages/preact-iso/router.js b/packages/preact-iso/router.js index 88dc7da96..a000ddb8d 100644 --- a/packages/preact-iso/router.js +++ b/packages/preact-iso/router.js @@ -19,26 +19,26 @@ const UPDATE = (state, url, push) => { }; export const exec = (url, route, matches) => { - url = url.trim('/').split('/'); - route = (route || '').trim('/').split('/'); - for (let i=0, val; i= '?') continue; - // rest (+/*) match: - matches[param] = url.slice(i).map(decodeURIComponent).join('/'); - break; + url = url.trim('/').split('/'); + route = (route || '').trim('/').split('/'); + for (let i = 0, val; i < Math.max(url.length, route.length); i++) { + let [, m, param, flag] = (route[i] || '').match(/^(:?)(.*?)([+*?]?)$/); + val = url[i]; + // segment match: + if (!m && param == val) continue; + // segment mismatch / missing required field: + if (!m || (!val && flag != '?' && flag != '*')) return; + // field match: + matches[param] = val && decodeURIComponent(val); + // normal/optional field: + if (flag >= '?') continue; + // rest (+/*) match: + matches[param] = url.slice(i).map(decodeURIComponent).join('/'); + break; } - return matches; -} + return matches; +}; export function LocationProvider(props) { const [url, route] = useReducer(UPDATE, location.pathname + location.search); @@ -106,19 +106,15 @@ export function Router(props) { let p, d, m; [].concat(props.children || []).some(vnode => { - const matches = exec(path, vnode.props.path, m = { path, query }); + const matches = exec(path, vnode.props.path, (m = { path, query })); if (matches) { - return p = h( - RouteContext.Provider, - { value: m }, - cloneElement(vnode, m) - ); + return (p = h(RouteContext.Provider, { value: m }, cloneElement(vnode, m))); } if (vnode.props.default) d = cloneElement(vnode, m); }); - return [curChildren.current = p || d, prevChildren.current]; + return [(curChildren.current = p || d), prevChildren.current]; } Router.Provider = LocationProvider; From 7156a4934f45759bbe11ddf969b4deca0e526e51 Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Sun, 21 Feb 2021 14:34:45 -0500 Subject: [PATCH 20/21] TypeScript fixes --- package.json | 1 + packages/preact-iso/index.d.ts | 9 +-------- packages/preact-iso/prerender.d.ts | 7 ++++++- packages/preact-iso/router.d.ts | 10 ++++++++-- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index d8f7be3f7..c6d1afb12 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ }, "eslintIgnore": [ "*.cjs", + "*.ts", "packages/wmr/test/fixtures/**/*.expected.*", "packages/wmr/test/fixtures/*/dist", "packages/wmr/test/fixtures/*/.cache" diff --git a/packages/preact-iso/index.d.ts b/packages/preact-iso/index.d.ts index f0915719f..e44a1801c 100644 --- a/packages/preact-iso/index.d.ts +++ b/packages/preact-iso/index.d.ts @@ -1,11 +1,4 @@ -import { VNode } from 'preact'; -import { PrerenderOptions } from './prerender'; - +export { default as prerender } from './prerender'; export { Router, LocationProvider, useLoc, useLocation } from './router'; export { default as lazy, ErrorBoundary } from './lazy'; export { default as hydrate } from './hydrate'; - -export default function prerender( - vnode: VNode, - options?: PrerenderOptions -): Promise<{ html: string; links: Set }>; diff --git a/packages/preact-iso/prerender.d.ts b/packages/preact-iso/prerender.d.ts index 794b91626..4e9673367 100644 --- a/packages/preact-iso/prerender.d.ts +++ b/packages/preact-iso/prerender.d.ts @@ -5,7 +5,12 @@ export interface PrerenderOptions { props?: Record; } +export interface PrerenderResult { + html: string; + links?: Set +} + export default function prerender( vnode: VNode, options?: PrerenderOptions -): Promise<{ html: string; links: Set }>; +): Promise; diff --git a/packages/preact-iso/router.d.ts b/packages/preact-iso/router.d.ts index 0ee0ffc73..0f771234b 100644 --- a/packages/preact-iso/router.d.ts +++ b/packages/preact-iso/router.d.ts @@ -4,9 +4,15 @@ export const LocationProvider: FunctionComponent; export function Router(props: { onLoadEnd?: () => void; onLoadStart?: () => void; children?: VNode[] }): VNode; -type LocationHook = { url: string; path: string; query: Record; route: (url: string) => void }; -export const useLoc: () => LocationHook; +interface LocationHook { + url: string; + path: string; + query: Record; + route: (url: string) => void; +}; export const useLocation: () => LocationHook; +/** @deprecated renamed to useLocation() */ +export const useLoc: () => LocationHook; interface RoutableProps { path?: string; From ff6c16c6607c5cd8595a4cfc0d7e2fc7b2f9c1a8 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Sun, 21 Feb 2021 20:44:56 +0100 Subject: [PATCH 21/21] add type for useRoute --- packages/preact-iso/router.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/preact-iso/router.d.ts b/packages/preact-iso/router.d.ts index 0f771234b..466d2a22e 100644 --- a/packages/preact-iso/router.d.ts +++ b/packages/preact-iso/router.d.ts @@ -14,6 +14,8 @@ export const useLocation: () => LocationHook; /** @deprecated renamed to useLocation() */ export const useLoc: () => LocationHook; +export const useRoute: () => { [key: string]: string }; + interface RoutableProps { path?: string; default?: boolean;