diff --git a/package.json b/package.json index 1d66b3c..5eb8b07 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,8 @@ "types": "src/index.d.ts", "exports": { ".": "./src/index.js", - "./router": "./src/router.js", - "./lazy": "./src/lazy.js", "./prerender": "./src/prerender.js", - "./hydrate": "./src/hydrate.js" + "./package.json": "./package.json" }, "license": "MIT", "description": "Isomorphic utilities for Preact", @@ -35,6 +33,11 @@ "preact": ">=10", "preact-render-to-string": ">=6.4.0" }, + "peerDependenciesMeta": { + "preact-render-to-string": { + "optional": true + } + }, "devDependencies": { "@types/mocha": "^10.0.7", "@types/sinon-chai": "^3.2.12", diff --git a/src/hydrate.d.ts b/src/hydrate.d.ts index ace9158..f56e39d 100644 --- a/src/hydrate.d.ts +++ b/src/hydrate.d.ts @@ -1,3 +1,3 @@ import { ComponentChild } from 'preact'; -export default function hydrate(jsx: ComponentChild, parent?: Element | Document | ShadowRoot | DocumentFragment): void; +export function hydrate(jsx: ComponentChild, parent?: Element | Document | ShadowRoot | DocumentFragment): void; diff --git a/src/hydrate.js b/src/hydrate.js index 68d9375..796ed5c 100644 --- a/src/hydrate.js +++ b/src/hydrate.js @@ -2,10 +2,10 @@ import { render, hydrate as hydrativeRender } from 'preact'; let initialized; -/** @type {typeof render} */ -export default function hydrate(jsx, parent) { +/** @type {typeof hydrativeRender} */ +export function hydrate(jsx, parent) { if (typeof window === 'undefined') return; - let isodata = document.querySelector('script[type=isodata]'); + let isodata = document.getElementById('isodata'); // @ts-ignore-next parent = parent || (isodata && isodata.parentNode) || document.body; if (!initialized && isodata) { diff --git a/src/index.d.ts b/src/index.d.ts index 70ba4a2..1d8e633 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,4 +1,3 @@ -export { default as prerender } from './prerender.js'; export * from './router.js'; -export { default as lazy, ErrorBoundary } from './lazy.js'; -export { default as hydrate } from './hydrate.js'; +export * from './lazy.js'; +export * from './hydrate.js'; diff --git a/src/index.js b/src/index.js index 3bfc751..44fe534 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,4 @@ +// lack of wildcard export is intentional to avoid exposing `exec` export { Router, LocationProvider, useLocation, Route, useRoute } from './router.js'; -export { default as lazy, ErrorBoundary } from './lazy.js'; -export { default as hydrate } from './hydrate.js'; - -export function prerender(vnode, options) { - return import('./prerender.js').then(m => m.default(vnode, options)); -} +export * from './lazy.js'; +export * from './hydrate.js'; diff --git a/src/lazy.d.ts b/src/lazy.d.ts index 829cdf3..8805198 100644 --- a/src/lazy.d.ts +++ b/src/lazy.d.ts @@ -1,5 +1,5 @@ import { ComponentChildren, VNode } from 'preact'; -export default function lazy(load: () => Promise<{ default: T } | T>): T; +export function lazy(load: () => Promise<{ default: T } | T>): T; export function ErrorBoundary(props: { children?: ComponentChildren; onError?: (error: Error) => void }): VNode; diff --git a/src/lazy.js b/src/lazy.js index 9670e8d..17ce2c8 100644 --- a/src/lazy.js +++ b/src/lazy.js @@ -1,7 +1,7 @@ import { h, options } from 'preact'; import { useState, useRef } from 'preact/hooks'; -export default function lazy(load) { +export function lazy(load) { let p, c; return props => { const [, update] = useState(0); diff --git a/src/prerender.d.ts b/src/prerender.d.ts index b9958bc..104ba3d 100644 --- a/src/prerender.d.ts +++ b/src/prerender.d.ts @@ -9,7 +9,7 @@ export interface PrerenderResult { links?: Set } -export default function prerender( +export function prerender( vnode: VNode, options?: PrerenderOptions ): Promise; diff --git a/src/prerender.js b/src/prerender.js index be435bf..3c85cab 100644 --- a/src/prerender.js +++ b/src/prerender.js @@ -14,7 +14,7 @@ options.vnode = vnode => { * @param {object} [options] * @param {object} [options.props] Additional props to merge into the root JSX element */ -export default async function prerender(vnode, options) { +export async function prerender(vnode, options) { options = options || {}; const props = options.props; @@ -34,7 +34,7 @@ export default async function prerender(vnode, options) { try { let html = await renderToStringAsync(vnode); - html += ``; + html += ``; return { html, links }; } finally { vnodeHook = null; diff --git a/src/router.d.ts b/src/router.d.ts index f658874..57cd3f5 100644 --- a/src/router.d.ts +++ b/src/router.d.ts @@ -14,18 +14,12 @@ export function Router(props: { interface LocationHook { url: string; path: string; - query: Record; + pathParams: Record; + searchParams: Record; route: (url: string, replace?: boolean) => void; } export const useLocation: () => LocationHook; -interface RouteHook { - path: string; - query: Record; - params: Record; -} -export const useRoute: () => RouteHook; - interface RoutableProps { path?: string; default?: boolean; diff --git a/src/router.js b/src/router.js index 354f451..8c1d30f 100644 --- a/src/router.js +++ b/src/router.js @@ -1,4 +1,4 @@ -import { h, createContext, cloneElement, toChildArray } from 'preact'; +import { h, Fragment, createContext, cloneElement, toChildArray } from 'preact'; import { useContext, useMemo, useReducer, useLayoutEffect, useRef } from 'preact/hooks'; /** @@ -47,10 +47,10 @@ export const exec = (url, route, matches) => { url = url.split('/').filter(Boolean); route = (route || '').split('/').filter(Boolean); for (let i = 0, val, rest; i < Math.max(url.length, route.length); i++) { - let [, m, param, flag] = (route[i] || '').match(/^(:?)(.*?)([+*?]?)$/); + let [, m, pathParam, flag] = (route[i] || '').match(/^(:?)(.*?)([+*?]?)$/); val = url[i]; // segment match: - if (!m && param == val) continue; + if (!m && pathParam == val) continue; // /foo/* match if (!m && val && flag == '*') { matches.rest = '/' + url.slice(i).map(decodeURIComponent).join('/'); @@ -63,30 +63,33 @@ export const exec = (url, route, matches) => { if (rest) val = url.slice(i).map(decodeURIComponent).join('/'); // normal/optional field: else if (val) val = decodeURIComponent(val); - matches.params[param] = val; - if (!(param in matches)) matches[param] = val; + matches.pathParams[pathParam] = val; + if (!(pathParam in matches)) matches[pathParam] = val; if (rest) break; } return matches; }; export function LocationProvider(props) { - const [url, route] = useReducer(UPDATE, props.url || location.pathname + location.search); + const [url, route] = useReducer(UPDATE, location.pathname + location.search); const wasPush = push === true; + /** @type {import('./router.d.ts').LocationHook} */ const value = useMemo(() => { const u = new URL(url, location.origin); const path = u.pathname.replace(/\/+$/g, '') || '/'; - // @ts-ignore-next + return { url, path, - query: Object.fromEntries(u.searchParams), + pathParams: {}, + searchParams: Object.fromEntries(u.searchParams), route: (url, replace) => route({ url, replace }), wasPush }; }, [url]); + useLayoutEffect(() => { addEventListener('click', route); addEventListener('popstate', route); @@ -97,7 +100,6 @@ export function LocationProvider(props) { }; }, []); - // @ts-ignore return h(LocationProvider.ctx.Provider, { value }, props.children); } @@ -106,8 +108,7 @@ const RESOLVED = Promise.resolve(); export function Router(props) { const [c, update] = useReducer(c => c + 1, 0); - const { url, query, wasPush, path } = useLocation(); - const { rest = path, params = {} } = useContext(RouteContext); + const { url, path, pathParams, searchParams, wasPush } = useLocation(); const isLoading = useRef(false); const prevRoute = useRef(path); @@ -129,7 +130,7 @@ export function Router(props) { let pathRoute, defaultRoute, matchProps; toChildArray(props.children).some((/** @type {VNode} */ vnode) => { - const matches = exec(rest, vnode.props.path, (matchProps = { ...vnode.props, path: rest, query, params, rest: '' })); + const matches = exec(path, vnode.props.path, (matchProps = { ...vnode.props, path, pathParams, searchParams, rest: '' })); if (matches) return (pathRoute = cloneElement(vnode, matchProps)); if (vnode.props.default) defaultRoute = cloneElement(vnode, matchProps); }); @@ -140,7 +141,7 @@ export function Router(props) { prev.current = cur.current; // Only mark as an update if the route component changed. - const outgoing = prev.current && prev.current.props.children; + const outgoing = prev.current; if (!outgoing || !incoming || incoming.type !== outgoing.type || incoming.props.component !== outgoing.props.component) { // This hack prevents Preact from diffing when we swap `cur` to `prev`: if (this.__v && this.__v.__k) this.__v.__k.reverse(); @@ -152,7 +153,8 @@ export function Router(props) { const isHydratingSuspense = cur.current && cur.current.__u & MODE_HYDRATE && cur.current.__u & MODE_SUSPENDED; const isHydratingBool = cur.current && cur.current.__h; // @ts-ignore - cur.current = /** @type {VNode} */ (h(RouteContext.Provider, { value: matchProps }, incoming)); + // TODO: Figure out how to set `.__h` properly so that it's preserved for the next render. + cur.current = h(Fragment, {}, incoming); if (isHydratingSuspense) { cur.current.__u |= MODE_HYDRATE; cur.current.__u |= MODE_SUSPENDED; @@ -254,11 +256,7 @@ Router.Provider = LocationProvider; LocationProvider.ctx = createContext( /** @type {import('./router.d.ts').LocationHook & { wasPush: boolean }} */ ({}) ); -const RouteContext = createContext( - /** @type {import('./router.d.ts').RouteHook & { rest: string }} */ ({}) -); export const Route = props => h(props.component, props); export const useLocation = () => useContext(LocationProvider.ctx); -export const useRoute = () => useContext(RouteContext); diff --git a/test/node/prerender.test.js b/test/node/prerender.test.js index 434b151..163e74c 100644 --- a/test/node/prerender.test.js +++ b/test/node/prerender.test.js @@ -2,7 +2,7 @@ import { test } from 'uvu'; import * as assert from 'uvu/assert'; import { html } from 'htm/preact'; -import { default as prerender } from '../../src/prerender.js'; +import { prerender } from '../../src/prerender.js'; test('extracts links', async () => { const App = () => html` @@ -23,7 +23,7 @@ test('extracts links', async () => { test('appends iso data script', async () => { const { html: h } = await prerender(html`
`); // Empty for now, but used for hydration vs render detection - assert.match(h, /