This commit is contained in:
Richard Harris
2019-02-02 12:53:36 -05:00
parent 81bbfce448
commit 85c86b5562
6 changed files with 80 additions and 76 deletions

View File

@@ -9,14 +9,17 @@ import {
Redirect,
ComponentLoader,
ComponentConstructor,
Page,
PageData
Route,
Page
} from './types';
import goto from './goto';
// injected at build time
declare const __IGNORE__, __COMPONENTS__, __PAGES__, __SAPPER__;
const ignore = __IGNORE__;
export const components: ComponentLoader[] = __COMPONENTS__;
export const pages: Page[] = __PAGES__;
export const routes: Route[] = __PAGES__;
let ready = false;
let root_component: Component;
@@ -64,19 +67,19 @@ export { _history as history };
export const scroll_history: Record<string, ScrollPosition> = {};
export function select_route(url: URL): Target {
export function select_target(url: URL): Target {
if (url.origin !== location.origin) return null;
if (!url.pathname.startsWith(initial_data.baseUrl)) return null;
const path = url.pathname.slice(initial_data.baseUrl.length);
// avoid accidental clashes between server routes and pages
// avoid accidental clashes between server routes and page routes
if (ignore.some(pattern => pattern.test(path))) return;
for (let i = 0; i < pages.length; i += 1) {
const page = pages[i];
for (let i = 0; i < routes.length; i += 1) {
const route = routes[i];
const match = page.pattern.exec(path);
const match = route.pattern.exec(path);
if (match) {
const query: Record<string, string | string[]> = Object.create(null);
if (url.search.length > 0) {
@@ -84,11 +87,22 @@ export function select_route(url: URL): Target {
let [, key, value] = /([^=]*)(?:=(.*))?/.exec(decodeURIComponent(searchParam));
value = (value || '').replace(/\+/g, ' ');
if (typeof query[key] === 'string') query[key] = [<string>query[key]];
if (typeof query[key] === 'object') query[key].push(value);
if (typeof query[key] === 'object') (query[key] as string[]).push(value);
else query[key] = value;
});
}
return { url, path, page, match, query };
const part = route.parts[route.parts.length - 1];
const params = part.params ? part.params(match) : {};
return {
href: url.href,
path,
route,
match,
query,
params
};
}
}
}
@@ -122,23 +136,23 @@ export async function navigate(target: Target, id: number, noscroll?: boolean, h
// TODO path, params, query
});
}
const loaded = prefetching && prefetching.href === target.url.href ?
const loaded = prefetching && prefetching.href === target.href ?
prefetching.promise :
prepare_page(target);
hydrate_target(target);
prefetching = null;
const token = current_token = {};
const { redirect, page, data, branch } = await loaded;
const { redirect, page, props, branch } = await loaded;
if (redirect) return goto(redirect.location, { replaceState: true });
await render(branch, data, page, scroll_history[id], noscroll, hash, token);
await render(branch, props, page, scroll_history[id], noscroll, hash, token);
if (document.activeElement) document.activeElement.blur();
}
async function render(branch: any[], props: any, page: PageData, scroll: ScrollPosition, noscroll: boolean, hash: string, token: {}) {
async function render(branch: any[], props: any, page: Page, scroll: ScrollPosition, noscroll: boolean, hash: string, token: {}) {
if (current_token !== token) return;
stores.page.set(page);
@@ -191,17 +205,31 @@ async function render(branch: any[], props: any, page: PageData, scroll: ScrollP
ready = true;
}
export async function prepare_page(target: Target): Promise<{
export async function hydrate_target(target: Target): Promise<{
redirect?: Redirect;
data?: any;
page: PageData
props?: any;
page?: Page;
branch?: Array<{ Component: ComponentConstructor, preload: (page) => Promise<any>, segment: string }>
}> {
const { page, path, query } = target;
const { route, path, query, params } = target;
const segments = path.split('/').filter(Boolean);
let redirect: Redirect = null;
let error: { statusCode: number, message: Error | string } = null;
const preload_context = {
fetch: (url: string, opts?: any) => fetch(url, opts),
redirect: (statusCode: number, location: string) => {
if (redirect && (redirect.statusCode !== statusCode || redirect.location !== location)) {
throw new Error(`Conflicting redirects`);
}
redirect = { statusCode, location };
},
error: (statusCode: number, message: Error | string) => {
error = { statusCode, message };
}
};
if (!root_preload) {
const preload_fn = RootStatic['pre' + 'load']; // Rollup makes us jump through these hoops :(
root_preload = preload_fn
@@ -216,20 +244,7 @@ export async function prepare_page(target: Target): Promise<{
let branch;
try {
const preload_context = {
fetch: (url: string, opts?: any) => fetch(url, opts),
redirect: (statusCode: number, location: string) => {
if (redirect && (redirect.statusCode !== statusCode || redirect.location !== location)) {
throw new Error(`Conflicting redirects`);
}
redirect = { statusCode, location };
},
error: (statusCode: number, message: Error | string) => {
error = { statusCode, message };
}
};
branch = await Promise.all(page.parts.map(async (part, i) => {
branch = await Promise.all(route.parts.map(async (part, i) => {
if (!part) return null;
const segment = segments[i];
@@ -259,22 +274,18 @@ export async function prepare_page(target: Target): Promise<{
if (!root_data) root_data = await root_preload;
if (redirect) {
return { redirect, page: null };
}
const deepest = page.parts[page.parts.length - 1];
if (redirect) return { redirect };
const page_data = {
path,
query,
params: deepest.params ? deepest.params(target.match) : {}
params
};
if (error) {
return {
page: page_data,
data: {
props: {
child: {
component: ErrorComponent,
props: {
@@ -287,28 +298,20 @@ export async function prepare_page(target: Target): Promise<{
};
}
const props = {
child: {
segment: segments[0]
}
};
const props = { child: {} };
let level = props.child;
for (let i = 0; i < page.parts.length; i += 1) {
const part = page.parts[i];
if (!part) continue;
branch.forEach(node => {
if (!node) return;
level.component = branch[i].Component;
level.props = Object.assign({}, branch[i].preloaded, {
child: {}
});
level.segment = node.segment;
level.component = node.Component;
level.props = Object.assign({}, node.preloaded, { child: {} });
level = level.props.child;
level.segment = segments[i + 1];
}
});
return { data: props, page: page_data, branch };
return { props, page: page_data, branch };
}
function load_css(chunk: string) {

View File

@@ -1,7 +1,7 @@
import { history, select_route, navigate, cid } from '../app';
import { history, select_target, navigate, cid } from '../app';
export default function goto(href: string, opts = { replaceState: false }) {
const target = select_route(new URL(href, document.baseURI));
const target = select_target(new URL(href, document.baseURI));
if (target) {
history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);

View File

@@ -1,12 +1,12 @@
import { select_route, prefetching, set_prefetching, prepare_page } from '../app';
import { select_target, prefetching, set_prefetching, hydrate_target } from '../app';
import { Target } from '../types';
export default function prefetch(href: string) {
const target: Target = select_route(new URL(href, document.baseURI));
const target: Target = select_target(new URL(href, document.baseURI));
if (target) {
if (!prefetching || href !== prefetching.href) {
set_prefetching(href, prepare_page(target));
set_prefetching(href, hydrate_target(target));
}
return prefetching.promise;

View File

@@ -1,11 +1,11 @@
import { components, pages, load_component } from "../app";
import { components, routes, load_component } from "../app";
export default function prefetchRoutes(pathnames: string[]) {
return pages
.filter(route => {
if (!pathnames) return true;
return pathnames.some(pathname => route.pattern.test(pathname));
})
return routes
.filter(pathnames
? route => pathnames.some(pathname => route.pattern.test(pathname))
: () => true
)
.reduce((promise: Promise<any>, route) => promise.then(() => {
return Promise.all(route.parts.map(part => part && load_component(components[part.i])));
}), Promise.resolve());

View File

@@ -5,7 +5,7 @@ import {
navigate,
scroll_history,
scroll_state,
select_route,
select_target,
set_target,
uid,
set_uid,
@@ -35,7 +35,7 @@ export default function start(opts: {
history.replaceState({ id: uid }, '', href);
if (!initial_data.error) {
const target = select_route(new URL(location.href));
const target = select_target(new URL(location.href));
if (target) return navigate(target, uid, false, hash);
}
});
@@ -92,7 +92,7 @@ function handle_click(event: MouseEvent) {
// Don't handle hash changes
if (url.pathname === location.pathname && url.search === location.search) return;
const target = select_route(url);
const target = select_target(url);
if (target) {
const noscroll = a.hasAttribute('sapper-noscroll');
navigate(target, null, noscroll, url.hash);
@@ -115,7 +115,7 @@ function handle_popstate(event: PopStateEvent) {
if (event.state) {
const url = new URL(location.href);
const target = select_route(url);
const target = select_target(url);
if (target) {
navigate(target, event.state.id);
} else {

View File

@@ -23,7 +23,7 @@ export type ComponentLoader = {
css: string[]
};
export type Page = {
export type Route = {
pattern: RegExp;
parts: Array<{
i: number;
@@ -35,7 +35,7 @@ export type Manifest = {
ignore: RegExp[];
root: ComponentConstructor;
error: () => Promise<{ default: ComponentConstructor }>;
pages: Page[]
pages: Route[]
};
export type ScrollPosition = {
@@ -44,11 +44,12 @@ export type ScrollPosition = {
};
export type Target = {
url: URL;
href: string;
path: string;
page: Page;
route: Route;
match: RegExpExecArray;
query: Record<string, string | string[]>;
params: Record<string, string>;
};
export type Redirect = {
@@ -60,7 +61,7 @@ export type Store = {
get: () => any;
}
export type PageData = {
export type Page = {
path: string;
params: Record<string, string>;
query: Record<string, string | string[]>;