diff --git a/.gitignore b/.gitignore index 7dcc88c..7b62003 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store node_modules cypress/screenshots -test/app/.sapper \ No newline at end of file +test/app/.sapper +runtime.js \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6b26666..54cd665 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 1575426..9b8cf3c 100644 --- a/package.json +++ b/package.json @@ -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": [ diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..77a4f03 --- /dev/null +++ b/rollup.config.js @@ -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') + }) + ] + } +]; \ No newline at end of file diff --git a/runtime/app.js b/runtime/app.js index 1654bca..43ebb81 100644 --- a/runtime/app.js +++ b/runtime/app.js @@ -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
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 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 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; - } -} \ No newline at end of file +console.error('sapper/runtime/app.js has been deprecated in favour of sapper/runtime.js'); +export * from '../runtime.js'; \ No newline at end of file diff --git a/src/runtime/index.ts b/src/runtime/index.ts new file mode 100644 index 0000000..063785b --- /dev/null +++ b/src/runtime/index.ts @@ -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