Merge branch 'master' into spread_routes

This commit is contained in:
Rich Harris
2019-04-27 11:16:18 -04:00
committed by GitHub
13 changed files with 172 additions and 64 deletions

View File

@@ -10,6 +10,7 @@ import {
ComponentLoader, ComponentLoader,
ComponentConstructor, ComponentConstructor,
Route, Route,
Query,
Page Page
} from './types'; } from './types';
import goto from './goto'; import goto from './goto';
@@ -80,6 +81,20 @@ export { _history as history };
export const scroll_history: Record<string, ScrollPosition> = {}; export const scroll_history: Record<string, ScrollPosition> = {};
export function extract_query(search: string) {
const query = Object.create(null);
if (search.length > 0) {
search.slice(1).split('&').forEach(searchParam => {
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] as string[]).push(value);
else query[key] = value;
});
}
return query;
}
export function select_target(url: URL): Target { export function select_target(url: URL): Target {
if (url.origin !== location.origin) return null; if (url.origin !== location.origin) return null;
if (!url.pathname.startsWith(initial_data.baseUrl)) return null; if (!url.pathname.startsWith(initial_data.baseUrl)) return null;
@@ -93,18 +108,9 @@ export function select_target(url: URL): Target {
const route = routes[i]; const route = routes[i];
const match = route.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) {
url.search.slice(1).split('&').forEach(searchParam => {
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] as string[]).push(value);
else query[key] = value;
});
}
if (match) {
const query: Query = extract_query(url.search);
const part = route.parts[route.parts.length - 1]; const part = route.parts[route.parts.length - 1];
const params = part.params ? part.params(match) : {}; const params = part.params ? part.params(match) : {};
@@ -115,6 +121,35 @@ export function select_target(url: URL): Target {
} }
} }
export function handle_error(url: URL) {
const { pathname, search } = location;
const { session, preloaded, status, error } = initial_data;
if (!root_preloaded) {
root_preloaded = preloaded && preloaded[0]
}
const props = {
error,
status,
session,
level0: {
props: root_preloaded
},
level1: {
props: {
status,
error
},
component: ErrorComponent
},
segments: preloaded
}
const query = extract_query(search);
render(null, [], props, { path: pathname, query, params: {} });
}
export function scroll_state() { export function scroll_state() {
return { return {
x: pageXOffset, x: pageXOffset,

View File

@@ -6,6 +6,7 @@ import {
scroll_history, scroll_history,
scroll_state, scroll_state,
select_target, select_target,
handle_error,
set_target, set_target,
uid, uid,
set_uid, set_uid,
@@ -34,10 +35,12 @@ export default function start(opts: {
history.replaceState({ id: uid }, '', href); history.replaceState({ id: uid }, '', href);
if (!initial_data.error) { const url = new URL(location.href);
const target = select_target(new URL(location.href));
if (target) return navigate(target, uid, false, hash); if (initial_data.error) return handle_error(url);
}
const target = select_target(url);
if (target) return navigate(target, uid, false, hash);
}); });
} }
@@ -127,4 +130,4 @@ function handle_popstate(event: PopStateEvent) {
set_cid(uid); set_cid(uid);
history.replaceState({ id: cid }, '', location.href); history.replaceState({ id: cid }, '', location.href);
} }
} }

View File

@@ -225,7 +225,7 @@ export function get_page_handler(
props[`level${l++}`] = { props[`level${l++}`] = {
component: part.component, component: part.component,
props: preloaded[i + 1], props: preloaded[i + 1] || {},
segment: segments[i] segment: segments[i]
}; };
} }
@@ -243,11 +243,12 @@ export function get_page_handler(
preloaded: `[${preloaded.map(data => try_serialize(data)).join(',')}]`, preloaded: `[${preloaded.map(data => try_serialize(data)).join(',')}]`,
session: session && try_serialize(session, err => { session: session && try_serialize(session, err => {
throw new Error(`Failed to serialize session data: ${err.message}`); throw new Error(`Failed to serialize session data: ${err.message}`);
}) }),
error: error && try_serialize(props.error)
}; };
let script = `__SAPPER__={${[ let script = `__SAPPER__={${[
error && `error:1`, error && `error:${serialized.error},status:${status}`,
`baseUrl:"${req.baseUrl}"`, `baseUrl:"${req.baseUrl}"`,
serialized.preloaded && `preloaded:${serialized.preloaded}`, serialized.preloaded && `preloaded:${serialized.preloaded}`,
serialized.session && `session:${serialized.session}` serialized.session && `session:${serialized.session}`
@@ -329,12 +330,10 @@ export function get_page_handler(
return; return;
} }
if (!server_routes.some(route => route.pattern.test(req.path))) { for (const page of pages) {
for (const page of pages) { if (page.pattern.test(req.path)) {
if (page.pattern.test(req.path)) { handle_page(page, req, res);
handle_page(page, req, res); return;
return;
}
} }
} }

View File

@@ -292,4 +292,4 @@ async function _build(
console.log(event.result.print()); console.log(event.result.print());
} }
}); });
} }

View File

@@ -44,7 +44,7 @@ export function create_serviceworker_manifest({ manifest_data, output, client_fi
client_files: string[]; client_files: string[];
static_files: string; static_files: string;
}) { }) {
let files: string[] = ['/service-worker-index.html']; let files: string[] = ['service-worker-index.html'];
if (fs.existsSync(static_files)) { if (fs.existsSync(static_files)) {
files = files.concat(walk(static_files)); files = files.concat(walk(static_files));

View File

@@ -1,6 +1,6 @@
import format_messages from 'webpack-format-messages'; import format_messages from 'webpack-format-messages';
import { CompileResult, BuildInfo, CompileError, Chunk, CssFile } from './interfaces'; import { CompileResult, BuildInfo, CompileError, Chunk, CssFile } from './interfaces';
import { ManifestData, Dirs } from '../../interfaces'; import { ManifestData, Dirs, PageComponent } from '../../interfaces';
const locPattern = /\((\d+):(\d+)\)$/; const locPattern = /\((\d+):(\d+)\)$/;
@@ -66,12 +66,15 @@ export default class WebpackResult implements CompileResult {
assets: this.assets, assets: this.assets,
css: { css: {
main: extract_css(this.assets.main), main: extract_css(this.assets.main),
chunks: Object chunks: manifest_data.components
.keys(this.assets) .reduce((chunks: Record<string, string[]>, component: PageComponent) => {
.filter(chunkName => chunkName !== 'main') const css_dependencies = [];
.reduce((chunks: { [key: string]: string }, chukName) => { const css = extract_css(this.assets[component.name]);
const assets = this.assets[chukName];
chunks[chukName] = extract_css(assets); if (css) css_dependencies.push(css);
chunks[component.file] = css_dependencies;
return chunks; return chunks;
}, {}) }, {})
} }
@@ -81,4 +84,4 @@ export default class WebpackResult implements CompileResult {
print() { print() {
return this.stats.toString({ colors: true }); return this.stats.toString({ colors: true });
} }
} }

View File

@@ -0,0 +1,8 @@
export function get(req, res, next) {
if (req.headers.accept === 'application/json') {
res.end('{"json":true}');
return;
}
next();
}

View File

@@ -0,0 +1 @@
<h1>HTML</h1>

View File

@@ -8,6 +8,25 @@ import { wait } from '../../utils';
declare let deleted: { id: number }; declare let deleted: { id: number };
declare let el: any; declare let el: any;
function get(url: string, opts?: any): Promise<{ headers: Record<string, string>, body: string }> {
return new Promise((fulfil, reject) => {
const req = http.get(url, opts || {}, res => {
res.on('error', reject);
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
fulfil({
headers: res.headers as Record<string, string>,
body
});
});
});
req.on('error', reject);
});
}
describe('basics', function() { describe('basics', function() {
this.timeout(10000); this.timeout(10000);
@@ -116,38 +135,25 @@ describe('basics', function() {
assert.equal(requests[1], `${base}/b.json`); assert.equal(requests[1], `${base}/b.json`);
}); });
// TODO equivalent test for a webpack app // TODO equivalent test for a webpack app
it('sets Content-Type, Link...modulepreload, and Cache-Control headers', () => { it('sets Content-Type, Link...modulepreload, and Cache-Control headers', async () => {
return new Promise((fulfil, reject) => { const { headers } = await get(base);
const req = http.get(base, res => {
try {
const { headers } = res;
assert.equal( assert.equal(
headers['content-type'], headers['content-type'],
'text/html' 'text/html'
); );
assert.equal( assert.equal(
headers['cache-control'], headers['cache-control'],
'max-age=600' 'max-age=600'
); );
// TODO preload more than just the entry point // TODO preload more than just the entry point
const regex = /<\/client\/client\.\w+\.js>;rel="modulepreload"/; const regex = /<\/client\/client\.\w+\.js>;rel="modulepreload"/;
const link = <string>headers['link']; const link = <string>headers['link'];
assert.ok(regex.test(link), link); assert.ok(regex.test(link), link);
fulfil();
} catch (err) {
reject(err);
}
});
req.on('error', reject);
});
}); });
it('calls a delete handler', async () => { it('calls a delete handler', async () => {
@@ -293,4 +299,18 @@ describe('basics', function() {
'xyz,abc,qwe' 'xyz,abc,qwe'
); );
}); });
it('runs server route handlers before page handlers, if they match', async () => {
const json = await get(`${base}/middleware`, {
headers: {
'Accept': 'application/json'
}
});
assert.equal(json.body, '{"json":true}');
const html = await get(`${base}/middleware`);
assert.ok(html.body.indexOf('<h1>HTML</h1>') !== -1);
});
}); });

View File

@@ -1,3 +1,17 @@
<script>
import { onMount, onDestroy } from 'svelte';
export let status, error = {};
let mounted = false;
onMount(() => {
mounted = 'success';
})
</script>
<h1>{status}</h1> <h1>{status}</h1>
<p>{error.message}</p> <h2>{mounted}</h2>
<p>{error.message}</p>

View File

@@ -112,6 +112,17 @@ describe('errors', function() {
); );
}); });
it('execute error page hooks', async () => {
await page.goto(`${base}/some-throw-page`);
await start();
await wait(50);
assert.equal(
await page.$eval('h2', node => node.textContent),
'success'
);
})
it('does not serve error page for async non-page error', async () => { it('does not serve error page for async non-page error', async () => {
await page.goto(`${base}/async-throw.json`); await page.goto(`${base}/async-throw.json`);
@@ -134,4 +145,4 @@ describe('errors', function() {
await wait(50); await wait(50);
assert.equal(await title(), 'No error here'); assert.equal(await title(), 'No error here');
}); });
}); });

View File

@@ -0,0 +1,5 @@
<script context="module">
export function preload() {}
</script>
<h1>Page loaded</h1>

View File

@@ -37,6 +37,15 @@ describe('preloading', function() {
assert.equal(await title(), 'true'); assert.equal(await title(), 'true');
}); });
it('prevent crash if preload return nothing', async () => {
await page.goto(`${base}/preload-nothing`);
await start();
await wait(50);
assert.equal(await title(), 'Page loaded');
});
it('bails on custom classes returned from preload', async () => { it('bails on custom classes returned from preload', async () => {
await page.goto(`${base}/preload-values/custom-class`); await page.goto(`${base}/preload-values/custom-class`);