port runtime to typescript, move runtime/app.js to runtime.js

This commit is contained in:
Rich Harris
2017-12-25 17:14:39 -05:00
parent c29e8022cc
commit b3027c5816
11 changed files with 356 additions and 220 deletions

3
.gitignore vendored
View File

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

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

@@ -30,14 +30,20 @@
"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",
"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,215 +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 } };
}
}
}
let current_token;
function render(Component, data, scroll, token) {
Promise.resolve(
Component.preload ? Component.preload(data) : {}
).then(preloaded => {
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, 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], current_token = {});
});
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';

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

@@ -0,0 +1,198 @@
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: { query: Query, params: Params }, scroll: ScrollPosition, token: {}) {
Promise.resolve(
Component.preload ? Component.preload(data) : {}
).then(preloaded => {
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: Object.assign(data, preloaded),
hydrate: !!component
});
if (scroll) {
window.scrollTo(scroll.x, scroll.y);
}
});
}
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 };
}
selected.route.load().then(mod => {
render(mod.default, selected.data, scroll_history[id], current_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);
}
}
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) {
selected.route.load().then(mod => {
if (mod.default.preload) mod.default.preload(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

@@ -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

@@ -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__);

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"
]
}