move all page info to app-level stores

This commit is contained in:
Rich Harris
2019-02-01 22:28:47 -05:00
parent 7ba1a0a9fa
commit 548de702ac
17 changed files with 135 additions and 154 deletions

View File

@@ -5,7 +5,7 @@ import { Page, PageComponent, ManifestData } from '../interfaces';
const app = fs.readFileSync(path.resolve(__dirname, '../templates/App.html'), 'utf-8'); const app = fs.readFileSync(path.resolve(__dirname, '../templates/App.html'), 'utf-8');
const internal = fs.readFileSync(path.resolve(__dirname, '../templates/internal.mjs'), 'utf-8'); const internal = fs.readFileSync(path.resolve(__dirname, '../templates/internal.mjs'), 'utf-8');
const layout = `<svelte:component this={child.component} {...child.props}/>`; const layout = fs.readFileSync(path.resolve(__dirname, '../templates/layout.html'), 'utf-8');
export function create_main_manifests({ export function create_main_manifests({
bundler, bundler,

View File

@@ -1 +1,8 @@
import { writable } from 'svelte/store';
export const stores = {
preloading: writable(null),
page: writable(null)
};
export const CONTEXT_KEY = {}; export const CONTEXT_KEY = {};

1
templates/layout.html Normal file
View File

@@ -0,0 +1 @@
<svelte:component this={child.component} {...child.props}/>

View File

@@ -1,5 +1,5 @@
import App from '@sapper/App.html'; import App from '@sapper/App.html';
import { preloading, page } from '../shared/stores'; import { stores } from '@sapper/internal';
import Root, * as RootStatic from '__ROOT__'; import Root, * as RootStatic from '__ROOT__';
import ErrorComponent from '__ERROR__'; import ErrorComponent from '__ERROR__';
import { import {
@@ -10,7 +10,8 @@ import {
ComponentLoader, ComponentLoader,
ComponentConstructor, ComponentConstructor,
RootProps, RootProps,
Page Page,
PageData
} from './types'; } from './types';
import goto from './goto'; import goto from './goto';
@@ -25,20 +26,9 @@ let current_token: {};
let root_preload: Promise<any>; let root_preload: Promise<any>;
let root_data: any; let root_data: any;
const root_props: RootProps = {
path: null,
params: null,
query: null,
child: {
segment: null,
component: null,
props: {}
}
};
export let prefetching: { export let prefetching: {
href: string; href: string;
promise: Promise<{ redirect?: Redirect, data?: any, nullable_depth?: number, new_segments?: any }>; promise: Promise<{ redirect?: Redirect, data?: any, new_segments?: any }>;
} = null; } = null;
export function set_prefetching(href, promise) { export function set_prefetching(href, promise) {
prefetching = { href, promise }; prefetching = { href, promise };
@@ -111,7 +101,7 @@ export function scroll_state() {
}; };
} }
export function navigate(target: Target, id: number, noscroll?: boolean, hash?: string): Promise<any> { export async function navigate(target: Target, id: number, noscroll?: boolean, hash?: string): Promise<any> {
let scroll: ScrollPosition; let scroll: ScrollPosition;
if (id) { if (id) {
// popstate or initial navigation // popstate or initial navigation
@@ -129,7 +119,7 @@ export function navigate(target: Target, id: number, noscroll?: boolean, hash?:
cid = id; cid = id;
if (root_component) { if (root_component) {
preloading.set({ stores.preloading.set({
// TODO path, params, query // TODO path, params, query
}); });
} }
@@ -141,38 +131,22 @@ export function navigate(target: Target, id: number, noscroll?: boolean, hash?:
const token = current_token = {}; const token = current_token = {};
return loaded.then(({ redirect, data, nullable_depth, new_segments }) => { const { redirect, page, data, new_segments, results } = await loaded;
if (redirect) {
return goto(redirect.location, { replaceState: true }); if (redirect) return goto(redirect.location, { replaceState: true });
} if (new_segments) segments = new_segments;
if (new_segments) {
segments = new_segments; await render(results, data, page, scroll_history[id], noscroll, hash, token);
} if (document.activeElement) document.activeElement.blur();
render(data, nullable_depth, scroll_history[id], noscroll, hash, token);
if (document.activeElement) document.activeElement.blur();
});
} }
async function render(props: any, nullable_depth: number, scroll: ScrollPosition, noscroll: boolean, hash: string, token: {}) { async function render(results: any[], props: any, page: PageData, scroll: ScrollPosition, noscroll: boolean, hash: string, token: {}) {
if (current_token !== token) return; if (current_token !== token) return;
preloading.set(null); stores.page.set(page);
stores.preloading.set(null);
if (root_component) { if (root_component) {
// first, clear out highest-level root component
let level = props.child;
for (let i = 0; i < nullable_depth; i += 1) {
if (i === nullable_depth) break;
level = level.props.child;
}
const { component } = level;
level.component = null;
root_component.props = props;
// then render new stuff
// TODO do we need to call `flush` before doing this?
level.component = component;
root_component.props = props; root_component.props = props;
} else { } else {
// first load — remove SSR'd <head> contents // first load — remove SSR'd <head> contents
@@ -185,7 +159,7 @@ async function render(props: any, nullable_depth: number, scroll: ScrollPosition
detach(end); detach(end);
} }
Object.assign(props, root_data); Object.assign(props, root_data); // TODO what is root_data, do we still need it?
root_component = new App({ root_component = new App({
target, target,
@@ -215,14 +189,16 @@ async function render(props: any, nullable_depth: number, scroll: ScrollPosition
if (scroll) scrollTo(scroll.x, scroll.y); if (scroll) scrollTo(scroll.x, scroll.y);
} }
Object.assign(root_props, props); previous_thingummy = results;
ready = true; ready = true;
} }
let previous_thingummy = [];
export function prepare_page(target: Target): Promise<{ export function prepare_page(target: Target): Promise<{
redirect?: Redirect; redirect?: Redirect;
data?: any; data?: any;
nullable_depth?: number; page: PageData
}> { }> {
const { page, path, query } = target; const { page, path, query } = target;
const new_segments = path.split('/').filter(Boolean); const new_segments = path.split('/').filter(Boolean);
@@ -240,9 +216,9 @@ export function prepare_page(target: Target): Promise<{
let redirect: Redirect = null; let redirect: Redirect = null;
let error: { statusCode: number, message: Error | string } = null; let error: { statusCode: number, message: Error | string } = null;
let page_data: PageData;
const preload_context = { const preload_context = {
store,
fetch: (url: string, opts?: any) => fetch(url, opts), fetch: (url: string, opts?: any) => fetch(url, opts),
redirect: (statusCode: number, location: string) => { redirect: (statusCode: number, location: string) => {
if (redirect && (redirect.statusCode !== statusCode || redirect.location !== location)) { if (redirect && (redirect.statusCode !== statusCode || redirect.location !== location)) {
@@ -267,11 +243,13 @@ export function prepare_page(target: Target): Promise<{
} }
return Promise.all(page.parts.map((part, i) => { return Promise.all(page.parts.map((part, i) => {
if (i < changed_from) return null; const segment = new_segments[i];
if (i < changed_from || !part) return previous_thingummy[i];
if (!part) return null; if (!part) return null;
return load_component(components[part.i]).then(({ default: Component, preload }) => { return load_component(components[part.i]).then(({ default: Component, preload }) => {
const req = { page_data = {
path, path,
query, query,
params: part.params ? part.params(target.match) : {} params: part.params ? part.params(target.match) : {}
@@ -280,7 +258,7 @@ export function prepare_page(target: Target): Promise<{
let preloaded; let preloaded;
if (ready || !initial_data.preloaded[i + 1]) { if (ready || !initial_data.preloaded[i + 1]) {
preloaded = preload preloaded = preload
? preload.call(preload_context, req) ? preload.call(preload_context, page_data)
: {}; : {};
} else { } else {
preloaded = initial_data.preloaded[i + 1]; preloaded = initial_data.preloaded[i + 1];
@@ -304,72 +282,55 @@ export function prepare_page(target: Target): Promise<{
} }
}).then(results => { }).then(results => {
if (redirect) { if (redirect) {
return { redirect, new_segments }; return { redirect, new_segments, page: null };
} }
const get_params = page.parts[page.parts.length - 1].params || (() => ({})); const deepest = page.parts[page.parts.length - 1];
const params = get_params(target.match);
const page_data = {
path,
query,
params: deepest.params ? deepest.params(target.match) : {}
};
if (error) { if (error) {
const props = {
path,
query,
params,
error: typeof error.message === 'string' ? new Error(error.message) : error.message,
status: error.statusCode
};
return { return {
nullable_depth: 0,
new_segments, new_segments,
data: Object.assign({}, props, { page: page_data,
data: {
child: { child: {
component: ErrorComponent, component: ErrorComponent,
props props: {
error: typeof error.message === 'string' ? new Error(error.message) : error.message,
status: error.statusCode
}
} }
}) }
}; };
} }
const props = { path, query, error: null, status: null }; const props = {
const data = { child: {
path,
child: Object.assign({}, root_props.child, {
segment: new_segments[0] segment: new_segments[0]
}) }
}; };
if (changed(query, root_props.query)) data.query = query;
if (changed(params, root_props.params)) data.params = params;
let level = data.child; let level = props.child;
let nullable_depth = 0;
for (let i = 0; i < page.parts.length; i += 1) { for (let i = 0; i < page.parts.length; i += 1) {
const part = page.parts[i]; const part = page.parts[i];
if (!part) continue; if (!part) continue;
const get_params = part.params || (() => ({})); level.component = results[i].Component;
level.props = Object.assign({}, results[i].preloaded, {
if (i < changed_from) { child: {}
level.props.path = path; });
level.props.query = query;
level.props.child = Object.assign({}, level.props.child);
nullable_depth += 1;
} else {
level.component = results[i].Component;
level.props = Object.assign({}, level.props, props, {
params: get_params(target.match),
}, results[i].preloaded);
level.props.child = {};
}
level = level.props.child; level = level.props.child;
level.segment = new_segments[i + 1]; level.segment = new_segments[i + 1];
} }
return { data, nullable_depth, new_segments }; return { data: props, new_segments, page: page_data, results };
}); });
} }
@@ -402,8 +363,4 @@ export function load_component(component: ComponentLoader): Promise<{
function detach(node: Node) { function detach(node: Node) {
node.parentNode.removeChild(node); node.parentNode.removeChild(node);
} }
function changed(a: Record<string, string | true>, b: Record<string, string | true>) {
return JSON.stringify(a) !== JSON.stringify(b);
}

View File

@@ -1,6 +1,5 @@
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { CONTEXT_KEY } from '@sapper/internal'; import { CONTEXT_KEY, stores } from '@sapper/internal';
import * as stores from '../shared/stores';
export const preloading = { subscribe: stores.preloading.subscribe }; export const preloading = { subscribe: stores.preloading.subscribe };
export const page = { subscribe: stores.page.subscribe }; export const page = { subscribe: stores.page.subscribe };

View File

@@ -65,4 +65,10 @@ export type Redirect = {
export type Store = { export type Store = {
get: () => any; get: () => any;
} }
export type PageData = {
path: string;
params: Record<string, string>;
query: Record<string, string | string[]>;
};

View File

@@ -4,9 +4,9 @@ import cookie from 'cookie';
import devalue from 'devalue'; import devalue from 'devalue';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import URL from 'url'; import URL from 'url';
import * as stores from '../../shared/stores';
import { build_dir, dev, src_dir, IGNORE } from '../placeholders'; import { build_dir, dev, src_dir, IGNORE } from '../placeholders';
import { Manifest, Page, Props, Req, Res } from './types'; import { Manifest, Page, Props, Req, Res } from './types';
import { stores } from '@sapper/internal';
import App from '@sapper/App.html'; import App from '@sapper/App.html';
export function get_page_handler( export function get_page_handler(
@@ -135,6 +135,7 @@ export function get_page_handler(
let preloaded; let preloaded;
let match; let match;
let params;
try { try {
const root_preloaded = manifest.root_preload const root_preloaded = manifest.root_preload
@@ -147,16 +148,20 @@ export function get_page_handler(
match = error ? null : page.pattern.exec(req.path); match = error ? null : page.pattern.exec(req.path);
let toPreload = [root_preloaded]; let toPreload = [root_preloaded];
if (!isSWIndexHtml) { if (!isSWIndexHtml) {
toPreload = toPreload.concat(page.parts.map(part => { toPreload = toPreload.concat(page.parts.map(part => {
if (!part) return null; if (!part) return null;
// the deepest level is used below, to initialise the store
params = part.params ? part.params(match) : {};
return part.preload return part.preload
? part.preload.call(preload_context, { ? part.preload.call(preload_context, {
path: req.path, path: req.path,
query: req.query, query: req.query,
params: part.params ? part.params(match) : {} params
}) })
: {}; : {};
})) }))
@@ -186,60 +191,46 @@ export function get_page_handler(
const segments = req.path.split('/').filter(Boolean); const segments = req.path.split('/').filter(Boolean);
const props: Props = { const props = Object.assign({}, preloaded[0], {
path: req.path,
query: req.query,
params: {},
child: null
};
if (error) {
props.error = error instanceof Error ? error : { message: error };
props.status = status;
}
const data = Object.assign({}, props, preloaded[0], {
params: {},
child: { child: {
segment: segments[0] segment: segments[0],
props: {}
} }
}); });
let level = data.child; let level = props.child;
if (isSWIndexHtml) { if (!isSWIndexHtml) {
level.props = Object.assign({}, props, {
params: {}
})
} else {
for (let i = 0; i < page.parts.length; i += 1) { for (let i = 0; i < page.parts.length; i += 1) {
const part = page.parts[i]; const part = page.parts[i];
if (!part) continue; if (!part) continue;
const get_params = part.params || (() => ({}));
Object.assign(level, { Object.assign(level, {
component: part.component, component: part.component,
props: Object.assign({}, props, { props: Object.assign({}, preloaded[i + 1])
params: get_params(match)
}, preloaded[i + 1])
}); });
level.props.child = <Props["child"]>{ level.props.child = <Props["child"]>{
segment: segments[i + 1] segment: segments[i + 1],
props: {}
}; };
level = level.props.child; level = level.props.child;
} }
} }
if (error) {
props.child.props.error = error instanceof Error ? error : { message: error };
props.child.props.status = status;
}
stores.page.set({ stores.page.set({
path: req.path, path: req.path,
query: req.query, query: req.query,
params: req.params params: params
}); });
const { html, head, css } = App.render({ const { html, head, css } = App.render({
Root: manifest.root, Root: manifest.root,
props: data, props: props,
session session
}); });
@@ -313,6 +304,7 @@ export function get_page_handler(
res.statusCode = status; res.statusCode = status;
res.end(body); res.end(body);
} catch(err) { } catch(err) {
console.log(err);
if (error) { if (error) {
// we encountered an error while rendering the error page — oops // we encountered an error while rendering the error page — oops
res.statusCode = 500; res.statusCode = 500;

View File

@@ -12,6 +12,7 @@ export type Page = {
name: string; name: string;
component: Component; component: Component;
params?: (match: RegExpMatchArray) => Record<string, string>; params?: (match: RegExpMatchArray) => Record<string, string>;
preload?: (data: any) => any | Promise<any>;
}> }>
}; };
@@ -19,6 +20,7 @@ export type Manifest = {
server_routes: ServerRoute[]; server_routes: ServerRoute[];
pages: Page[]; pages: Page[];
root: Component; root: Component;
root_preload?: (data: any) => any | Promise<any>;
error: Component; error: Component;
} }
@@ -29,9 +31,6 @@ export type Store = {
}; };
export type Props = { export type Props = {
path: string;
query: Record<string, string>;
params: Record<string, string>;
error?: { message: string }; error?: { message: string };
status?: number; status?: number;
child: { child: {
@@ -64,6 +63,5 @@ interface Component {
head: string; head: string;
css: { code: string, map: any }; css: { code: string, map: any };
html: string html: string
}, }
preload: (data: any) => any | Promise<any>
} }

View File

@@ -1,4 +0,0 @@
import { writable } from 'svelte/store';
export const preloading = writable(null);
export const page = writable(null);

View File

@@ -1 +1,5 @@
<h1>{params.slug.toUpperCase()}</h1> <script>
import { page } from '@sapper/app';
</script>
<h1>{$page.params.slug.toUpperCase()}</h1>

View File

@@ -1 +1,5 @@
<h1>{JSON.stringify(query)}</h1> <script>
import { page } from '@sapper/app';
</script>
<h1>{JSON.stringify($page.query)}</h1>

View File

@@ -7,8 +7,8 @@
</script> </script>
<script> <script>
import { page } from '@sapper/app';
export let slug; export let slug;
export let query;
</script> </script>
<h1>{slug} {JSON.stringify(query)}</h1> <h1>{slug} {JSON.stringify($page.query)}</h1>

View File

@@ -2,6 +2,11 @@ import * as sapper from '@sapper/app';
window.start = () => sapper.start({ window.start = () => sapper.start({
target: document.querySelector('#sapper') target: document.querySelector('#sapper')
}).catch(err => {
console.error(`OH NO! ${err.message}`);
throw err;
}).then(() => {
console.log(`STARTED`);
}); });
window.prefetchRoutes = () => sapper.prefetchRoutes(); window.prefetchRoutes = () => sapper.prefetchRoutes();

View File

@@ -9,10 +9,10 @@
</script> </script>
<script> <script>
export let params; import { page } from '@sapper/app';
export let count; export let count;
export let segment = params.z;
</script> </script>
<span>z: {segment} {count}</span> <span>z: {$page.params.z} {count}</span>
<a href="foo/bar/qux">click me</a> <a href="foo/bar/qux">click me</a>

View File

@@ -9,13 +9,13 @@
</script> </script>
<script> <script>
export let params; import { page } from '@sapper/app';
export let count; export let count;
export let child; export let child;
export let segment = params.y;
</script> </script>
<span>y: {segment} {count}</span> <span>y: {$page.params.y} {count}</span>
<svelte:component this={child.component} {...child.props}/> <svelte:component this={child.component} {...child.props}/>
<span>child segment: {child.segment}</span> <span>child segment: {child.segment}</span>

View File

@@ -26,10 +26,18 @@ describe('layout', function() {
it('only recreates components when necessary', async () => { it('only recreates components when necessary', async () => {
await page.goto(`${base}/foo/bar/baz`); await page.goto(`${base}/foo/bar/baz`);
await start();
const text1 = String(await page.evaluate(() => document.querySelector('#sapper').textContent)); const text1 = String(await page.evaluate(() => document.querySelector('#sapper').textContent));
assert.deepEqual(text1.split('\n').filter(Boolean), [ assert.deepEqual(text1.split('\n').filter(Boolean).map(str => str.trim()), [
'y: bar 1',
'z: baz 1',
'click me',
'child segment: baz'
]);
await start();
const text2 = String(await page.evaluate(() => document.querySelector('#sapper').textContent));
assert.deepEqual(text2.split('\n').filter(Boolean).map(str => str.trim()), [
'y: bar 1', 'y: bar 1',
'z: baz 1', 'z: baz 1',
'click me', 'click me',
@@ -39,8 +47,8 @@ describe('layout', function() {
await page.click('[href="foo/bar/qux"]'); await page.click('[href="foo/bar/qux"]');
await wait(50); await wait(50);
const text2 = String(await page.evaluate(() => document.querySelector('#sapper').textContent)); const text3 = String(await page.evaluate(() => document.querySelector('#sapper').textContent));
assert.deepEqual(text2.split('\n').filter(Boolean), [ assert.deepEqual(text3.split('\n').filter(Boolean).map(str => str.trim()), [
'y: bar 1', 'y: bar 1',
'z: qux 2', 'z: qux 2',
'click me', 'click me',

View File

@@ -1 +1,5 @@
<h1>{params.slug}</h1> <script>
import { page } from '@sapper/app';
</script>
<h1>{$page.params.slug}</h1>