Compare commits

..

17 Commits

Author SHA1 Message Date
Rich Harris
ff1e632057 -> v0.3.0 2017-12-31 16:10:36 -05:00
Rich Harris
aeeb231477 Merge branch 'master' of github.com:sveltejs/sapper 2017-12-31 16:08:14 -05:00
Rich Harris
d1940db8c0 Merge pull request #56 from sveltejs/gh-55
handle nested routes
2017-12-31 16:08:08 -05:00
Rich Harris
98f9a64b64 specify pkg.files 2017-12-31 16:05:26 -05:00
Rich Harris
b9bef802d3 lint 2017-12-31 16:05:18 -05:00
Rich Harris
a7024b3806 fix index route 2017-12-31 16:04:54 -05:00
Rich Harris
423e02aeae handle nested routes - fixes #55 2017-12-31 13:25:52 -05:00
Rich Harris
12b73ecebf Merge pull request #54 from sveltejs/preload-req
pass server request object to preload
2017-12-31 13:02:24 -05:00
Rich Harris
e1bc38b5a7 bump timeout 2017-12-31 12:49:17 -05:00
Rich Harris
b66f624f01 pass server request object to preload 2017-12-30 17:31:07 -05:00
Rich Harris
502dd547d1 only preload once - fixes #49 2017-12-25 18:06:27 -05:00
Rich Harris
4c343490d2 merge master -> gh-49 2017-12-25 17:34:27 -05:00
Rich Harris
b3027c5816 port runtime to typescript, move runtime/app.js to runtime.js 2017-12-25 17:14:39 -05:00
Rich Harris
c29e8022cc replace all occurences of variables 2017-12-25 16:18:19 -05:00
Rich Harris
e4cd4c9cb0 Merge pull request #52 from sveltejs/gh-48
cancel navigation if overtaken by a second navigation
2017-12-25 16:17:48 -05:00
Rich Harris
feddad42b2 failing test for #49 2017-12-24 21:34:28 -05:00
Rich Harris
3c4ebcda30 cancel navigation if overtaken by a second navigation - fixes #48 2017-12-24 20:37:00 -05:00
22 changed files with 553 additions and 242 deletions

3
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.DS_Store
node_modules
cypress/screenshots
test/app/.sapper
test/app/.sapper
runtime.js

View File

@@ -1,5 +1,13 @@
# sapper changelog
## 0.3.0
* Move `sapper/runtime/app.js` to `sapper/runtime.js`
* Cancel navigation if overtaken by second navigation ([#48](https://github.com/sveltejs/sapper/issues/48))
* Store preloaded data, to avoiding double prefetching ([#49](https://github.com/sveltejs/sapper/issues/49))
* Pass server request object to `preload` ([#54](https://github.com/sveltejs/sapper/pull/54))
* Nested routes ([#55](https://github.com/sveltejs/sapper/issues/55))
## 0.2.10
* Handle deep links correctly ([#44](https://github.com/sveltejs/sapper/issues/44))

View File

@@ -151,7 +151,7 @@ function get_route_handler(fn) {
const data = { params: req.params, query: req.query };
if (mod.preload) {
const promise = Promise.resolve(mod.preload(data)).then(preloaded => {
const promise = Promise.resolve(mod.preload(req)).then(preloaded => {
Object.assign(data, preloaded);
return mod.render(data);
});

View File

@@ -16,9 +16,22 @@ module.exports = function create_matchers(files) {
.filter(part => part[0] === '[')
.map(part => part.slice(1, -1));
const pattern = new RegExp(
`^\\/${parts.map(p => p[0] === '[' ? '([^/]+)' : p).join('\\/')}$`
);
let pattern_string = '';
let i = parts.length;
let nested = true;
while (i--) {
const part = parts[i];
const dynamic = part[0] === '[';
if (dynamic) {
pattern_string = nested ? `(?:\\/([^/]+)${pattern_string})?` : `\\/([^/]+)${pattern_string}`;
} else {
nested = false;
pattern_string = `\\/${part}${pattern_string}`;
}
}
const pattern = new RegExp(`^${pattern_string || '\\/'}$`);
const test = url => pattern.test(url);
@@ -58,8 +71,8 @@ module.exports = function create_matchers(files) {
return 0;
}
if (!a_part) return 1;
if (!b_part) return -1;
if (!a_part) return -1;
if (!b_part) return 1;
const a_is_dynamic = a_part[0] === '[';
const b_is_dynamic = b_part[0] === '[';

View File

@@ -59,10 +59,10 @@ function generate_service_worker(chunk_files) {
}]`;
return read('templates/service-worker.js')
.replace('__timestamp__', Date.now())
.replace('__assets__', JSON.stringify(assets))
.replace('__shell__', JSON.stringify(chunk_files.concat('/index.html')))
.replace('__routes__', route_code);
.replace(/__timestamp__/g, Date.now())
.replace(/__assets__/g, JSON.stringify(assets))
.replace(/__shell__/g, JSON.stringify(chunk_files.concat('/index.html')))
.replace(/__routes__/g, route_code);
}
function generate_index(main_file) {

69
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "sapper",
"version": "0.2.7",
"version": "0.2.10",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -790,6 +790,12 @@
"integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
"dev": true
},
"compare-versions": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-2.0.1.tgz",
"integrity": "sha1-Htwfk2h/2XoyXFn1XkWgfbEGrKY=",
"dev": true
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1707,6 +1713,12 @@
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
"integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM="
},
"estree-walker": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.2.1.tgz",
"integrity": "sha1-va/oCVOD2EFNXcLs9MkXO225QS4=",
"dev": true
},
"esutils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
@@ -5583,6 +5595,43 @@
"inherits": "2.0.3"
}
},
"rollup": {
"version": "0.53.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-0.53.0.tgz",
"integrity": "sha512-bG5RzkF7wcOHmKoVAFtERZ5P9TNJP9/AF+ldwGm/Rx6pejura+Z9BDU0GJtzWu+lYXwjfINmgiCclhLJzP/OXA==",
"dev": true
},
"rollup-plugin-typescript": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/rollup-plugin-typescript/-/rollup-plugin-typescript-0.8.1.tgz",
"integrity": "sha1-L/fuzCHPa7K0P8J+W2iJUs5xkko=",
"dev": true,
"requires": {
"compare-versions": "2.0.1",
"object-assign": "4.1.1",
"rollup-pluginutils": "1.5.2",
"tippex": "2.3.1",
"typescript": "1.8.10"
},
"dependencies": {
"typescript": {
"version": "1.8.10",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-1.8.10.tgz",
"integrity": "sha1-tHXW4N/wv1DyluXKbvn7tccyDx4=",
"dev": true
}
}
},
"rollup-pluginutils": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz",
"integrity": "sha1-HhVud4+UtyVb+hs9AXi+j1xVJAg=",
"dev": true,
"requires": {
"estree-walker": "0.2.1",
"minimatch": "3.0.4"
}
},
"run-async": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
@@ -6191,6 +6240,12 @@
"setimmediate": "1.0.5"
}
},
"tippex": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tippex/-/tippex-2.3.1.tgz",
"integrity": "sha1-ov1bcIfXy/sgyYBqbBYQjCwPr9o=",
"dev": true
},
"tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
@@ -6229,6 +6284,12 @@
"integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
"dev": true
},
"tslib": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.8.1.tgz",
"integrity": "sha1-aUavLR1lGnsYY7Ux1uWvpBqkTqw=",
"dev": true
},
"tty-browserify": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
@@ -6275,6 +6336,12 @@
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
"dev": true
},
"typescript": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.2.tgz",
"integrity": "sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q=",
"dev": true
},
"uglify-js": {
"version": "2.8.29",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",

View File

@@ -1,11 +1,17 @@
{
"name": "sapper",
"version": "0.2.10",
"version": "0.3.0",
"description": "Military-grade apps, engineered by Svelte",
"main": "lib/index.js",
"bin": {
"sapper": "cli/index.js"
},
"files": [
"cli",
"lib",
"runtime",
"webpack"
],
"directories": {
"test": "test"
},
@@ -30,14 +36,21 @@
"nightmare": "^2.10.0",
"node-fetch": "^1.7.3",
"npm-run-all": "^4.1.2",
"rollup": "^0.53.0",
"rollup-plugin-typescript": "^0.8.1",
"style-loader": "^0.19.1",
"svelte": "^1.49.1",
"svelte-loader": "^2.3.2",
"tslib": "^1.8.1",
"typescript": "^2.6.2",
"wait-on": "^2.0.2"
},
"scripts": {
"cy:open": "cypress open",
"test": "mocha --opts mocha.opts"
"test": "mocha --opts mocha.opts",
"pretest": "npm run build",
"build": "rollup -c",
"dev": "rollup -cw"
},
"repository": "https://github.com/sveltejs/sapper",
"keywords": [

17
rollup.config.js Normal file
View File

@@ -0,0 +1,17 @@
import typescript from 'rollup-plugin-typescript';
export default [
// runtime.js
{
input: 'src/runtime/index.ts',
output: {
file: 'runtime.js',
format: 'es'
},
plugins: [
typescript({
typescript: require('typescript')
})
]
}
];

View File

@@ -1,211 +1,2 @@
const detach = node => {
node.parentNode.removeChild(node);
};
export let component;
let target;
let routes;
const history = typeof window !== 'undefined' ? window.history : {
pushState: () => {},
replaceState: () => {},
};
const scroll_history = {};
let uid = 1;
let cid;
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
function select_route(url) {
if (url.origin !== window.location.origin) return null;
for (const route of routes) {
const match = route.pattern.exec(url.pathname);
if (match) {
const params = route.params(match);
const query = {};
for (const [key, value] of url.searchParams) query[key] = value || true;
return { route, data: { params, query } };
}
}
}
function render(Component, data, scroll) {
Promise.resolve(
Component.preload ? Component.preload(data) : {}
).then(preloaded => {
if (component) {
component.destroy();
} else {
// first load — remove SSR'd <head> contents
const start = document.querySelector('#sapper-head-start');
const end = document.querySelector('#sapper-head-end');
if (start && end) {
while (start.nextSibling !== end) detach(start.nextSibling);
detach(start);
detach(end);
}
// preload additional routes
routes.reduce((promise, route) => promise.then(route.load), Promise.resolve());
}
component = new Component({
target,
data: Object.assign(data, preloaded),
hydrate: !!component
});
if (scroll) {
window.scrollTo(scroll.x, scroll.y);
}
});
}
function navigate(url, id) {
const selected = select_route(url);
if (selected) {
if (id) {
// popstate or initial navigation
cid = id;
} else {
// clicked on a link. preserve scroll state
scroll_history[cid] = scroll_state();
id = cid = ++uid;
scroll_history[cid] = { x: 0, y: 0 };
}
selected.route.load().then(mod => {
render(mod.default, selected.data, scroll_history[id]);
});
cid = id;
return true;
}
}
function handle_click(event) {
// Adapted from https://github.com/visionmedia/page.js
// MIT license https://github.com/visionmedia/page.js#license
if (which(event) !== 1) return;
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
if (event.defaultPrevented) return;
const a = findAnchor(event.target);
if (!a) return;
// check if link is inside an svg
// in this case, both href and target are always inside an object
const svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
const href = svg ? a.href.baseVal : a.href;
if (href === window.location.href) {
event.preventDefault();
return;
}
// Ignore if tag has
// 1. 'download' attribute
// 2. rel='external' attribute
if (a.hasAttribute('download') || a.getAttribute('rel') === 'external') return;
// Ignore if <a> has a target
if (svg ? a.target.baseVal : a.target) return;
const url = new URL(href);
// Don't handle hash changes
if (url.pathname === window.location.pathname && url.search === window.location.search) return;
if (navigate(url, null)) {
event.preventDefault();
history.pushState({ id: cid }, '', url.href);
}
}
function handle_popstate(event) {
scroll_history[cid] = scroll_state();
if (event.state) {
navigate(new URL(window.location), event.state.id);
} else {
// hashchange
cid = ++uid;
history.replaceState({ id: cid }, '', window.location.href);
}
}
function prefetch(event) {
const a = findAnchor(event.target);
if (!a || a.rel !== 'prefetch') return;
const selected = select_route(new URL(a.href));
if (selected) {
selected.route.load().then(mod => {
if (mod.default.preload) mod.default.preload(selected.data);
});
}
}
function findAnchor(node) {
while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG <a> elements have a lowercase name
return node;
}
let inited;
export function init(_target, _routes) {
target = _target;
routes = _routes;
if (!inited) { // this check makes HMR possible
window.addEventListener('click', handle_click);
window.addEventListener('popstate', handle_popstate);
// prefetch
window.addEventListener('touchstart', prefetch);
window.addEventListener('mouseover', prefetch);
inited = true;
}
setTimeout(() => {
const { hash, href } = window.location;
const deep_linked = hash && document.querySelector(hash);
scroll_history[uid] = deep_linked ?
{ x: 0, y: deep_linked.getBoundingClientRect().top } :
scroll_state();
history.replaceState({ id: uid }, '', href);
navigate(new URL(window.location), uid);
});
}
function which(event) {
event = event || window.event;
return event.which === null ? event.button : event.which;
}
function scroll_state() {
return {
x: window.scrollX,
y: window.scrollY
};
}
export function goto(href, opts = {}) {
if (navigate(new URL(href, window.location.href))) {
if (history) history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
} else {
window.location.href = href;
}
}
console.error('sapper/runtime/app.js has been deprecated in favour of sapper/runtime.js');
export * from '../runtime.js';

217
src/runtime/index.ts Normal file
View File

@@ -0,0 +1,217 @@
import { detach, findAnchor, scroll_state, which } from './utils';
import { Component, ComponentConstructor, Params, Query, Route, RouteData, ScrollPosition } from './interfaces';
export let component: Component;
let target: Node;
let routes: Route[];
const history = typeof window !== 'undefined' ? window.history : {
pushState: (state: any, title: string, href: string) => {},
replaceState: (state: any, title: string, href: string) => {},
scrollRestoration: ''
};
const scroll_history: Record<string, ScrollPosition> = {};
let uid = 1;
let cid: number;
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
function select_route(url: URL): { route: Route, data: RouteData } {
if (url.origin !== window.location.origin) return null;
for (const route of routes) {
const match = route.pattern.exec(url.pathname);
if (match) {
const params = route.params(match);
const query: Record<string, string | true> = {};
for (const [key, value] of url.searchParams) query[key] = value || true;
return { route, data: { params, query } };
}
}
}
let current_token: {};
function render(Component: ComponentConstructor, data: any, scroll: ScrollPosition, token: {}) {
if (current_token !== token) return;
if (component) {
component.destroy();
} else {
// first load — remove SSR'd <head> contents
const start = document.querySelector('#sapper-head-start');
const end = document.querySelector('#sapper-head-end');
if (start && end) {
while (start.nextSibling !== end) detach(start.nextSibling);
detach(start);
detach(end);
}
// preload additional routes
routes.reduce((promise: Promise<any>, route) => promise.then(route.load), Promise.resolve());
}
component = new Component({
target,
data,
hydrate: !!component
});
if (scroll) {
window.scrollTo(scroll.x, scroll.y);
}
}
function prepare_route(Component, data) {
return Promise.resolve(
Component.preload ? Component.preload(data) : {}
).then(preloaded => {
Object.assign(data, preloaded)
return { Component, data };
});
}
function navigate(url: URL, id: number) {
const selected = select_route(url);
if (selected) {
if (id) {
// popstate or initial navigation
cid = id;
} else {
// clicked on a link. preserve scroll state
scroll_history[cid] = scroll_state();
id = cid = ++uid;
scroll_history[cid] = { x: 0, y: 0 };
}
const loaded = prefetching && prefetching.href === url.href ?
prefetching.promise :
selected.route.load().then(mod => prepare_route(mod.default, selected.data));
prefetching = null;
const token = current_token = {};
loaded.then(({ Component, data }) => {
render(Component, data, scroll_history[id], token);
});
cid = id;
return true;
}
}
function handle_click(event: MouseEvent) {
// Adapted from https://github.com/visionmedia/page.js
// MIT license https://github.com/visionmedia/page.js#license
if (which(event) !== 1) return;
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
if (event.defaultPrevented) return;
const a: HTMLAnchorElement | SVGAElement = <HTMLAnchorElement | SVGAElement>findAnchor(<Node>event.target);
if (!a) return;
// check if link is inside an svg
// in this case, both href and target are always inside an object
const svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
const href = String(svg ? (<SVGAElement>a).href.baseVal : a.href);
if (href === window.location.href) {
event.preventDefault();
return;
}
// Ignore if tag has
// 1. 'download' attribute
// 2. rel='external' attribute
if (a.hasAttribute('download') || a.getAttribute('rel') === 'external') return;
// Ignore if <a> has a target
if (svg ? (<SVGAElement>a).target.baseVal : a.target) return;
const url = new URL(href);
// Don't handle hash changes
if (url.pathname === window.location.pathname && url.search === window.location.search) return;
if (navigate(url, null)) {
event.preventDefault();
history.pushState({ id: cid }, '', url.href);
}
}
function handle_popstate(event: PopStateEvent) {
scroll_history[cid] = scroll_state();
if (event.state) {
navigate(new URL(window.location.href), event.state.id);
} else {
// hashchange
cid = ++uid;
history.replaceState({ id: cid }, '', window.location.href);
}
}
let prefetching: {
href: string;
promise: Promise<{ Component: ComponentConstructor, data: any }>;
} = null;
function prefetch(event: MouseEvent | TouchEvent) {
const a: HTMLAnchorElement = <HTMLAnchorElement>findAnchor(<Node>event.target);
if (!a || a.rel !== 'prefetch') return;
const selected = select_route(new URL(a.href));
if (selected) {
prefetching = {
href: a.href,
promise: selected.route.load().then(mod => prepare_route(mod.default, selected.data))
};
}
}
let inited: boolean;
export function init(_target: Node, _routes: Route[]) {
target = _target;
routes = _routes;
if (!inited) { // this check makes HMR possible
window.addEventListener('click', handle_click);
window.addEventListener('popstate', handle_popstate);
// prefetch
window.addEventListener('touchstart', prefetch);
window.addEventListener('mouseover', prefetch);
inited = true;
}
setTimeout(() => {
const { hash, href } = window.location;
const deep_linked = hash && document.querySelector(hash);
scroll_history[uid] = deep_linked ?
{ x: 0, y: deep_linked.getBoundingClientRect().top } :
scroll_state();
history.replaceState({ id: uid }, '', href);
navigate(new URL(window.location.href), uid);
});
}
export function goto(href: string, opts = { replaceState: false }) {
if (navigate(new URL(href, window.location.href), null)) {
if (history) history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
} else {
window.location.href = href;
}
}

23
src/runtime/interfaces.ts Normal file
View File

@@ -0,0 +1,23 @@
export type Params = Record<string, string>;
export type Query = Record<string, string | true>;
export type RouteData = { params: Params, query: Query };
export interface ComponentConstructor {
new (options: { target: Node, data: any, hydrate: boolean }): Component;
preload: (data: { params: Params, query: Query }) => Promise<any>;
};
export interface Component {
destroy: () => void;
}
export type Route = {
pattern: RegExp;
params: (match: RegExpExecArray) => Record<string, string>;
load: () => Promise<{ default: ComponentConstructor }>
};
export type ScrollPosition = {
x: number;
y: number;
};

19
src/runtime/utils.ts Normal file
View File

@@ -0,0 +1,19 @@
export function detach(node: Node) {
node.parentNode.removeChild(node);
}
export function findAnchor(node: Node) {
while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG <a> elements have a lowercase name
return node;
}
export function which(event: MouseEvent) {
return event.which === null ? event.button : event.which;
}
export function scroll_state() {
return {
x: window.scrollX,
y: window.scrollY
};
}

View File

@@ -1,10 +1,8 @@
<nav>
<ul>
<li><a class='{{page === "home" ? "selected" : ""}}' href='/'>home</a></li>
<li><a class='{{page === "about" ? "selected" : ""}}' href='/about'>about</a></li>
<!-- for the blog link, we're using rel=prefetch so that Sapper prefetches
the blog data when we hover over the link or tap it on a touchscreen -->
<li><a href='/'>home</a></li>
<li><a href='/about'>about</a></li>
<li><a href='/slow-preload'>slow preload</a></li>
<li><a rel=prefetch class='{{page === "blog" ? "selected" : ""}}' href='/blog'>blog</a></li>
</ul>
</nav>

View File

@@ -12,7 +12,7 @@
<script>
import Layout from './_components/Layout.html';
import { goto } from '../../../runtime/app.js';
import { goto } from '../../../runtime.js';
export default {
components: {

View File

@@ -13,7 +13,7 @@ export function get(req, res, next) {
if (slug in lookup) {
res.set({
'Content-Type': 'application/json',
'Cache-Control': `max-age=${30 * 60 * 1e3}` // cache for 30 minutes
'Cache-Control': `no-cache`
});
res.end(lookup[slug]);

View File

@@ -0,0 +1,9 @@
<p>URL is {{url}}</p>
<script>
export default {
preload({ url }) {
return { url };
}
};
</script>

View File

@@ -0,0 +1,11 @@
<h1>This page should never render</h1>
<script>
export default {
preload() {
return new Promise(fulfil => {
window.fulfil = fulfil;
});
}
};
</script>

View File

@@ -1,4 +1,4 @@
import { init } from '../../../runtime/app.js';
import { init } from '../../../runtime.js';
// `routes` is an array of route objects injected by Sapper
init(document.querySelector('#sapper'), __routes__);

View File

@@ -11,7 +11,7 @@ run('development');
function run(env) {
describe(`env=${env}`, function () {
this.timeout(5000);
this.timeout(20000);
let PORT;
let server;
@@ -208,6 +208,73 @@ function run(env) {
assert.ok(scrollY > 0, scrollY);
});
it('reuses prefetch promise', async () => {
await nightmare
.goto(`${base}/blog`)
.wait(() => window.READY)
.wait(200);
const mouseover_requests = (await capture(async () => {
await nightmare
.mouseover('[href="/blog/what-is-sapper"]')
.wait(200);
})).map(r => r.url);
assert.deepEqual(mouseover_requests, [
'/api/blog/what-is-sapper'
]);
const click_requests = (await capture(async () => {
await nightmare
.click('[href="/blog/what-is-sapper"]')
.wait(200);
})).map(r => r.url);
assert.deepEqual(click_requests, []);
});
it('cancels navigation if subsequent navigation occurs during preload', async () => {
await nightmare
.goto(base)
.wait(() => window.READY)
.click('a[href="/slow-preload"]')
.wait(100)
.click('a[href="/about"]')
.wait(100);
assert.equal(
await nightmare.path(),
'/about'
);
assert.equal(
await nightmare.evaluate(() => document.querySelector('h1').textContent),
'About this site'
);
await nightmare
.evaluate(() => window.fulfil({}))
.wait(100);
assert.equal(
await nightmare.path(),
'/about'
);
assert.equal(
await nightmare.evaluate(() => document.querySelector('h1').textContent),
'About this site'
);
});
it('passes entire request object to preload', async () => {
const html = await nightmare
.goto(`${base}/show-url`)
.evaluate(() => document.querySelector('p').innerHTML);
assert.equal(html, `URL is /show-url`);
});
});
describe('headers', () => {

View File

@@ -10,12 +10,12 @@ describe('create_routes', () => {
assert.deepEqual(
routes.map(r => r.file),
[
'index.html',
'about.html',
'post/foo.html',
'post/bar.html',
'post/[id].html',
'about.html',
'[wildcard].html',
'index.html'
'[wildcard].html'
]
);
});
@@ -45,8 +45,8 @@ describe('create_routes', () => {
assert.deepEqual(
routes.map(r => r.file),
[
'e/f/g/h.html',
'index.html'
'index.html',
'e/f/g/h.html'
]
);
});
@@ -71,4 +71,44 @@ describe('create_routes', () => {
create_routes(['[foo].html', '[bar]/index.html']);
}, /The \[foo\].html and \[bar\]\/index.html routes clash/);
});
it('matches nested routes', () => {
const route = create_routes(['settings/[submenu].html'])[0];
assert.deepEqual(route.exec('/settings/foo'), {
submenu: 'foo'
});
assert.deepEqual(route.exec('/settings'), {
submenu: null
});
});
it('prefers index routes to nested routes', () => {
const routes = create_routes(['settings/[submenu].html', 'settings.html']);
assert.deepEqual(
routes.map(r => r.file),
['settings.html', 'settings/[submenu].html']
);
});
it('matches deeply nested routes', () => {
const route = create_routes(['settings/[a]/[b]/index.html'])[0];
assert.deepEqual(route.exec('/settings/foo/bar'), {
a: 'foo',
b: 'bar'
});
assert.deepEqual(route.exec('/settings/foo'), {
a: 'foo',
b: null
});
assert.deepEqual(route.exec('/settings'), {
a: null,
b: null
});
});
});

18
tsconfig.json Normal file
View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"diagnostics": true,
"noImplicitThis": true,
"noImplicitAny": true,
"noEmitOnError": true,
"allowJs": true,
"lib": ["es5", "es6", "dom"],
"importHelpers": true
},
"target": "ES5",
"include": [
"src"
],
"exclude": [
"node_modules"
]
}

View File

@@ -1,5 +1,4 @@
const path = require('path');
const { src, dest, dev, entry } = require('../lib/config.js');
const { dest, dev, entry } = require('../lib/config.js');
module.exports = {
dev,
@@ -25,7 +24,7 @@ module.exports = {
entry: () => {
return {
main: entry.server
}
};
},
output: () => {