diff --git a/runtime/src/app/app.ts b/runtime/src/app/app.ts index 2de7052..f819ba1 100644 --- a/runtime/src/app/app.ts +++ b/runtime/src/app/app.ts @@ -10,6 +10,7 @@ import { ComponentLoader, ComponentConstructor, Route, + Query, Page } from './types'; import goto from './goto'; @@ -80,6 +81,20 @@ export { _history as history }; export const scroll_history: Record = {}; +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] = [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 { if (url.origin !== location.origin) 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 match = route.pattern.exec(path); - if (match) { - const query: Record = 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] = [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 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() { return { x: pageXOffset, diff --git a/runtime/src/app/start/index.ts b/runtime/src/app/start/index.ts index e371d83..c68f371 100644 --- a/runtime/src/app/start/index.ts +++ b/runtime/src/app/start/index.ts @@ -6,6 +6,7 @@ import { scroll_history, scroll_state, select_target, + handle_error, set_target, uid, set_uid, @@ -34,10 +35,12 @@ export default function start(opts: { history.replaceState({ id: uid }, '', href); - if (!initial_data.error) { - const target = select_target(new URL(location.href)); - if (target) return navigate(target, uid, false, hash); - } + const url = new URL(location.href); + + 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); history.replaceState({ id: cid }, '', location.href); } -} \ No newline at end of file +} diff --git a/runtime/src/server/middleware/get_page_handler.ts b/runtime/src/server/middleware/get_page_handler.ts index c0a39cc..6a4ea14 100644 --- a/runtime/src/server/middleware/get_page_handler.ts +++ b/runtime/src/server/middleware/get_page_handler.ts @@ -225,7 +225,7 @@ export function get_page_handler( props[`level${l++}`] = { component: part.component, - props: preloaded[i + 1], + props: preloaded[i + 1] || {}, segment: segments[i] }; } @@ -243,11 +243,12 @@ export function get_page_handler( preloaded: `[${preloaded.map(data => try_serialize(data)).join(',')}]`, session: session && try_serialize(session, err => { throw new Error(`Failed to serialize session data: ${err.message}`); - }) + }), + error: error && try_serialize(props.error) }; let script = `__SAPPER__={${[ - error && `error:1`, + error && `error:${serialized.error},status:${status}`, `baseUrl:"${req.baseUrl}"`, serialized.preloaded && `preloaded:${serialized.preloaded}`, serialized.session && `session:${serialized.session}` @@ -329,12 +330,10 @@ export function get_page_handler( return; } - if (!server_routes.some(route => route.pattern.test(req.path))) { - for (const page of pages) { - if (page.pattern.test(req.path)) { - handle_page(page, req, res); - return; - } + for (const page of pages) { + if (page.pattern.test(req.path)) { + handle_page(page, req, res); + return; } } diff --git a/src/cli.ts b/src/cli.ts index 5ebacc9..e0b5bac 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -292,4 +292,4 @@ async function _build( console.log(event.result.print()); } }); -} \ No newline at end of file +} diff --git a/src/core/create_app.ts b/src/core/create_app.ts index 2606365..2a0df00 100644 --- a/src/core/create_app.ts +++ b/src/core/create_app.ts @@ -44,7 +44,7 @@ export function create_serviceworker_manifest({ manifest_data, output, client_fi client_files: string[]; static_files: string; }) { - let files: string[] = ['/service-worker-index.html']; + let files: string[] = ['service-worker-index.html']; if (fs.existsSync(static_files)) { files = files.concat(walk(static_files)); diff --git a/src/core/create_compilers/WebpackResult.ts b/src/core/create_compilers/WebpackResult.ts index c014f11..c08e16f 100644 --- a/src/core/create_compilers/WebpackResult.ts +++ b/src/core/create_compilers/WebpackResult.ts @@ -1,6 +1,6 @@ import format_messages from 'webpack-format-messages'; import { CompileResult, BuildInfo, CompileError, Chunk, CssFile } from './interfaces'; -import { ManifestData, Dirs } from '../../interfaces'; +import { ManifestData, Dirs, PageComponent } from '../../interfaces'; const locPattern = /\((\d+):(\d+)\)$/; @@ -66,12 +66,15 @@ export default class WebpackResult implements CompileResult { assets: this.assets, css: { main: extract_css(this.assets.main), - chunks: Object - .keys(this.assets) - .filter(chunkName => chunkName !== 'main') - .reduce((chunks: { [key: string]: string }, chukName) => { - const assets = this.assets[chukName]; - chunks[chukName] = extract_css(assets); + chunks: manifest_data.components + .reduce((chunks: Record, component: PageComponent) => { + const css_dependencies = []; + const css = extract_css(this.assets[component.name]); + + if (css) css_dependencies.push(css); + + chunks[component.file] = css_dependencies; + return chunks; }, {}) } @@ -81,4 +84,4 @@ export default class WebpackResult implements CompileResult { print() { return this.stats.toString({ colors: true }); } -} \ No newline at end of file +} diff --git a/test/apps/basics/src/routes/middleware/index.js b/test/apps/basics/src/routes/middleware/index.js new file mode 100644 index 0000000..52de5b6 --- /dev/null +++ b/test/apps/basics/src/routes/middleware/index.js @@ -0,0 +1,8 @@ +export function get(req, res, next) { + if (req.headers.accept === 'application/json') { + res.end('{"json":true}'); + return; + } + + next(); +} \ No newline at end of file diff --git a/test/apps/basics/src/routes/middleware/index.svelte b/test/apps/basics/src/routes/middleware/index.svelte new file mode 100644 index 0000000..9c4e094 --- /dev/null +++ b/test/apps/basics/src/routes/middleware/index.svelte @@ -0,0 +1 @@ +

HTML

\ No newline at end of file diff --git a/test/apps/basics/test.ts b/test/apps/basics/test.ts index 6fe167b..9516e9f 100644 --- a/test/apps/basics/test.ts +++ b/test/apps/basics/test.ts @@ -8,6 +8,25 @@ import { wait } from '../../utils'; declare let deleted: { id: number }; declare let el: any; +function get(url: string, opts?: any): Promise<{ headers: Record, 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, + body + }); + }); + }); + + req.on('error', reject); + }); +} + describe('basics', function() { this.timeout(10000); @@ -116,38 +135,25 @@ describe('basics', function() { assert.equal(requests[1], `${base}/b.json`); }); - // TODO equivalent test for a webpack app - it('sets Content-Type, Link...modulepreload, and Cache-Control headers', () => { - return new Promise((fulfil, reject) => { - const req = http.get(base, res => { - try { - const { headers } = res; + it('sets Content-Type, Link...modulepreload, and Cache-Control headers', async () => { + const { headers } = await get(base); - assert.equal( - headers['content-type'], - 'text/html' - ); + assert.equal( + headers['content-type'], + 'text/html' + ); - assert.equal( - headers['cache-control'], - 'max-age=600' - ); + assert.equal( + headers['cache-control'], + 'max-age=600' + ); - // TODO preload more than just the entry point - const regex = /<\/client\/client\.\w+\.js>;rel="modulepreload"/; - const link = headers['link']; + // TODO preload more than just the entry point + const regex = /<\/client\/client\.\w+\.js>;rel="modulepreload"/; + const link = headers['link']; - assert.ok(regex.test(link), link); - - fulfil(); - } catch (err) { - reject(err); - } - }); - - req.on('error', reject); - }); + assert.ok(regex.test(link), link); }); it('calls a delete handler', async () => { @@ -293,4 +299,18 @@ describe('basics', function() { '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('

HTML

') !== -1); + }); }); diff --git a/test/apps/errors/src/routes/_error.svelte b/test/apps/errors/src/routes/_error.svelte index 4cd55d2..d76724e 100644 --- a/test/apps/errors/src/routes/_error.svelte +++ b/test/apps/errors/src/routes/_error.svelte @@ -1,3 +1,17 @@ + +

{status}

-

{error.message}

\ No newline at end of file +

{mounted}

+ +

{error.message}

diff --git a/test/apps/errors/test.ts b/test/apps/errors/test.ts index 539587d..d69470c 100644 --- a/test/apps/errors/test.ts +++ b/test/apps/errors/test.ts @@ -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 () => { await page.goto(`${base}/async-throw.json`); @@ -134,4 +145,4 @@ describe('errors', function() { await wait(50); assert.equal(await title(), 'No error here'); }); -}); \ No newline at end of file +}); diff --git a/test/apps/preloading/src/routes/preload-nothing/index.svelte b/test/apps/preloading/src/routes/preload-nothing/index.svelte new file mode 100644 index 0000000..6268767 --- /dev/null +++ b/test/apps/preloading/src/routes/preload-nothing/index.svelte @@ -0,0 +1,5 @@ + + +

Page loaded

diff --git a/test/apps/preloading/test.ts b/test/apps/preloading/test.ts index d30a8d1..97f68b7 100644 --- a/test/apps/preloading/test.ts +++ b/test/apps/preloading/test.ts @@ -37,6 +37,15 @@ describe('preloading', function() { 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 () => { await page.goto(`${base}/preload-values/custom-class`);