diff --git a/README.md b/README.md index 719df12..49756ba 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ const App = () => ( Primarily meant for use with prerendering via [`@preact/preset-vite`](https://github.com/preactjs/preset-vite#prerendering-configuration) or other prerendering systems that share the API. If you're server-side rendering your app via any other method, you can use `preact-render-to-string` (specifically `renderToStringAsync()`) directly. ```js +import { render, hydrate } from 'preact'; import { LocationProvider, ErrorBoundary, Router, lazy, prerender as ssr } from 'preact-iso'; // Asynchronous (throws a promise) @@ -67,7 +68,10 @@ const App = () => ( ); -hydrate(); +if (typeof window !== 'undefined') { + const target = document.getElementById('app'); + import.meta.env.DEV ? render(, target) : hydrate(, target); +} export async function prerender(data) { return await ssr(); @@ -270,31 +274,6 @@ const App = () => ( ); ``` -### `hydrate` - -A thin wrapper around Preact's `hydrate` export, it switches between hydrating and rendering the provided element, depending on whether the current page has been prerendered. Additionally, it checks to ensure it's running in a browser context before attempting any rendering, making it a no-op during SSR. - -Pairs with the `prerender()` function. - -Params: - -- `jsx: ComponentChild` - The JSX element or component to render -- `parent?: Element | Document | ShadowRoot | DocumentFragment` - The parent element to render into. Defaults to `document.body` if not provided. - -```js -import { hydrate } from 'preact-iso'; - -const App = () => ( -
-

Hello World

-
-); - -hydrate(); -``` - -However, it is just a simple utility method. By no means is it essential to use, you can always use Preact's `hydrate` export directly. - ### `prerender` Renders a Virtual DOM tree to an HTML string using `preact-render-to-string`. The Promise returned from `prerender()` resolves to an Object with `html` and `links[]` properties. The `html` property contains your pre-rendered static HTML markup, and `links` is an Array of any non-external URL strings found in links on the generated page. diff --git a/package.json b/package.json index 30a1202..509a3c0 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 deleted file mode 100644 index 227b5ec..0000000 --- a/src/hydrate.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { ComponentChild, ContainerNode } from 'preact'; - -export default function hydrate(jsx: ComponentChild, parent?: ContainerNode): void; diff --git a/src/hydrate.js b/src/hydrate.js deleted file mode 100644 index 269bc58..0000000 --- a/src/hydrate.js +++ /dev/null @@ -1,17 +0,0 @@ -import { render, hydrate as hydrativeRender } from 'preact'; - -let initialized; - -/** @type {typeof hydrativeRender} */ -export default function hydrate(jsx, parent) { - if (typeof window === 'undefined') return; - let isodata = document.querySelector('script[type=isodata]'); - // @ts-ignore-next - parent = parent || (isodata && isodata.parentNode) || document.body; - if (!initialized && isodata) { - hydrativeRender(jsx, parent); - } else { - render(jsx, parent); - } - initialized = true; -} diff --git a/src/index.d.ts b/src/index.d.ts index 70ba4a2..2cb7567 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,4 +1,2 @@ -export { default as prerender } from './prerender.js'; +export * from './lazy.js'; export * from './router.js'; -export { default as lazy, ErrorBoundary } from './lazy.js'; -export { default as hydrate } from './hydrate.js'; diff --git a/src/index.js b/src/index.js index 3bfc751..2cb7567 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,2 @@ -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 './router.js'; diff --git a/src/lazy.d.ts b/src/lazy.d.ts index 24e3b1b..7841eef 100644 --- a/src/lazy.d.ts +++ b/src/lazy.d.ts @@ -1,6 +1,6 @@ import { ComponentChildren, VNode } from 'preact'; -export default function lazy(load: () => Promise<{ default: T } | T>): T & { +export function lazy(load: () => Promise<{ default: T } | T>): T & { preload: () => Promise; }; diff --git a/src/lazy.js b/src/lazy.js index 9fb7fe3..a0feaa2 100644 --- a/src/lazy.js +++ b/src/lazy.js @@ -10,7 +10,7 @@ options.__b = (vnode) => { if (oldDiff) oldDiff(vnode); }; -export default function lazy(load) { +export function lazy(load) { let p, c; const loadModule = () => 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 879576f..9ef2211 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; @@ -25,7 +25,7 @@ export default async function prerender(vnode, options) { vnode = cloneElement(vnode, props); } - let links = new Set(); + const links = new Set(); vnodeHook = ({ type, props }) => { if (type === 'a' && props && props.href && (!props.target || props.target === '_self')) { links.add(props.href); @@ -33,8 +33,7 @@ export default async function prerender(vnode, options) { }; try { - let html = await renderToStringAsync(vnode); - html += ``; + const html = await renderToStringAsync(vnode); return { html, links }; } finally { vnodeHook = null; diff --git a/src/router.d.ts b/src/router.d.ts index f58d550..d142a08 100644 --- a/src/router.d.ts +++ b/src/router.d.ts @@ -9,7 +9,7 @@ type NestedArray = Array>; /** * Check if a URL path matches against a URL path pattern. - * + * * Warning: This is an internal API exported only for testing purpose. API could change in future. * @param url - URL path (e.g. /user/12345) * @param route - URL pattern (e.g. /user/:id) @@ -38,18 +38,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; - type RoutableProps = | { path: string; default?: false; } | { path?: never; default: true; } diff --git a/src/router.js b/src/router.js index ac5d5cc..90290cc 100644 --- a/src/router.js +++ b/src/router.js @@ -51,12 +51,12 @@ const UPDATE = (state, url) => { export const exec = (url, route, matches = {}) => { url = url.split('/').filter(Boolean); route = (route || '').split('/').filter(Boolean); - if (!matches.params) matches.params = {}; + if (!matches.pathParams) matches.pathParams = {}; 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('/'); @@ -69,8 +69,8 @@ export const exec = (url, route, matches = {}) => { if (rest) val = url.slice(i).map(decodeURIComponent).join('/') || undefined; // 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; @@ -80,19 +80,19 @@ export const exec = (url, route, matches = {}) => { * @type {import('./router.d.ts').LocationProvider} */ export function LocationProvider(props) { - // @ts-expect-error - props.url is not implemented correctly & will be removed in the future - const [url, route] = useReducer(UPDATE, props.url || location.pathname + location.search); + const [url, route] = useReducer(UPDATE, location.pathname + location.search); if (props.scope) scope = props.scope; const wasPush = push === true; 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 }; @@ -108,7 +108,6 @@ export function LocationProvider(props) { }; }, []); - // @ts-ignore return h(LocationProvider.ctx.Provider, { value }, props.children); } @@ -117,8 +116,8 @@ 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 { rest = path } = useContext(RouterContext); const isLoading = useRef(false); const prevRoute = useRef(path); @@ -138,7 +137,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(rest, vnode.props.path, (matchProps = { ...vnode.props, path: rest, pathParams, searchParams })); if (matches) return (pathRoute = cloneElement(vnode, matchProps)); if (vnode.props.default) defaultRoute = cloneElement(vnode, matchProps); }); @@ -151,7 +150,7 @@ export function Router(props) { const routeChanged = useMemo(() => { prev.current = cur.current; - cur.current = /** @type {VNode} */ (h(RouteContext.Provider, { value: matchProps }, incoming)); + cur.current = /** @type {VNode} */ (h(RouterContext.Provider, { value: matchProps }, incoming)); // Only mark as an update if the route component changed. const outgoing = prev.current && prev.current.props.children; @@ -265,11 +264,10 @@ 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 }} */ ({}) +const RouterContext = createContext( + /** @type {{ 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/lazy.test.js b/test/lazy.test.js index 1d29f57..6af6f80 100644 --- a/test/lazy.test.js +++ b/test/lazy.test.js @@ -4,7 +4,7 @@ import * as sinon from 'sinon'; import sinonChai from 'sinon-chai'; import { LocationProvider, Router } from '../src/router.js'; -import lazy, { ErrorBoundary } from '../src/lazy.js'; +import { lazy, ErrorBoundary } from '../src/lazy.js'; import './setup.js'; diff --git a/test/node/prerender.test.js b/test/node/prerender.test.js index 434b151..ba771e7 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` @@ -20,10 +20,4 @@ test('extracts links', async () => { assert.ok(links.has('/baz'), `missing: /baz`); }); -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, /