From c36780fdc87d556f1de68ef7533b3e3fa3e2a9ef Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 28 Feb 2018 14:23:19 -0500 Subject: [PATCH 1/2] prevent client-side navigation to server routes - fixes #145 --- src/core/create_app.ts | 5 ++- src/runtime/index.ts | 2 ++ src/runtime/interfaces.ts | 3 +- test/app/routes/blog/[slug].html | 2 +- .../blog/[slug].js => blog/[slug].json.js} | 0 test/app/routes/{api => }/blog/_posts.js | 2 +- test/app/routes/blog/index.html | 2 +- .../blog/contents.js => blog/list.json.js} | 0 test/common/test.js | 35 +++++++++++++------ 9 files changed, 36 insertions(+), 15 deletions(-) rename test/app/routes/{api/blog/[slug].js => blog/[slug].json.js} (100%) rename test/app/routes/{api => }/blog/_posts.js (98%) rename test/app/routes/{api/blog/contents.js => blog/list.json.js} (100%) diff --git a/src/core/create_app.ts b/src/core/create_app.ts index ca405db..fdab1f9 100644 --- a/src/core/create_app.ts +++ b/src/core/create_app.ts @@ -22,8 +22,11 @@ function generate_client(routes: Route[], src: string, dev: boolean, dev_port?: // This file is generated by Sapper — do not edit it! export const routes = [ ${routes - .filter(route => route.type === 'page') .map(route => { + if (route.type !== 'page') { + return `{ pattern: ${route.pattern}, ignore: true }`; + } + const file = posixify(`../../routes/${route.file}`); if (route.id === '_4xx' || route.id === '_5xx') { diff --git a/src/runtime/index.ts b/src/runtime/index.ts index 92fdd0e..43e9ee4 100644 --- a/src/runtime/index.ts +++ b/src/runtime/index.ts @@ -26,6 +26,8 @@ function select_route(url: URL): Target { for (const route of routes) { const match = route.pattern.exec(url.pathname); if (match) { + if (route.ignore) return null; + const params = route.params(match); const query: Record = {}; diff --git a/src/runtime/interfaces.ts b/src/runtime/interfaces.ts index 30d9025..e20349e 100644 --- a/src/runtime/interfaces.ts +++ b/src/runtime/interfaces.ts @@ -14,7 +14,8 @@ export interface Component { export type Route = { pattern: RegExp; params: (match: RegExpExecArray) => Record; - load: () => Promise<{ default: ComponentConstructor }> + load: () => Promise<{ default: ComponentConstructor }>; + ignore?: boolean; }; export type ScrollPosition = { diff --git a/test/app/routes/blog/[slug].html b/test/app/routes/blog/[slug].html index 98e9f7a..a1c3d23 100644 --- a/test/app/routes/blog/[slug].html +++ b/test/app/routes/blog/[slug].html @@ -63,7 +63,7 @@ return this.error(500, 'something went wrong'); } - return fetch(`/api/blog/${slug}`).then(r => { + return fetch(`/blog/${slug}.json`).then(r => { if (r.status === 200) { return r.json().then(post => ({ post })); this.error(r.status, '') diff --git a/test/app/routes/api/blog/[slug].js b/test/app/routes/blog/[slug].json.js similarity index 100% rename from test/app/routes/api/blog/[slug].js rename to test/app/routes/blog/[slug].json.js diff --git a/test/app/routes/api/blog/_posts.js b/test/app/routes/blog/_posts.js similarity index 98% rename from test/app/routes/api/blog/_posts.js rename to test/app/routes/blog/_posts.js index f3520d1..75aa0a1 100644 --- a/test/app/routes/api/blog/_posts.js +++ b/test/app/routes/blog/_posts.js @@ -70,7 +70,7 @@ const posts = [
  • It's powered by Svelte instead of React, so it's faster and your apps are smaller
  • Instead of route masking, we encode route parameters in filenames. For example, the page you're looking at right now is routes/blog/[slug].html
  • -
  • As well as pages (Svelte components, which render on server or client), you can create server routes in your routes directory. These are just .js files that export functions corresponding to HTTP methods, and receive Express request and response objects as arguments. This makes it very easy to, for example, add a JSON API such as the one powering this very page (look in routes/api/blog)
  • +
  • As well as pages (Svelte components, which render on server or client), you can create server routes in your routes directory. These are just .js files that export functions corresponding to HTTP methods, and receive Express request and response objects as arguments. This makes it very easy to, for example, add a JSON API such as the one powering this very page
  • Links are just <a> elements, rather than framework-specific <Link> components. That means, for example, that this link right here, despite being inside a blob of HTML, works with the router as you'd expect.
` diff --git a/test/app/routes/blog/index.html b/test/app/routes/blog/index.html index affe73f..fa3494d 100644 --- a/test/app/routes/blog/index.html +++ b/test/app/routes/blog/index.html @@ -32,7 +32,7 @@ }, preload({ params, query }) { - return fetch(`/api/blog/contents`).then(r => r.json()).then(posts => { + return fetch(`/blog/list.json`).then(r => r.json()).then(posts => { return { posts }; }); } diff --git a/test/app/routes/api/blog/contents.js b/test/app/routes/blog/list.json.js similarity index 100% rename from test/app/routes/api/blog/contents.js rename to test/app/routes/blog/list.json.js diff --git a/test/common/test.js b/test/common/test.js index ff7b735..801d464 100644 --- a/test/common/test.js +++ b/test/common/test.js @@ -12,6 +12,10 @@ run('development'); Nightmare.action('page', { title(done) { this.evaluate_now(() => document.querySelector('h1').textContent, done); + }, + + text(done) { + this.evaluate_now(() => document.body.textContent, done); } }); @@ -193,7 +197,7 @@ function run(env) { }); }) .then(requests => { - assert.ok(!!requests.find(r => r.url === '/api/blog/why-the-name')); + assert.ok(!!requests.find(r => r.url === '/blog/why-the-name.json')); }); }); @@ -219,7 +223,7 @@ function run(env) { }); }) .then(mouseover_requests => { - assert.ok(mouseover_requests.findIndex(r => r.url === '/api/blog/what-is-sapper') !== -1); + assert.ok(mouseover_requests.findIndex(r => r.url === '/blog/what-is-sapper.json') !== -1); return capture(() => { return nightmare @@ -228,7 +232,7 @@ function run(env) { }); }) .then(click_requests => { - assert.ok(click_requests.findIndex(r => r.url === '/api/blog/what-is-sapper') === -1); + assert.ok(click_requests.findIndex(r => r.url === '/blog/what-is-sapper.json') === -1); }); }); @@ -376,6 +380,17 @@ function run(env) { assert.equal(title, 'Internal server error'); }); }); + + it('does not attempt client-side navigation to server routes', () => { + return nightmare.goto(`${base}/blog/how-is-sapper-different-from-next`) + .init() + .click(`[href="/blog/how-is-sapper-different-from-next.json"]`) + .wait(200) + .page.text() + .then(text => { + JSON.parse(text); + }); + }); }); describe('headers', () => { @@ -415,13 +430,13 @@ function run(env) { 'blog/what-is-sapper/index.html', 'blog/why-the-name/index.html', - 'api/blog/contents', - 'api/blog/a-very-long-post', - 'api/blog/how-can-i-get-involved', - 'api/blog/how-is-sapper-different-from-next', - 'api/blog/how-to-use-sapper', - 'api/blog/what-is-sapper', - 'api/blog/why-the-name', + 'blog/list.json', + 'blog/a-very-long-post.json', + 'blog/how-can-i-get-involved.json', + 'blog/how-is-sapper-different-from-next.json', + 'blog/how-to-use-sapper.json', + 'blog/what-is-sapper.json', + 'blog/why-the-name.json', 'favicon.png', 'global.css', From 58754c6d156c21db4a3302b4e9c07bc901df77c4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 28 Feb 2018 16:17:07 -0500 Subject: [PATCH 2/2] use blog.json instead of blog/list.json --- test/app/routes/{blog/list.json.js => blog.json.js} | 2 +- test/app/routes/blog/index.html | 2 +- test/common/test.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename test/app/routes/{blog/list.json.js => blog.json.js} (88%) diff --git a/test/app/routes/blog/list.json.js b/test/app/routes/blog.json.js similarity index 88% rename from test/app/routes/blog/list.json.js rename to test/app/routes/blog.json.js index 0a42690..6d2b5cb 100644 --- a/test/app/routes/blog/list.json.js +++ b/test/app/routes/blog.json.js @@ -1,4 +1,4 @@ -import posts from './_posts.js'; +import posts from './blog/_posts.js'; const contents = JSON.stringify(posts.map(post => { return { diff --git a/test/app/routes/blog/index.html b/test/app/routes/blog/index.html index fa3494d..d8deaf5 100644 --- a/test/app/routes/blog/index.html +++ b/test/app/routes/blog/index.html @@ -32,7 +32,7 @@ }, preload({ params, query }) { - return fetch(`/blog/list.json`).then(r => r.json()).then(posts => { + return fetch(`/blog.json`).then(r => r.json()).then(posts => { return { posts }; }); } diff --git a/test/common/test.js b/test/common/test.js index 801d464..9119394 100644 --- a/test/common/test.js +++ b/test/common/test.js @@ -430,7 +430,7 @@ function run(env) { 'blog/what-is-sapper/index.html', 'blog/why-the-name/index.html', - 'blog/list.json', + 'blog.json', 'blog/a-very-long-post.json', 'blog/how-can-i-get-involved.json', 'blog/how-is-sapper-different-from-next.json',