Skip to content
This repository was archived by the owner on Jan 11, 2023. It is now read-only.

Commit bb51470

Browse files
authored
Merge pull request #259 from sveltejs/gh-157
switch to single App component model
2 parents c4c0955 + 971342a commit bb51470

File tree

7 files changed

+111
-51
lines changed

7 files changed

+111
-51
lines changed

src/middleware.ts

+21-14
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,7 @@ type RouteObject = {
2020
type: 'page' | 'route';
2121
pattern: RegExp;
2222
params: (match: RegExpMatchArray) => Record<string, string>;
23-
module: {
24-
render: (data: any, opts: { store: Store }) => {
25-
head: string;
26-
css: { code: string, map: any };
27-
html: string
28-
},
29-
preload: (data: any) => any | Promise<any>
30-
};
23+
module: Component;
3124
error?: string;
3225
}
3326

@@ -47,10 +40,24 @@ interface Req extends ClientRequest {
4740
headers: Record<string, string>;
4841
}
4942

50-
export default function middleware({ routes, store }: {
43+
interface Component {
44+
render: (data: any, opts: { store: Store }) => {
45+
head: string;
46+
css: { code: string, map: any };
47+
html: string
48+
},
49+
preload: (data: any) => any | Promise<any>
50+
}
51+
52+
export default function middleware({ App, routes, store }: {
53+
App: Component,
5154
routes: RouteObject[],
5255
store: (req: Req) => Store
5356
}) {
57+
if (!App) {
58+
throw new Error(`As of 0.12, you must supply an App component to Sapper — see https://sapper.svelte.technology/guide#0-11-to-0-12 for more information`);
59+
}
60+
5461
const output = locations.dest();
5562

5663
const client_info = JSON.parse(fs.readFileSync(path.join(output, 'client_info.json'), 'utf-8'));
@@ -90,7 +97,7 @@ export default function middleware({ routes, store }: {
9097
cache_control: 'max-age=31536000'
9198
}),
9299

93-
get_route_handler(client_info.assets, routes, store)
100+
get_route_handler(client_info.assets, App, routes, store)
94101
].filter(Boolean));
95102

96103
return middleware;
@@ -135,7 +142,7 @@ function serve({ prefix, pathname, cache_control }: {
135142

136143
const resolved = Promise.resolve();
137144

138-
function get_route_handler(chunks: Record<string, string>, routes: RouteObject[], store_getter: (req: Req) => Store) {
145+
function get_route_handler(chunks: Record<string, string>, App: Component, routes: RouteObject[], store_getter: (req: Req) => Store) {
139146
const template = dev()
140147
? () => fs.readFileSync(`${locations.app()}/template.html`, 'utf-8')
141148
: (str => () => str)(fs.readFileSync(`${locations.dest()}/template.html`, 'utf-8'));
@@ -170,7 +177,7 @@ function get_route_handler(chunks: Record<string, string>, routes: RouteObject[]
170177
res.setHeader('Link', link);
171178

172179
const store = store_getter ? store_getter(req) : null;
173-
const data = { params: req.params, query: req.query };
180+
const props = { params: req.params, query: req.query, path: req.path };
174181

175182
let redirect: { statusCode: number, location: string };
176183
let error: { statusCode: number, message: Error | string };
@@ -240,9 +247,9 @@ function get_route_handler(chunks: Record<string, string>, routes: RouteObject[]
240247
preloaded: mod.preload && try_serialize(preloaded),
241248
store: store && try_serialize(store.get())
242249
};
243-
Object.assign(data, preloaded);
250+
Object.assign(props, preloaded);
244251

245-
const { html, head, css } = mod.render(data, {
252+
const { html, head, css } = App.render({ Page: mod, props }, {
246253
store
247254
});
248255

src/runtime/index.ts

+51-33
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Component, ComponentConstructor, Params, Query, Route, RouteData, Scrol
33

44
const manifest = typeof window !== 'undefined' && window.__SAPPER__;
55

6+
export let App: ComponentConstructor;
67
export let component: Component;
78
let target: Node;
89
let store: Store;
@@ -27,10 +28,10 @@ function select_route(url: URL): Target {
2728
if (url.origin !== window.location.origin) return null;
2829
if (!url.pathname.startsWith(manifest.baseUrl)) return null;
2930

30-
const pathname = url.pathname.slice(manifest.baseUrl.length);
31+
const path = url.pathname.slice(manifest.baseUrl.length);
3132

3233
for (const route of routes) {
33-
const match = route.pattern.exec(pathname);
34+
const match = route.pattern.exec(path);
3435
if (match) {
3536
if (route.ignore) return null;
3637

@@ -43,18 +44,24 @@ function select_route(url: URL): Target {
4344
query[key] = value || true;
4445
})
4546
}
46-
return { url, route, data: { params, query } };
47+
return { url, route, props: { params, query, path } };
4748
}
4849
}
4950
}
5051

5152
let current_token: {};
5253

53-
function render(Component: ComponentConstructor, data: any, scroll: ScrollPosition, token: {}) {
54+
function render(Page: ComponentConstructor, props: any, scroll: ScrollPosition, token: {}) {
5455
if (current_token !== token) return;
5556

57+
const data = {
58+
Page,
59+
props,
60+
preloading: false
61+
};
62+
5663
if (component) {
57-
component.destroy();
64+
component.set(data);
5865
} else {
5966
// first load — remove SSR'd <head> contents
6067
const start = document.querySelector('#sapper-head-start');
@@ -65,33 +72,39 @@ function render(Component: ComponentConstructor, data: any, scroll: ScrollPositi
6572
detach(start);
6673
detach(end);
6774
}
68-
}
6975

70-
component = new Component({
71-
target,
72-
data,
73-
store,
74-
hydrate: !component
75-
});
76+
component = new App({
77+
target,
78+
data,
79+
store,
80+
hydrate: true
81+
});
82+
}
7683

7784
if (scroll) {
7885
window.scrollTo(scroll.x, scroll.y);
7986
}
8087
}
8188

82-
function prepare_route(Component: ComponentConstructor, data: RouteData) {
89+
function prepare_route(Page: ComponentConstructor, props: RouteData) {
8390
let redirect: { statusCode: number, location: string } = null;
8491
let error: { statusCode: number, message: Error | string } = null;
8592

86-
if (!Component.preload) {
87-
return { Component, data, redirect, error };
93+
if (!Page.preload) {
94+
return { Page, props, redirect, error };
8895
}
8996

9097
if (!component && manifest.preloaded) {
91-
return { Component, data: Object.assign(data, manifest.preloaded), redirect, error };
98+
return { Page, props: Object.assign(props, manifest.preloaded), redirect, error };
9299
}
93100

94-
return Promise.resolve(Component.preload.call({
101+
if (component) {
102+
component.set({
103+
preloading: true
104+
});
105+
}
106+
107+
return Promise.resolve(Page.preload.call({
95108
store,
96109
fetch: (url: string, opts?: any) => window.fetch(url, opts),
97110
redirect: (statusCode: number, location: string) => {
@@ -100,23 +113,23 @@ function prepare_route(Component: ComponentConstructor, data: RouteData) {
100113
error: (statusCode: number, message: Error | string) => {
101114
error = { statusCode, message };
102115
}
103-
}, data)).catch(err => {
116+
}, props)).catch(err => {
104117
error = { statusCode: 500, message: err };
105118
}).then(preloaded => {
106119
if (error) {
107120
const route = error.statusCode >= 400 && error.statusCode < 500
108121
? errors['4xx']
109122
: errors['5xx'];
110123

111-
return route.load().then(({ default: Component }: { default: ComponentConstructor }) => {
124+
return route.load().then(({ default: Page }: { default: ComponentConstructor }) => {
112125
const err = error.message instanceof Error ? error.message : new Error(error.message);
113-
Object.assign(data, { status: error.statusCode, error: err });
114-
return { Component, data, redirect: null };
126+
Object.assign(props, { status: error.statusCode, error: err });
127+
return { Page, props, redirect: null };
115128
});
116129
}
117130

118-
Object.assign(data, preloaded)
119-
return { Component, data, redirect };
131+
Object.assign(props, preloaded)
132+
return { Page, props, redirect };
120133
});
121134
}
122135

@@ -136,18 +149,18 @@ function navigate(target: Target, id: number) {
136149

137150
const loaded = prefetching && prefetching.href === target.url.href ?
138151
prefetching.promise :
139-
target.route.load().then(mod => prepare_route(mod.default, target.data));
152+
target.route.load().then(mod => prepare_route(mod.default, target.props));
140153

141154
prefetching = null;
142155

143156
const token = current_token = {};
144157

145-
return loaded.then(({ Component, data, redirect }) => {
158+
return loaded.then(({ Page, props, redirect }) => {
146159
if (redirect) {
147160
return goto(redirect.location, { replaceState: true });
148161
}
149162

150-
render(Component, data, scroll_history[id], token);
163+
render(Page, props, scroll_history[id], token);
151164
});
152165
}
153166

@@ -208,7 +221,7 @@ function handle_popstate(event: PopStateEvent) {
208221

209222
let prefetching: {
210223
href: string;
211-
promise: Promise<{ Component: ComponentConstructor, data: any }>;
224+
promise: Promise<{ Page: ComponentConstructor, props: any }>;
212225
} = null;
213226

214227
export function prefetch(href: string) {
@@ -217,7 +230,7 @@ export function prefetch(href: string) {
217230
if (selected && (!prefetching || href !== prefetching.href)) {
218231
prefetching = {
219232
href,
220-
promise: selected.route.load().then(mod => prepare_route(mod.default, selected.data))
233+
promise: selected.route.load().then(mod => prepare_route(mod.default, selected.props))
221234
};
222235
}
223236
}
@@ -240,12 +253,17 @@ function trigger_prefetch(event: MouseEvent | TouchEvent) {
240253

241254
let inited: boolean;
242255

243-
export function init(_target: Node, _routes: Route[], opts?: { store?: (data: any) => Store }) {
244-
target = _target;
245-
routes = _routes.filter(r => !r.error);
256+
export function init(opts: { App: ComponentConstructor, target: Node, routes: Route[], store?: (data: any) => Store }) {
257+
if (opts instanceof HTMLElement) {
258+
throw new Error(`The signature of init(...) has changed — see https://sapper.svelte.technology/guide#0-11-to-0-12 for more information`);
259+
}
260+
261+
App = opts.App;
262+
target = opts.target;
263+
routes = opts.routes.filter(r => !r.error);
246264
errors = {
247-
'4xx': _routes.find(r => r.error === '4xx'),
248-
'5xx': _routes.find(r => r.error === '5xx')
265+
'4xx': opts.routes.find(r => r.error === '4xx'),
266+
'5xx': opts.routes.find(r => r.error === '5xx')
249267
};
250268

251269
if (opts && opts.store) {

src/runtime/interfaces.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import { Store } from '../interfaces';
33
export { Store };
44
export type Params = Record<string, string>;
55
export type Query = Record<string, string | true>;
6-
export type RouteData = { params: Params, query: Query };
6+
export type RouteData = { params: Params, query: Query, path: string };
77

88
export interface ComponentConstructor {
99
new (options: { target: Node, data: any, store: Store, hydrate: boolean }): Component;
10-
preload: (data: { params: Params, query: Query }) => Promise<any>;
10+
preload: (props: { params: Params, query: Query }) => Promise<any>;
1111
};
1212

1313
export interface Component {
@@ -30,5 +30,5 @@ export type ScrollPosition = {
3030
export type Target = {
3131
url: URL;
3232
route: Route;
33-
data: RouteData;
33+
props: RouteData;
3434
};

test/app/app/App.html

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{#if preloading}
2+
<progress class='preloading-progress' value=0.5/>
3+
{/if}
4+
5+
<svelte:component this={Page} {...props}/>
6+

test/app/app/client.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import { init, prefetchRoutes } from '../../../runtime.js';
22
import { Store } from 'svelte/store.js';
33
import { routes } from './manifest/client.js';
4+
import App from './App.html';
45

56
window.init = () => {
6-
return init(document.querySelector('#sapper'), routes, {
7+
return init({
8+
target: document.querySelector('#sapper'),
9+
App,
10+
routes,
711
store: data => new Store(data)
812
});
913
};

test/app/app/server.js

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import serve from 'serve-static';
55
import sapper from '../../../dist/middleware.ts.js';
66
import { Store } from 'svelte/store.js';
77
import { routes } from './manifest/server.js';
8+
import App from './App.html'
89

910
let pending;
1011
let ended;
@@ -86,6 +87,7 @@ const middlewares = [
8687
},
8788

8889
sapper({
90+
App,
8991
routes,
9092
store: () => {
9193
return new Store({

test/common/test.js

+23
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,29 @@ function run({ mode, basepath = '' }) {
574574
assert.ok(html.indexOf('service-worker.js') !== -1);
575575
});
576576
});
577+
578+
it('sets preloading true when appropriate', () => {
579+
return nightmare
580+
.goto(base)
581+
.init()
582+
.click('a[href="slow-preload"]')
583+
.wait(100)
584+
.evaluate(() => {
585+
const progress = document.querySelector('progress');
586+
return !!progress;
587+
})
588+
.then(hasProgressIndicator => {
589+
assert.ok(hasProgressIndicator);
590+
})
591+
.then(() => nightmare.evaluate(() => window.fulfil()))
592+
.then(() => nightmare.evaluate(() => {
593+
const progress = document.querySelector('progress');
594+
return !!progress;
595+
}))
596+
.then(hasProgressIndicator => {
597+
assert.ok(!hasProgressIndicator);
598+
});
599+
});
577600
});
578601

579602
describe('headers', () => {

0 commit comments

Comments
 (0)