Skip to content

Commit

Permalink
refactor: new router (#55)
Browse files Browse the repository at this point in the history
This implements a new, more robust router.
  • Loading branch information
lucacasonato authored Dec 17, 2020
1 parent 07224b0 commit cd4ba8a
Show file tree
Hide file tree
Showing 20 changed files with 174 additions and 145 deletions.
10 changes: 7 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ jobs:

strategy:
matrix:
deno: ["v1.x", "nightly"]
os: [macOS-latest, ubuntu-latest] # disable on windows-latest because fly tls aint working there
deno: ["v1.x", "nightly"]
os: [macOS-latest, ubuntu-latest, windows-latest]

steps:
- name: Disable autocrlf
Expand All @@ -31,8 +31,12 @@ jobs:
- name: Check formatting
run: deno fmt --check

- name: Check lint
run: deno lint --unstable

- name: Install dependencies
run: deno cache --no-check ./deps/mod.ts
run: |
deno cache --no-check ./deps/mod.ts ./deps/preact/mod.ts ./deps/preact/hooks.ts ./deps/preact/debug.ts ./deps/preact/ssr.ts ./example/deps.ts
- name: Run Tests
run: deno test -A --unstable
14 changes: 13 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
{
"deno.enable": true,
"deno.lint": true,
"deno.unstable": true
"deno.unstable": true,
"[javascript]": {
"editor.defaultFormatter": "denoland.vscode-deno"
},
"[javascriptreact]": {
"editor.defaultFormatter": "denoland.vscode-deno"
},
"[typescript]": {
"editor.defaultFormatter": "denoland.vscode-deno"
},
"[typescriptreact]": {
"editor.defaultFormatter": "denoland.vscode-deno"
}
}
2 changes: 0 additions & 2 deletions cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,8 +345,6 @@ async function create(_options: unknown, maybeRoot?: string) {
jsx: "react",
jsxFactory: "h",
jsxFragmentFactory: "Fragment",
importsNotUsedAsValues: "error",
isolatedModules: true,
},
}),
);
Expand Down
4 changes: 3 additions & 1 deletion example/deps.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export { Fragment, h } from "https://deno.land/x/[email protected]/deps/preact/mod.ts";
export { Fragment, h } from "../deps/preact/mod.ts";
export type { JSX } from "../deps/preact/mod.ts";
export { useLocation } from "../mod.ts";
export type {
AppProps,
DocumentProps,
Expand Down
4 changes: 3 additions & 1 deletion example/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Fragment, h } from "../deps.ts";
import { Fragment, h, useLocation } from "../deps.ts";
import type { GetStaticData, PageProps } from "../deps.ts";

interface Data {
random: string;
}

function IndexPage(props: PageProps<Data>) {
const [path] = useLocation();

return (
<>
<h1>Hello World!!!</h1>
Expand Down
4 changes: 1 addition & 3 deletions example/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
"lib": ["esnext", "dom", "deno.ns"],
"jsx": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment",
"importsNotUsedAsValues": "error",
"isolatedModules": true
"jsxFragmentFactory": "Fragment"
}
}
3 changes: 0 additions & 3 deletions src/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,6 @@ export async function bundle(
output: outputOptions,
preserveEntrySignatures: false,
cache: options.cache,
treeshake: {
moduleSideEffects: false,
},
};

const outDir = outputOptions.dir!;
Expand Down
1 change: 0 additions & 1 deletion src/runtime/default_app.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { h } from "../../deps/preact/mod.ts";
import type { AppProps } from "./type.ts";

function App(props: AppProps) {
Expand Down
17 changes: 17 additions & 0 deletions src/runtime/memo.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// deno-lint-ignore-file
import {
ComponentProps,
FunctionalComponent,
FunctionComponent,
} from "../../deps/preact/mod.ts";
export function memo<P = {}>(
component: FunctionalComponent<P>,
comparer?: (prev: P, next: P) => boolean,
): FunctionComponent<P>;
export function memo<C extends FunctionalComponent<any>>(
component: C,
comparer?: (
prev: ComponentProps<C>,
next: ComponentProps<C>,
) => boolean,
): C;
34 changes: 34 additions & 0 deletions src/runtime/memo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/// <reference types="./memo.d.ts" />

import { createElement } from "../../deps/preact/mod.ts";

export function shallowDiffers(a, b) {
for (const i in a) if (i !== "__source" && !(i in b)) return true;
for (const i in b) if (i !== "__source" && a[i] !== b[i]) return true;
return false;
}

export function memo(c, comparer) {
function shouldUpdate(nextProps) {
const ref = this.props.ref;
const updateRef = ref == nextProps.ref;
if (!updateRef && ref) {
ref.call ? ref(null) : (ref.current = null);
}

if (!comparer) {
return shallowDiffers(this.props, nextProps);
}

return !comparer(this.props, nextProps) || !updateRef;
}

function Memoed(props) {
this.shouldComponentUpdate = shouldUpdate;
return createElement(c, props);
}
Memoed.displayName = "Memo(" + (c.displayName || c.name) + ")";
Memoed.prototype.isReactComponent = true;
Memoed._forwarded = true;
return Memoed;
}
97 changes: 69 additions & 28 deletions src/runtime/mod.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,34 @@
import { h, hydrate } from "../../deps/preact/mod.ts";
import type { ComponentType } from "../../deps/preact/mod.ts";
import { useEffect, useMemo, useState } from "../../deps/preact/hooks.ts";
import {
useCallback,
useEffect,
useMemo,
useState,
} from "../../deps/preact/hooks.ts";
import type { AppProps, PageProps } from "./type.ts";
import { Router } from "./router/router.ts";
import { useLocation } from "./router/location.ts";
import { initRouter } from "./router/interceptor.ts";
import { locationCtx } from "./router/location.ts";
import { memo } from "./memo.js";

type Route = [route: string, data: RouteData];

type RouteData = [
component: () => Promise<{ default: ComponentType<PageProps> }>,
hasStaticData: boolean,
];

export async function start(routes: Route[], app: ComponentType<AppProps>) {
const router = new Router<RouteData>(routes);
const path = location.pathname;
const path = window.location.pathname;
const [route] = router.getRoute(path);
if (!route) throw new Error("Failed to match inital route.");

const initialPage = await loadComponent(route[1][0](), route[1][1], path);

// sets up event listeners on <a> elements
initRouter(router);

hydrate(
<Dext router={router} app={app} initialPage={initialPage} />,
document.getElementById("__dext")!,
window.document.getElementById("__dext")!,
);
}

Expand All @@ -39,36 +41,75 @@ function Dext(props: {
app: ComponentType<AppProps>;
initialPage: PageComponent;
}) {
const [path] = useLocation();
const [route, match] = useMemo(() => props.router.getRoute(path), [
props.router,
path,
]);
const [desiredPath, setDesiredPath] = useState(window.location.pathname);
const [desiredRoute, desiredMatch] = useMemo(
() => props.router.getRoute(desiredPath),
[props.router, desiredPath],
);

const [[Page], setPage] = useState<[PageComponent | null]>([
props.initialPage,
]);
const navigate = useCallback(
(to: string) => {
window.history.pushState(null, "", to);
setDesiredPath(to);
},
[setDesiredPath],
);

useEffect(() => {
// sets up event listeners on <a> elements
initRouter(props.router, navigate);

window.addEventListener("popstate", (event) => {
setDesiredPath(window.location.pathname);
});
}, [props.router, navigate]);

const [page, setPage] = useState<
[PageComponent | null, string, Record<string, string | string[]>]
>([props.initialPage, desiredPath, desiredMatch]);

useEffect(() => {
let cancelled = false;
if (route) {
loadComponent(route[1][0](), route[1][1], path).then((page) => {
if (!cancelled) setPage([page]);
});
if (desiredRoute) {
loadComponent(desiredRoute[1][0](), desiredRoute[1][1], desiredPath)
.then((page) => {
if (!cancelled) {
setPage([page, desiredPath, desiredMatch]);
}
})
.catch((err) => {
if (!cancelled) {
console.error(err);
window.location.pathname = desiredPath;
}
});
} else {
setPage([null]);
setPage([null, desiredPath, desiredMatch]);
}
() => (cancelled = true);
}, [route]);
}, [desiredRoute, desiredPath, desiredMatch]);

const App = props.app;
return (
<div>
<App>{Page === null ? <Error404 /> : <Page route={match!} />}</App>
</div>
);
return <DextPage App={props.app} page={page} navigate={navigate} />;
}

const DextPage = memo(
(props: {
App: ComponentType<AppProps>;
page: [PageComponent | null, string, Record<string, string | string[]>];
navigate: (to: string) => void;
}) => {
const { App, page, navigate } = props;
const [Page, path, match] = page;
return (
<locationCtx.Provider value={[path, navigate]}>
<div>
<App>{Page === null ? <Error404 /> : <Page route={match!} />}</App>
</div>
</locationCtx.Provider>
);
},
);

async function loadComponent(
componentPromise: Promise<{ default: ComponentType<PageProps> }>,
hasStaticData: boolean,
Expand Down
2 changes: 2 additions & 0 deletions src/runtime/prerender_page_host.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const { data, route, path } = rawData.length == 0
? undefined
: JSON.parse(new TextDecoder().decode(rawData));
window.location = { pathname: path } as Location;
// @ts-expect-error because this is a hidden variable.
window.__DEXT_SSR = true;
const body = render(
<div>
<App>
Expand Down
10 changes: 6 additions & 4 deletions src/runtime/router/interceptor.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// deno-lint-ignore-file no-undef
import type { Router } from "./router.ts";

export function initRouter(router: Router<unknown>) {
export function initRouter(
router: Router<unknown>,
navigate: (to: string) => void,
) {
function routeFromLink(node: HTMLAnchorElement) {
// only valid elements
if (!node || !node.getAttribute) return;

let href = node.getAttribute("href"),
const href = node.getAttribute("href"),
target = node.getAttribute("target");

// ignore links with targets and non-path URLs
Expand All @@ -20,7 +22,7 @@ export function initRouter(router: Router<unknown>) {

const [route] = router.getRoute(href);
if (route) {
history.pushState(null, "", href);
navigate(href);
return true;
}

Expand Down
Loading

0 comments on commit cd4ba8a

Please sign in to comment.