Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v3 #17

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft

v3 #17

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/hydrate.d.ts
Original file line number Diff line number Diff line change
@@ -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;
6 changes: 3 additions & 3 deletions src/hydrate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
5 changes: 2 additions & 3 deletions src/index.d.ts
Original file line number Diff line number Diff line change
@@ -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';
9 changes: 3 additions & 6 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -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';
2 changes: 1 addition & 1 deletion src/lazy.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ComponentChildren, VNode } from 'preact';

export default function lazy<T>(load: () => Promise<{ default: T } | T>): T;
export function lazy<T>(load: () => Promise<{ default: T } | T>): T;

export function ErrorBoundary(props: { children?: ComponentChildren; onError?: (error: Error) => void }): VNode;
2 changes: 1 addition & 1 deletion src/lazy.js
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/prerender.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface PrerenderResult {
links?: Set<string>
}

export default function prerender(
export function prerender(
vnode: VNode,
options?: PrerenderOptions
): Promise<PrerenderResult>;
Expand Down
4 changes: 2 additions & 2 deletions src/prerender.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -34,7 +34,7 @@ export default async function prerender(vnode, options) {

try {
let html = await renderToStringAsync(vnode);
html += `<script type="isodata"></script>`;
html += `<script id="isodata"></script>`;
return { html, links };
} finally {
vnodeHook = null;
Expand Down
10 changes: 2 additions & 8 deletions src/router.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,12 @@ export function Router(props: {
interface LocationHook {
url: string;
path: string;
query: Record<string, string>;
pathParams: Record<string, string>;
searchParams: Record<string, string>;
route: (url: string, replace?: boolean) => void;
}
export const useLocation: () => LocationHook;

interface RouteHook {
path: string;
query: Record<string, string>;
params: Record<string, string>;
}
export const useRoute: () => RouteHook;

interface RoutableProps {
path?: string;
default?: boolean;
Expand Down
34 changes: 16 additions & 18 deletions src/router.js
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down Expand Up @@ -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('/');
Expand All @@ -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);
Expand All @@ -97,7 +100,6 @@ export function LocationProvider(props) {
};
}, []);

// @ts-ignore
return h(LocationProvider.ctx.Provider, { value }, props.children);
}

Expand All @@ -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);
Expand All @@ -129,7 +130,7 @@ export function Router(props) {

let pathRoute, defaultRoute, matchProps;
toChildArray(props.children).some((/** @type {VNode<any>} */ 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);
});
Expand All @@ -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();
Expand All @@ -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<any>} */ (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;
Expand Down Expand Up @@ -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);
4 changes: 2 additions & 2 deletions test/node/prerender.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -23,7 +23,7 @@ test('extracts links', async () => {
test('appends iso data script', async () => {
const { html: h } = await prerender(html`<div />`);
// Empty for now, but used for hydration vs render detection
assert.match(h, /<script type="isodata"><\/script>/, 'missing iso data script tag');
assert.match(h, /<script id="isodata"><\/script>/, 'missing iso data script tag');
});

test.run();
Loading
Loading