Compare commits

...

11 Commits

Author SHA1 Message Date
Rich Harris
25f0d94595 -> v0.12.0 2018-05-05 10:00:58 -04:00
Rich Harris
8155df2e22 Merge branch 'gh-157' 2018-05-05 09:58:12 -04:00
Rich Harris
bb51470004 Merge pull request #259 from sveltejs/gh-157
switch to single App component model
2018-05-05 09:57:28 -04:00
Rich Harris
53446e2ec7 Merge branch 'master' into gh-157 2018-05-05 09:45:04 -04:00
Rich Harris
c4c09550eb Merge pull request #260 from sveltejs/another-sorting-bug
fix sorting
2018-05-05 09:43:16 -04:00
Rich Harris
da47fdec96 fix sorting 2018-05-05 09:38:24 -04:00
Rich Harris
971342ac7a set preloading: true when appropriate 2018-05-04 23:23:41 -04:00
Rich Harris
3becc1cbe2 error on incorrect init args 2018-05-04 23:06:10 -04:00
Rich Harris
8ee5346900 switch to single App component model (#157) 2018-05-04 22:46:41 -04:00
Rich Harris
9e4b79c6ff Merge pull request #258 from sveltejs/gh-208
exit with code 1 if build/export fails
2018-05-04 17:48:29 -04:00
Rich Harris
4ec1c65395 exit with code 1 if build/export fails - fixes #208 2018-05-04 17:42:37 -04:00
11 changed files with 122 additions and 57 deletions

View File

@@ -1,5 +1,10 @@
# sapper changelog # sapper changelog
## 0.12.0
* Each app has a single `<App>` component. See the [migration guide](https://sapper.svelte.technology/guide#0-11-to-0-12) for more information ([#157](https://github.com/sveltejs/sapper/issues/157))
* Process exits with error code 1 if build/export fails ([#208](https://github.com/sveltejs/sapper/issues/208))
## 0.11.1 ## 0.11.1
* Limit routes with leading dots to `.well-known` URIs ([#252](https://github.com/sveltejs/sapper/issues/252)) * Limit routes with leading dots to `.well-known` URIs ([#252](https://github.com/sveltejs/sapper/issues/252))

View File

@@ -1,6 +1,6 @@
{ {
"name": "sapper", "name": "sapper",
"version": "0.11.1", "version": "0.12.0",
"description": "Military-grade apps, engineered by Svelte", "description": "Military-grade apps, engineered by Svelte",
"main": "dist/middleware.ts.js", "main": "dist/middleware.ts.js",
"bin": { "bin": {

View File

@@ -50,6 +50,7 @@ prog.command('build [dest]')
console.error(`\n> Finished in ${elapsed(start)}. Type ${clorox.bold.cyan(`node ${dest}`)} to run the app.`); console.error(`\n> Finished in ${elapsed(start)}. Type ${clorox.bold.cyan(`node ${dest}`)} to run the app.`);
} catch (err) { } catch (err) {
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error'); console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
process.exit(1);
} }
}); });
@@ -83,6 +84,7 @@ prog.command('export [dest]')
console.error(`\n> Finished in ${elapsed(start)}. Type ${clorox.bold.cyan(`npx serve ${dest}`)} to run the app.`); console.error(`\n> Finished in ${elapsed(start)}. Type ${clorox.bold.cyan(`npx serve ${dest}`)} to run the app.`);
} catch (err) { } catch (err) {
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error'); console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
process.exit(1);
} }
}); });

View File

@@ -30,12 +30,10 @@ export default function create_routes({ files } = { files: glob.sync('**/*.*', {
return !found; return !found;
}) })
.sort((a, b) => { .sort((a, b) => {
const max = Math.max(a.parts.length, b.parts.length); if (a.parts[0] === '4xx' || a.parts[0] === '5xx') return -1;
if (b.parts[0] === '4xx' || b.parts[0] === '5xx') return 1;
if (max === 1) { const max = Math.max(a.parts.length, b.parts.length);
if (a.parts[0] === '4xx' || a.parts[0] === '5xx') return -1;
if (b.parts[0] === '4xx' || b.parts[0] === '5xx') return 1;
}
for (let i = 0; i < max; i += 1) { for (let i = 0; i < max; i += 1) {
const a_part = a.parts[i]; const a_part = a.parts[i];

View File

@@ -20,14 +20,7 @@ type RouteObject = {
type: 'page' | 'route'; type: 'page' | 'route';
pattern: RegExp; pattern: RegExp;
params: (match: RegExpMatchArray) => Record<string, string>; params: (match: RegExpMatchArray) => Record<string, string>;
module: { module: Component;
render: (data: any, opts: { store: Store }) => {
head: string;
css: { code: string, map: any };
html: string
},
preload: (data: any) => any | Promise<any>
};
error?: string; error?: string;
} }
@@ -47,10 +40,24 @@ interface Req extends ClientRequest {
headers: Record<string, string>; headers: Record<string, string>;
} }
export default function middleware({ routes, store }: { interface Component {
render: (data: any, opts: { store: Store }) => {
head: string;
css: { code: string, map: any };
html: string
},
preload: (data: any) => any | Promise<any>
}
export default function middleware({ App, routes, store }: {
App: Component,
routes: RouteObject[], routes: RouteObject[],
store: (req: Req) => Store store: (req: Req) => Store
}) { }) {
if (!App) {
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`);
}
const output = locations.dest(); const output = locations.dest();
const client_info = JSON.parse(fs.readFileSync(path.join(output, 'client_info.json'), 'utf-8')); 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 }: {
cache_control: 'max-age=31536000' cache_control: 'max-age=31536000'
}), }),
get_route_handler(client_info.assets, routes, store) get_route_handler(client_info.assets, App, routes, store)
].filter(Boolean)); ].filter(Boolean));
return middleware; return middleware;
@@ -135,7 +142,7 @@ function serve({ prefix, pathname, cache_control }: {
const resolved = Promise.resolve(); const resolved = Promise.resolve();
function get_route_handler(chunks: Record<string, string>, routes: RouteObject[], store_getter: (req: Req) => Store) { function get_route_handler(chunks: Record<string, string>, App: Component, routes: RouteObject[], store_getter: (req: Req) => Store) {
const template = dev() const template = dev()
? () => fs.readFileSync(`${locations.app()}/template.html`, 'utf-8') ? () => fs.readFileSync(`${locations.app()}/template.html`, 'utf-8')
: (str => () => str)(fs.readFileSync(`${locations.dest()}/template.html`, 'utf-8')); : (str => () => str)(fs.readFileSync(`${locations.dest()}/template.html`, 'utf-8'));
@@ -170,7 +177,7 @@ function get_route_handler(chunks: Record<string, string>, routes: RouteObject[]
res.setHeader('Link', link); res.setHeader('Link', link);
const store = store_getter ? store_getter(req) : null; const store = store_getter ? store_getter(req) : null;
const data = { params: req.params, query: req.query }; const props = { params: req.params, query: req.query, path: req.path };
let redirect: { statusCode: number, location: string }; let redirect: { statusCode: number, location: string };
let error: { statusCode: number, message: Error | string }; let error: { statusCode: number, message: Error | string };
@@ -240,9 +247,9 @@ function get_route_handler(chunks: Record<string, string>, routes: RouteObject[]
preloaded: mod.preload && try_serialize(preloaded), preloaded: mod.preload && try_serialize(preloaded),
store: store && try_serialize(store.get()) store: store && try_serialize(store.get())
}; };
Object.assign(data, preloaded); Object.assign(props, preloaded);
const { html, head, css } = mod.render(data, { const { html, head, css } = App.render({ Page: mod, props }, {
store store
}); });

View File

@@ -3,6 +3,7 @@ import { Component, ComponentConstructor, Params, Query, Route, RouteData, Scrol
const manifest = typeof window !== 'undefined' && window.__SAPPER__; const manifest = typeof window !== 'undefined' && window.__SAPPER__;
export let App: ComponentConstructor;
export let component: Component; export let component: Component;
let target: Node; let target: Node;
let store: Store; let store: Store;
@@ -27,10 +28,10 @@ function select_route(url: URL): Target {
if (url.origin !== window.location.origin) return null; if (url.origin !== window.location.origin) return null;
if (!url.pathname.startsWith(manifest.baseUrl)) return null; if (!url.pathname.startsWith(manifest.baseUrl)) return null;
const pathname = url.pathname.slice(manifest.baseUrl.length); const path = url.pathname.slice(manifest.baseUrl.length);
for (const route of routes) { for (const route of routes) {
const match = route.pattern.exec(pathname); const match = route.pattern.exec(path);
if (match) { if (match) {
if (route.ignore) return null; if (route.ignore) return null;
@@ -43,18 +44,24 @@ function select_route(url: URL): Target {
query[key] = value || true; query[key] = value || true;
}) })
} }
return { url, route, data: { params, query } }; return { url, route, props: { params, query, path } };
} }
} }
} }
let current_token: {}; let current_token: {};
function render(Component: ComponentConstructor, data: any, scroll: ScrollPosition, token: {}) { function render(Page: ComponentConstructor, props: any, scroll: ScrollPosition, token: {}) {
if (current_token !== token) return; if (current_token !== token) return;
const data = {
Page,
props,
preloading: false
};
if (component) { if (component) {
component.destroy(); component.set(data);
} else { } else {
// first load — remove SSR'd <head> contents // first load — remove SSR'd <head> contents
const start = document.querySelector('#sapper-head-start'); const start = document.querySelector('#sapper-head-start');
@@ -65,33 +72,39 @@ function render(Component: ComponentConstructor, data: any, scroll: ScrollPositi
detach(start); detach(start);
detach(end); detach(end);
} }
}
component = new Component({ component = new App({
target, target,
data, data,
store, store,
hydrate: !component hydrate: true
}); });
}
if (scroll) { if (scroll) {
window.scrollTo(scroll.x, scroll.y); window.scrollTo(scroll.x, scroll.y);
} }
} }
function prepare_route(Component: ComponentConstructor, data: RouteData) { function prepare_route(Page: ComponentConstructor, props: RouteData) {
let redirect: { statusCode: number, location: string } = null; let redirect: { statusCode: number, location: string } = null;
let error: { statusCode: number, message: Error | string } = null; let error: { statusCode: number, message: Error | string } = null;
if (!Component.preload) { if (!Page.preload) {
return { Component, data, redirect, error }; return { Page, props, redirect, error };
} }
if (!component && manifest.preloaded) { if (!component && manifest.preloaded) {
return { Component, data: Object.assign(data, manifest.preloaded), redirect, error }; return { Page, props: Object.assign(props, manifest.preloaded), redirect, error };
} }
return Promise.resolve(Component.preload.call({ if (component) {
component.set({
preloading: true
});
}
return Promise.resolve(Page.preload.call({
store, store,
fetch: (url: string, opts?: any) => window.fetch(url, opts), fetch: (url: string, opts?: any) => window.fetch(url, opts),
redirect: (statusCode: number, location: string) => { redirect: (statusCode: number, location: string) => {
@@ -100,7 +113,7 @@ function prepare_route(Component: ComponentConstructor, data: RouteData) {
error: (statusCode: number, message: Error | string) => { error: (statusCode: number, message: Error | string) => {
error = { statusCode, message }; error = { statusCode, message };
} }
}, data)).catch(err => { }, props)).catch(err => {
error = { statusCode: 500, message: err }; error = { statusCode: 500, message: err };
}).then(preloaded => { }).then(preloaded => {
if (error) { if (error) {
@@ -108,15 +121,15 @@ function prepare_route(Component: ComponentConstructor, data: RouteData) {
? errors['4xx'] ? errors['4xx']
: errors['5xx']; : errors['5xx'];
return route.load().then(({ default: Component }: { default: ComponentConstructor }) => { return route.load().then(({ default: Page }: { default: ComponentConstructor }) => {
const err = error.message instanceof Error ? error.message : new Error(error.message); const err = error.message instanceof Error ? error.message : new Error(error.message);
Object.assign(data, { status: error.statusCode, error: err }); Object.assign(props, { status: error.statusCode, error: err });
return { Component, data, redirect: null }; return { Page, props, redirect: null };
}); });
} }
Object.assign(data, preloaded) Object.assign(props, preloaded)
return { Component, data, redirect }; return { Page, props, redirect };
}); });
} }
@@ -136,18 +149,18 @@ function navigate(target: Target, id: number) {
const loaded = prefetching && prefetching.href === target.url.href ? const loaded = prefetching && prefetching.href === target.url.href ?
prefetching.promise : prefetching.promise :
target.route.load().then(mod => prepare_route(mod.default, target.data)); target.route.load().then(mod => prepare_route(mod.default, target.props));
prefetching = null; prefetching = null;
const token = current_token = {}; const token = current_token = {};
return loaded.then(({ Component, data, redirect }) => { return loaded.then(({ Page, props, redirect }) => {
if (redirect) { if (redirect) {
return goto(redirect.location, { replaceState: true }); return goto(redirect.location, { replaceState: true });
} }
render(Component, data, scroll_history[id], token); render(Page, props, scroll_history[id], token);
}); });
} }
@@ -208,7 +221,7 @@ function handle_popstate(event: PopStateEvent) {
let prefetching: { let prefetching: {
href: string; href: string;
promise: Promise<{ Component: ComponentConstructor, data: any }>; promise: Promise<{ Page: ComponentConstructor, props: any }>;
} = null; } = null;
export function prefetch(href: string) { export function prefetch(href: string) {
@@ -217,7 +230,7 @@ export function prefetch(href: string) {
if (selected && (!prefetching || href !== prefetching.href)) { if (selected && (!prefetching || href !== prefetching.href)) {
prefetching = { prefetching = {
href, href,
promise: selected.route.load().then(mod => prepare_route(mod.default, selected.data)) promise: selected.route.load().then(mod => prepare_route(mod.default, selected.props))
}; };
} }
} }
@@ -240,12 +253,17 @@ function trigger_prefetch(event: MouseEvent | TouchEvent) {
let inited: boolean; let inited: boolean;
export function init(_target: Node, _routes: Route[], opts?: { store?: (data: any) => Store }) { export function init(opts: { App: ComponentConstructor, target: Node, routes: Route[], store?: (data: any) => Store }) {
target = _target; if (opts instanceof HTMLElement) {
routes = _routes.filter(r => !r.error); throw new Error(`The signature of init(...) has changed — see https://sapper.svelte.technology/guide#0-11-to-0-12 for more information`);
}
App = opts.App;
target = opts.target;
routes = opts.routes.filter(r => !r.error);
errors = { errors = {
'4xx': _routes.find(r => r.error === '4xx'), '4xx': opts.routes.find(r => r.error === '4xx'),
'5xx': _routes.find(r => r.error === '5xx') '5xx': opts.routes.find(r => r.error === '5xx')
}; };
if (opts && opts.store) { if (opts && opts.store) {

View File

@@ -3,11 +3,11 @@ import { Store } from '../interfaces';
export { Store }; export { Store };
export type Params = Record<string, string>; export type Params = Record<string, string>;
export type Query = Record<string, string | true>; export type Query = Record<string, string | true>;
export type RouteData = { params: Params, query: Query }; export type RouteData = { params: Params, query: Query, path: string };
export interface ComponentConstructor { export interface ComponentConstructor {
new (options: { target: Node, data: any, store: Store, hydrate: boolean }): Component; new (options: { target: Node, data: any, store: Store, hydrate: boolean }): Component;
preload: (data: { params: Params, query: Query }) => Promise<any>; preload: (props: { params: Params, query: Query }) => Promise<any>;
}; };
export interface Component { export interface Component {
@@ -30,5 +30,5 @@ export type ScrollPosition = {
export type Target = { export type Target = {
url: URL; url: URL;
route: Route; route: Route;
data: RouteData; props: RouteData;
}; };

6
test/app/app/App.html Normal file
View File

@@ -0,0 +1,6 @@
{#if preloading}
<progress class='preloading-progress' value=0.5/>
{/if}
<svelte:component this={Page} {...props}/>

View File

@@ -1,9 +1,13 @@
import { init, prefetchRoutes } from '../../../runtime.js'; import { init, prefetchRoutes } from '../../../runtime.js';
import { Store } from 'svelte/store.js'; import { Store } from 'svelte/store.js';
import { routes } from './manifest/client.js'; import { routes } from './manifest/client.js';
import App from './App.html';
window.init = () => { window.init = () => {
return init(document.querySelector('#sapper'), routes, { return init({
target: document.querySelector('#sapper'),
App,
routes,
store: data => new Store(data) store: data => new Store(data)
}); });
}; };

View File

@@ -5,6 +5,7 @@ import serve from 'serve-static';
import sapper from '../../../dist/middleware.ts.js'; import sapper from '../../../dist/middleware.ts.js';
import { Store } from 'svelte/store.js'; import { Store } from 'svelte/store.js';
import { routes } from './manifest/server.js'; import { routes } from './manifest/server.js';
import App from './App.html'
let pending; let pending;
let ended; let ended;
@@ -86,6 +87,7 @@ const middlewares = [
}, },
sapper({ sapper({
App,
routes, routes,
store: () => { store: () => {
return new Store({ return new Store({

View File

@@ -574,6 +574,29 @@ function run({ mode, basepath = '' }) {
assert.ok(html.indexOf('service-worker.js') !== -1); assert.ok(html.indexOf('service-worker.js') !== -1);
}); });
}); });
it('sets preloading true when appropriate', () => {
return nightmare
.goto(base)
.init()
.click('a[href="slow-preload"]')
.wait(100)
.evaluate(() => {
const progress = document.querySelector('progress');
return !!progress;
})
.then(hasProgressIndicator => {
assert.ok(hasProgressIndicator);
})
.then(() => nightmare.evaluate(() => window.fulfil()))
.then(() => nightmare.evaluate(() => {
const progress = document.querySelector('progress');
return !!progress;
}))
.then(hasProgressIndicator => {
assert.ok(!hasProgressIndicator);
});
});
}); });
describe('headers', () => { describe('headers', () => {