Skip to content
This repository has been archived by the owner on Jan 11, 2023. It is now read-only.

prevent client-side navigation to server routes #146

Merged
merged 2 commits into from
Feb 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/core/create_app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
2 changes: 2 additions & 0 deletions src/runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string | true> = {};
Expand Down
3 changes: 2 additions & 1 deletion src/runtime/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export interface Component {
export type Route = {
pattern: RegExp;
params: (match: RegExpExecArray) => Record<string, string>;
load: () => Promise<{ default: ComponentConstructor }>
load: () => Promise<{ default: ComponentConstructor }>;
ignore?: boolean;
};

export type ScrollPosition = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import posts from './_posts.js';
import posts from './blog/_posts.js';

const contents = JSON.stringify(posts.map(post => {
return {
Expand Down
2 changes: 1 addition & 1 deletion test/app/routes/blog/[slug].html
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ <h1>{{post.title}}</h1>
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, '')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const posts = [
<ul>
<li>It's powered by <a href='https://svelte.technology'>Svelte</a> instead of React, so it's faster and your apps are smaller</li>
<li>Instead of route masking, we encode route parameters in filenames. For example, the page you're looking at right now is <code>routes/blog/[slug].html</code></li>
<li>As well as pages (Svelte components, which render on server or client), you can create <em>server routes</em> in your <code>routes</code> directory. These are just <code>.js</code> files that export functions corresponding to HTTP methods, and receive Express <code>request</code> and <code>response</code> 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 <code>routes/api/blog</code>)</li>
<li>As well as pages (Svelte components, which render on server or client), you can create <em>server routes</em> in your <code>routes</code> directory. These are just <code>.js</code> files that export functions corresponding to HTTP methods, and receive Express <code>request</code> and <code>response</code> objects as arguments. This makes it very easy to, for example, add a JSON API such as the one <a href='/blog/how-is-sapper-different-from-next.json'>powering this very page</a></li>
<li>Links are just <code>&lt;a&gt;</code> elements, rather than framework-specific <code>&lt;Link&gt;</code> components. That means, for example, that <a href='/blog/how-can-i-get-involved'>this link right here</a>, despite being inside a blob of HTML, works with the router as you'd expect.</li>
</ul>
`
Expand Down
2 changes: 1 addition & 1 deletion test/app/routes/blog/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ <h1>Recent posts</h1>
},

preload({ params, query }) {
return fetch(`/api/blog/contents`).then(r => r.json()).then(posts => {
return fetch(`/blog.json`).then(r => r.json()).then(posts => {
return { posts };
});
}
Expand Down
35 changes: 25 additions & 10 deletions test/common/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});

Expand Down Expand Up @@ -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'));
});
});

Expand All @@ -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
Expand All @@ -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);
});
});

Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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.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',
Expand Down