mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-16 04:44:35 +00:00
Merge branch 'master' into spread_routes
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -292,4 +292,4 @@ async function _build(
|
|||||||
console.log(event.result.print());
|
console.log(event.result.print());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
test/apps/basics/src/routes/middleware/index.js
Normal file
8
test/apps/basics/src/routes/middleware/index.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export function get(req, res, next) {
|
||||||
|
if (req.headers.accept === 'application/json') {
|
||||||
|
res.end('{"json":true}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
1
test/apps/basics/src/routes/middleware/index.svelte
Normal file
1
test/apps/basics/src/routes/middleware/index.svelte
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<h1>HTML</h1>
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<script context="module">
|
||||||
|
export function preload() {}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1>Page loaded</h1>
|
||||||
@@ -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`);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user