From 8de1eabf62a24c14d39563993edfdf0facab5eec Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 1 Dec 2020 18:50:10 -0500 Subject: [PATCH 01/15] refactor routing logic --- packages/kit/.gitignore | 2 +- packages/kit/rollup.config.js | 12 +- packages/kit/src/api/dev/index.js | 4 +- packages/kit/src/core/create_app.js | 74 ++--- packages/kit/src/core/create_manifest_data.js | 2 +- .../core/test/create_manifest_data.spec.js | 3 +- packages/kit/src/renderer/page.js | 63 +--- .../kit/src/runtime/app/navigation/index.js | 39 +++ .../kit/src/runtime/{ => app}/stores/index.js | 0 .../src/runtime/internal/renderer/index.js | 294 ++++++++++++++++++ .../kit/src/runtime/internal/router/index.js | 202 ++++++++++++ .../kit/src/runtime/internal/singletons.js | 7 + packages/kit/src/runtime/internal/start.js | 33 ++ .../runtime/{navigation => internal}/utils.js | 0 .../kit/src/runtime/navigation/goto/index.js | 19 -- packages/kit/src/runtime/navigation/index.js | 4 - .../kit/src/runtime/navigation/internal.js | 215 ------------- .../src/runtime/navigation/prefetch/index.js | 47 --- .../navigation/prefetchRoutes/index.js | 11 - .../kit/src/runtime/navigation/start/index.js | 262 ---------------- .../runtime/navigation/start/page_store.js | 28 -- packages/snowpack-config/snowpack.config.js | 2 +- test/apps/basics/src/routes/$layout.svelte | 16 +- test/runner.js | 2 +- 24 files changed, 647 insertions(+), 694 deletions(-) create mode 100644 packages/kit/src/runtime/app/navigation/index.js rename packages/kit/src/runtime/{ => app}/stores/index.js (100%) create mode 100644 packages/kit/src/runtime/internal/renderer/index.js create mode 100644 packages/kit/src/runtime/internal/router/index.js create mode 100644 packages/kit/src/runtime/internal/singletons.js create mode 100644 packages/kit/src/runtime/internal/start.js rename packages/kit/src/runtime/{navigation => internal}/utils.js (100%) delete mode 100644 packages/kit/src/runtime/navigation/goto/index.js delete mode 100644 packages/kit/src/runtime/navigation/index.js delete mode 100644 packages/kit/src/runtime/navigation/internal.js delete mode 100644 packages/kit/src/runtime/navigation/prefetch/index.js delete mode 100644 packages/kit/src/runtime/navigation/prefetchRoutes/index.js delete mode 100644 packages/kit/src/runtime/navigation/start/index.js delete mode 100644 packages/kit/src/runtime/navigation/start/page_store.js diff --git a/packages/kit/.gitignore b/packages/kit/.gitignore index 9f4013ceffe7..0e2363f3cc19 100644 --- a/packages/kit/.gitignore +++ b/packages/kit/.gitignore @@ -1,6 +1,6 @@ .DS_Store /node_modules /dist -/assets/app +/assets/runtime /assets/renderer /client/**/*.d.ts diff --git a/packages/kit/rollup.config.js b/packages/kit/rollup.config.js index 9f2d9d9ac725..66fa4c932e26 100644 --- a/packages/kit/rollup.config.js +++ b/packages/kit/rollup.config.js @@ -12,16 +12,18 @@ const external = [].concat( export default [ { input: { - navigation: 'src/runtime/navigation/index.js', - stores: 'src/runtime/stores/index.js' + 'internal/start': 'src/runtime/internal/start.js', + 'internal/singletons': 'src/runtime/internal/singletons.js', + 'app/navigation': 'src/runtime/app/navigation/index.js', + 'app/stores': 'src/runtime/app/stores/index.js' }, output: { - dir: 'assets/app', + dir: 'assets/runtime', format: 'esm', sourcemap: true, paths: { - ROOT: '../generated/root.svelte', - MANIFEST: '../generated/manifest.js' + ROOT: '../../generated/root.svelte', + MANIFEST: '../../generated/manifest.js' } }, external: ['svelte', 'svelte/store', 'ROOT', 'MANIFEST'], diff --git a/packages/kit/src/api/dev/index.js b/packages/kit/src/api/dev/index.js index 667f33b6570e..46bdbe56ac66 100644 --- a/packages/kit/src/api/dev/index.js +++ b/packages/kit/src/api/dev/index.js @@ -139,7 +139,7 @@ class Watcher extends EventEmitter { root = (await load('/_app/assets/generated/root.js')).default; } catch (e) { res.statusCode = 500; - res.end(e.toString()); + res.end(e.stack); return; } @@ -162,7 +162,7 @@ class Watcher extends EventEmitter { manifest: this.manifest, target: this.config.target, client: { - entry: 'assets/app/navigation.js', + entry: 'assets/runtime/internal/start.js', deps: {} }, dev: true, diff --git a/packages/kit/src/core/create_app.js b/packages/kit/src/core/create_app.js index a90c5e926894..67bceb05d214 100644 --- a/packages/kit/src/core/create_app.js +++ b/packages/kit/src/core/create_app.js @@ -47,12 +47,6 @@ export function create_serviceworker_manifest({ write_if_changed(`${output}/service-worker.js`, code); } -function create_param_match(param, i) { - return /^\.{3}.+$/.test(param) - ? `${param.replace(/.{3}/, '')}: d(match[${i + 1}]).split('/')` - : `${param}: d(match[${i + 1}])`; -} - function generate_client_manifest(manifest_data) { const page_ids = new Set(manifest_data.pages.map(page => page.pattern.toString())); @@ -74,49 +68,48 @@ function generate_client_manifest(manifest_data) { let needs_decode = false; - let routes = `[ - ${manifest_data.pages - .map( - (page) => `{ + let pages = `[ + ${manifest_data.pages + .map( + (page) => `{ // ${page.parts[page.parts.length - 1].component.file} pattern: ${page.pattern}, parts: [ ${page.parts .map((part) => { const missing_layout = !part; - if (missing_layout) return 'null'; + if (missing_layout) return null; if (part.params.length > 0) { needs_decode = true; - const props = part.params.map(create_param_match); - return `{ i: ${ - component_indexes[part.component.name] - }, params: match => ({ ${props.join(', ')} }) }`; + const props = part.params.map((param, i) => { + return param.startsWith('...') + ? `${param.slice(3)}: d(m[${i + 1}]).split('/')` + : `${param}: d(m[${i + 1}])`; + }); + return `[components[${component_indexes[part.component.name]}], m => ({ ${props.join(', ')} })]`; } - return `{ i: ${component_indexes[part.component.name]} }`; + return `[components[${component_indexes[part.component.name]}]]`; }) - .join(',\n\t\t\t\t\t\t')} + .filter(Boolean) + .join(',\n\t\t\t\t')} ] - }` - ) - .join(',\n\n\t\t\t\t')} + }`).join(',\n\n\t\t')} ]`.replace(/^\t/gm, ''); if (needs_decode) { - routes = `(d => ${routes})(decodeURIComponent)`; + pages = `(d => ${pages})(decodeURIComponent)`; } return ` - import * as layout from ${JSON.stringify(manifest_data.layout.url)}; - export { layout }; - export { default as ErrorComponent } from ${JSON.stringify(manifest_data.error.url)}; + const components = ${components}; - export const ignore = [${endpoints_to_ignore.map(route => route.pattern).join(', ')}]; + export const pages = ${pages}; - export const components = ${components}; - - export const routes = ${routes}; + export const ignore = [ + ${endpoints_to_ignore.map(route => route.pattern).join(',\n\t\t\t')} + ]; ` .replace(/^\t{2}/gm, '') .trim(); @@ -131,17 +124,17 @@ function generate_app(manifest_data) { const levels = []; for (let i = 0; i < max_depth; i += 1) { - levels.push(i + 1); + levels.push(i); } - let l = max_depth; + let l = max_depth - 1; - let pyramid = ``; + let pyramid = ``; - while (l-- > 1) { + while (l-- > 0) { pyramid = ` - - {#if level${l + 1}} + + {#if components[${l + 1}]} ${pyramid.replace(/\n/g, '\n\t\t\t\t\t')} {/if} @@ -154,25 +147,24 @@ function generate_app(manifest_data) { - + {#if error} {:else} diff --git a/packages/kit/src/core/create_manifest_data.js b/packages/kit/src/core/create_manifest_data.js index 3be0da64b6e9..7ba2329ab781 100644 --- a/packages/kit/src/core/create_manifest_data.js +++ b/packages/kit/src/core/create_manifest_data.js @@ -128,7 +128,7 @@ export default function create_manifest_data( path.join(dir, item.basename), segments, params, - component ? stack.concat({ component, params }) : stack.concat(null) + component ? stack.concat({ component, params }) : stack ); } else if (item.is_page) { const component = { diff --git a/packages/kit/src/core/test/create_manifest_data.spec.js b/packages/kit/src/core/test/create_manifest_data.spec.js index 29105c4dedc0..771eefc35cd4 100644 --- a/packages/kit/src/core/test/create_manifest_data.spec.js +++ b/packages/kit/src/core/test/create_manifest_data.spec.js @@ -184,8 +184,7 @@ test('fails on clashes', () => { }, /The \[bar\]\/index\.svelte and \[foo\]\.svelte pages clash/); assert.throws(() => { - const { server_routes } = create_manifest_data(path.join(__dirname, 'samples/clash-routes')); - console.log(server_routes); + create_manifest_data(path.join(__dirname, 'samples/clash-routes')); }, /The \[bar\]\/index\.js and \[foo\]\.js routes clash/); }); diff --git a/packages/kit/src/renderer/page.js b/packages/kit/src/renderer/page.js index e43aa6aac54d..dd8ecee67253 100644 --- a/packages/kit/src/renderer/page.js +++ b/packages/kit/src/renderer/page.js @@ -20,7 +20,7 @@ async function get_response({ const segments = request.path.split('/').filter(Boolean); - const baseUrl = ''; // TODO + const base = ''; // TODO const dependencies = {}; @@ -136,18 +136,16 @@ async function get_response({ // these are only the parameters up to the current URL segment const params = parts_to_params(match, part); - const props = mod.preload - ? await mod.preload.call( - preload_context, - { - host: request.host, - path: request.path, - query: request.query, - params - }, - session - ) - : {}; + const props = mod.preload ? await mod.preload.call( + preload_context, + { + host: request.host, + path: request.path, + query: request.query, + params + }, + session + ) : {}; preloaded[i] = props; return { component: mod.default, props }; @@ -158,18 +156,6 @@ async function get_response({ if (redirected) return redirected; - // TODO make this less confusing - const layout_segments = [segments[0]]; - let l = 1; - - if (page) { - page.parts.forEach((part, i) => { - layout_segments[l] = segments[i + 1]; - if (!part) return; - l++; - }); - } - const props = { status, error, @@ -184,30 +170,15 @@ async function get_response({ preloading: readable(null, noop), session: writable(session) }, - // TODO stores, status, segments, notify, CONTEXT_KEY - segments: layout_segments, - level0: { - props: preloaded[0] - }, - level1: { - segment: segments[0], - props: {} - } + layout_props: preloaded[0], + components: parts.slice(1).map(part => part.component) }; // leveln (instead of levels[n]) makes it easy to avoid // unnecessary updates for layout components - l = 1; - for (let i = 1; i < parts.length; i += 1) { - const part = parts[i]; - if (!part) continue; - - props[`level${l++}`] = { - component: part.component, - props: preloaded[i] || {}, - segment: segments[i] - }; - } + parts.slice(1).forEach((part, i) => { + props[`props_${i}`] = part.props; + }); const serialized_preloads = `[${preloaded .map((data) => @@ -257,7 +228,7 @@ async function get_response({ import { start } from '/_app/${options.client.entry}'; start({ target: ${options.target ? `document.querySelector(${JSON.stringify(options.target)})` : 'document.body'}, - baseUrl: "${baseUrl}", + base: "${base}", status: ${status}, error: ${serialize_error(error)}, preloaded: ${serialized_preloads}, diff --git a/packages/kit/src/runtime/app/navigation/index.js b/packages/kit/src/runtime/app/navigation/index.js new file mode 100644 index 000000000000..a0fb6adb3914 --- /dev/null +++ b/packages/kit/src/runtime/app/navigation/index.js @@ -0,0 +1,39 @@ +import { router, renderer } from '../../internal/singletons'; + +export function goto(href, { noscroll = false, replaceState = false } = {}) { + const page = router.select(new URL(href, get_base_uri(document))); + + if (page) { + // history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href); + return router.navigate(page, null, { noscroll, replaceState }); + } + + location.href = href; + return new Promise(() => { + /* never resolves */ + }); +} + +export function prefetch(href) { + const page = router.select(new URL(href, get_base_uri(document))); + + return renderer.prefetch(page); + + // if (page) { + // if (!prefetching || href !== prefetching.href) { + // prefetching = { href, promise: hydrate_target(page) }; + // } + + // return prefetching.promise; + // } +} + +export async function prefetchRoutes(pathnames) { + const path_routes = pathnames + ? router.pages.filter((page) => pathnames.some((pathname) => page.pattern.test(pathname))) + : router.pages; + + const promises = path_routes.map((r) => Promise.all(r.parts.map((p) => p[0]()))); + + await Promise.all(promises); +} \ No newline at end of file diff --git a/packages/kit/src/runtime/stores/index.js b/packages/kit/src/runtime/app/stores/index.js similarity index 100% rename from packages/kit/src/runtime/stores/index.js rename to packages/kit/src/runtime/app/stores/index.js diff --git a/packages/kit/src/runtime/internal/renderer/index.js b/packages/kit/src/runtime/internal/renderer/index.js new file mode 100644 index 000000000000..f3aeadc9406f --- /dev/null +++ b/packages/kit/src/runtime/internal/renderer/index.js @@ -0,0 +1,294 @@ +import { writable } from 'svelte/store'; +import { find_anchor } from '../utils'; + +// TODO this seems weird +function page_store(value) { + const store = writable(value); + let ready = true; + + function notify() { + ready = true; + store.update((val) => val); + } + + function set(new_value) { + ready = false; + store.set(new_value); + } + + function subscribe(run) { + let old_value; + return store.subscribe((new_value) => { + if (old_value === undefined || (ready && new_value !== old_value)) { + run((old_value = new_value)); + } + }); + } + + return { notify, set, subscribe }; +} + +export class Renderer { + constructor({ + Root, + target, + error, + status, + preloaded, + session + }) { + this.Root = Root; + + // TODO ideally we wouldn't need to store these... + this.target = target; + + this.initial = { + preloaded, + error, + status + }; + + this.stores = { + page: page_store({}), + preloading: writable(false), + session: writable(session) + }; + + this.$session = null; + this.session_dirty = false; + + this.root = null; + + function trigger_prefetch(event) { + const a = find_anchor(event.target); + + if (a && a.rel === 'prefetch') { // TODO make this svelte-prefetch or something + prefetch(a.href); + } + } + + let mousemove_timeout; + function handle_mousemove(event) { + clearTimeout(mousemove_timeout); + mousemove_timeout = setTimeout(() => { + trigger_prefetch(event); + }, 20); + } + + addEventListener('touchstart', trigger_prefetch); + addEventListener('mousemove', handle_mousemove); + + let ready = false; + this.stores.session.subscribe(async (value) => { + this.$session = value; + + if (!ready) return; + this.session_dirty = true; + + await this.render(this.page); + + const dest = select_target(new URL(location.href)); + + const token = (current_token = {}); + const { redirect, props, branch } = await hydrate_target(dest); + if (token !== current_token) return; // a secondary navigation happened while we were loading + + if (redirect) { + await goto(redirect.location, { replaceState: true }); + } else { + await render(branch, props, buildPageContext(props, dest.page)); + } + }); + ready = true; + } + + async start(page) { + const props = { + stores: this.stores, + error: this.initial.error, + status: this.initial.status, + notify: this.stores.page.notify, // TODO this is weird + layout_props: this.initial.preloaded[0], // TODO or call preload, if serialisation failed + components: [] + }; + + let params = {}; + + if (!this.initial.error) { + try { + const promises = []; + + const match = page.route.pattern.exec(page.page.path); + + page.route.parts.forEach(([loader, get_params], i) => { + const part_params = params = get_params ? get_params(match) : {}; + + promises.push(loader().then(async mod => { + props.components[i] = mod.default; + + if (mod.preload) { + props[`props_${i}`] = this.initial.preloaded[i + 1] || ( + await mod.preload({ + // TODO tidy this up + ...page.page, + params: part_params + }, this.$session) + ); + } + })); + }); + + await Promise.all(promises); + } catch (error) { + props.error = error; + props.status = error.status || 500; + } + } + + this.stores.page.set({ + ...page.page, + params + }); // TODO need to rename some stuff + + this.root = new this.Root({ + target: this.target, + props, + hydrate: true + }); + + // TODO set this.path (path through route DAG) so we can avoid updating unchanged branches + // TODO set this.query, for the same reason + + this.initial = null; + } + + async error(error, status) { + this.root.$set({ + error, + status + }); + + this.path = []; + } + + async render(page) { + throw new Error('nope'); + const token = this.token = {}; + + this.stores.page.set(page); + this.stores.preloading.set(false); + + const props = { + error: null, + status: 200 + }; + + // TODO level1, level2 etc + + if (this.token === token) { // check render wasn't aborted + this.root.$set(props); + // TODO set this.path (path through route DAG) so we can avoid updating unchanged branches + // TODO set this.query, for the same reason + } + } + + async hydrate({ route, page, match }) { + const segments = page.path.split('/').filter(Boolean); + + let redirect = null; + + const props = { error: null, status: 200, segments: [segments[0]] }; + + const preload_context = { + fetch: (url, opts) => fetch(url, opts), + redirect: (statusCode, location) => { + if (redirect && (redirect.statusCode !== statusCode || redirect.location !== location)) { + throw new Error('Conflicting redirects'); + } + redirect = { statusCode, location }; + }, + error: (status, error) => { + props.error = typeof error === 'string' ? new Error(error) : error; + props.status = status; + } + }; + + if (!root_preloaded) { + root_preloaded = + (layout.preload + ? layout.preload.call( + preload_context, + { + host: page.host, + path: page.path, + query: page.query, + params: {} + }, + $session + ) + : {}); + } + + let branch; + let l = 1; + + try { + const stringified_query = JSON.stringify(page.query); + const match = route.pattern.exec(page.path); + + let segment_dirty = false; + + branch = await Promise.all( + route.parts.map(async (part, i) => { + const segment = segments[i]; + + if (part_changed(i, segment, match, stringified_query)) segment_dirty = true; + + props.segments[l] = segments[i + 1]; // TODO make this less confusing + if (!part) return { segment }; + + const j = l++; + + if ( + !session_dirty && + !segment_dirty && + current_branch[i] && + current_branch[i].part === part.i + ) { + return current_branch[i]; + } + + segment_dirty = false; + + const { default: component, preload } = await components[part.i](); + + let preloaded; + if (ready || !initial_preloaded_data[i + 1]) { + preloaded = preload + ? await preload.call( + preload_context, + { + host: page.host, + path: page.path, + query: page.query, + params: part[1] ? part[1](match) : {} + }, + $session + ) + : {}; + } else { + preloaded = initial_preloaded_data[i + 1]; + } + + return (props[`level${j}`] = { component, props: preloaded, segment, match, part: part.i }); + }) + ); + } catch (error) { + props.error = error; + props.status = 500; + branch = []; + } + + return { redirect, props, branch }; + } +} \ No newline at end of file diff --git a/packages/kit/src/runtime/internal/router/index.js b/packages/kit/src/runtime/internal/router/index.js new file mode 100644 index 000000000000..bb4cda2f7014 --- /dev/null +++ b/packages/kit/src/runtime/internal/router/index.js @@ -0,0 +1,202 @@ +import { find_anchor } from "../utils"; + +function which(event) { + return event.which === null ? event.button : event.which; +} + +function scroll_state() { + return { + x: pageXOffset, + y: pageYOffset + }; +} + +export class Router { + constructor({ base, pages, ignore }) { + this.base = base; + this.pages = pages; + this.ignore = ignore; + + this.uid = 1; + this.cid = null; + + this.history = window.history || { + pushState: () => {}, + replaceState: () => {}, + scrollRestoration: 'auto' + }; + } + + init({ renderer }) { + this.renderer = renderer; + renderer.router = this; + + if ('scrollRestoration' in this.history) { + this.history.scrollRestoration = 'manual'; + } + + // Adopted from Nuxt.js + // Reset scrollRestoration to auto when leaving page, allowing page reload + // and back-navigation from other pages to use the browser to restore the + // scrolling position. + addEventListener('beforeunload', () => { + this.history.scrollRestoration = 'auto'; + }); + + // Setting scrollRestoration to manual again when returning to this page. + addEventListener('load', () => { + this.history.scrollRestoration = 'manual'; + }); + + addEventListener('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 || event.altKey) return; + if (event.defaultPrevented) return; + + const a = find_anchor(event.target); + if (!a) return; + + if (!a.href) 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 ? a.href.baseVal : a.href); + + if (href === location.href) { + if (!location.hash) 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 === location.pathname && url.search === location.search) return; + + const page = this.select(url); + if (page) { + const noscroll = a.hasAttribute('sapper:noscroll'); + this.navigate(page, null, noscroll, url.hash); + event.preventDefault(); + this.history.pushState({ id: this.cid }, '', url.href); + } + }); + + addEventListener('popstate', event => { + this.scroll_history[cid] = scroll_state(); + + if (event.state) { + const url = new URL(location.href); + const page = this.select(url); + if (page) { + this.navigate(page, event.state.id); + } else { + // eslint-disable-next-line + location.href = location.href; // nosonar + } + } else { + // hashchange + this.uid += 1; + this.cid = this.uid; + this.history.replaceState({ id: this.cid }, '', location.href); + } + }); + + // load current page + const { hash, href } = location; + + this.history.replaceState({ id: this.uid }, '', href); + + const page = this.select(new URL(location.href)); + // if (page) return this.navigate(page, this.uid, true, hash); + if (page) return this.renderer.start(page); + } + + select(url) { + if (url.origin !== location.origin) return null; + if (!url.pathname.startsWith(this.base)) return null; + + let path = url.pathname.slice(this.base.length); + + if (path === '') { + path = '/'; + } + + // avoid accidental clashes between server routes and page routes + if (this.ignore.some(pattern => pattern.test(path))) return; + + for (const route of this.pages) { + const match = route.pattern.exec(path); + + if (match) { + const query = new URLSearchParams(url.search); + const part = route.parts[route.parts.length - 1]; + const params = part.params ? part.params(match) : {}; + + const page = { host: location.host, path, query, params }; + + return { href: url.href, route, match, page }; + } + } + } + + async navigate( + page, + id, + noscroll, + hash + ) { + const popstate = !!id; + if (popstate) { + this.cid = id; + } else { + const current_scroll = scroll_state(); + + // clicked on a link. preserve scroll state + this.scroll_history[this.cid] = current_scroll; + + this.cid = id = ++this.uid; + this.scroll_history[this.cid] = noscroll ? current_scroll : { x: 0, y: 0 }; + } + + await this.renderer.render(page); + + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur(); + } + + if (!noscroll) { + let scroll = this.scroll_history[id]; + + let deep_linked; + if (hash) { + // scroll is an element id (from a hash), we need to compute y. + deep_linked = document.getElementById(hash.slice(1)); + + if (deep_linked) { + scroll = { + x: 0, + y: deep_linked.getBoundingClientRect().top + scrollY + }; + } + } + + this.scroll_history[this.cid] = scroll; + if (popstate || deep_linked) { + scrollTo(scroll.x, scroll.y); + } else { + scrollTo(0, 0); + } + } + } +} \ No newline at end of file diff --git a/packages/kit/src/runtime/internal/singletons.js b/packages/kit/src/runtime/internal/singletons.js new file mode 100644 index 000000000000..a1c69c4be56b --- /dev/null +++ b/packages/kit/src/runtime/internal/singletons.js @@ -0,0 +1,7 @@ +export let router; +export let renderer; + +export function init(opts) { + router = opts.router; + renderer = opts.renderer; +} \ No newline at end of file diff --git a/packages/kit/src/runtime/internal/start.js b/packages/kit/src/runtime/internal/start.js new file mode 100644 index 000000000000..955101a90b48 --- /dev/null +++ b/packages/kit/src/runtime/internal/start.js @@ -0,0 +1,33 @@ +import Root from 'ROOT'; +import { pages, ignore } from 'MANIFEST'; +import { Router } from './router'; +import { Renderer } from './renderer'; +import { init } from './singletons'; + +export async function start({ + base, + target, + session, + preloaded, + error, + status +}) { + const router = new Router({ + base, + pages, + ignore + }); + + const renderer = new Renderer({ + Root, + target, + preloaded, + error, + status, + session + }); + + init({ router, renderer }); + + await router.init({ renderer }); +} \ No newline at end of file diff --git a/packages/kit/src/runtime/navigation/utils.js b/packages/kit/src/runtime/internal/utils.js similarity index 100% rename from packages/kit/src/runtime/navigation/utils.js rename to packages/kit/src/runtime/internal/utils.js diff --git a/packages/kit/src/runtime/navigation/goto/index.js b/packages/kit/src/runtime/navigation/goto/index.js deleted file mode 100644 index f8f4de01ce65..000000000000 --- a/packages/kit/src/runtime/navigation/goto/index.js +++ /dev/null @@ -1,19 +0,0 @@ -import { cid, history, navigate, select_target } from '../internal'; -import { get_base_uri } from '../utils'; - -export default function goto( - href, - opts = { noscroll: false, replaceState: false } -) { - const target = select_target(new URL(href, get_base_uri(document))); - - if (target) { - history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href); - return navigate(target, null, opts.noscroll); - } - - location.href = href; - return new Promise(() => { - /* never resolves */ - }); -} diff --git a/packages/kit/src/runtime/navigation/index.js b/packages/kit/src/runtime/navigation/index.js deleted file mode 100644 index fb2ab4f8b033..000000000000 --- a/packages/kit/src/runtime/navigation/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export { default as goto } from './goto'; -export { default as prefetch } from './prefetch'; -export { default as prefetchRoutes } from './prefetchRoutes'; -export { default as start } from './start'; diff --git a/packages/kit/src/runtime/navigation/internal.js b/packages/kit/src/runtime/navigation/internal.js deleted file mode 100644 index dc65ffea3c2e..000000000000 --- a/packages/kit/src/runtime/navigation/internal.js +++ /dev/null @@ -1,215 +0,0 @@ -import { find_anchor } from './utils'; -import { ignore, routes } from 'MANIFEST'; - -export let uid = 1; -export function set_uid(n) { - uid = n; -} - -export let cid; -export function set_cid(n) { - cid = n; -} - -const _history = - typeof history !== 'undefined' - ? history - : { - pushState: () => {}, - replaceState: () => {}, - scrollRestoration: 'auto' - }; -export { _history as history }; - -export const scroll_history = {}; - -export async function load_current_page() { - const { hash, href } = location; - - _history.replaceState({ id: uid }, '', href); - - const target = select_target(new URL(location.href)); - if (target) return navigate(target, uid, true, hash); -} - -let base_url; -let handle_target; - -export function init(base, handler) { - base_url = base; - handle_target = handler; - - if ('scrollRestoration' in _history) { - _history.scrollRestoration = 'manual'; - } - - // Adopted from Nuxt.js - // Reset scrollRestoration to auto when leaving page, allowing page reload - // and back-navigation from other pages to use the browser to restore the - // scrolling position. - addEventListener('beforeunload', () => { - _history.scrollRestoration = 'auto'; - }); - - // Setting scrollRestoration to manual again when returning to this page. - addEventListener('load', () => { - _history.scrollRestoration = 'manual'; - }); - - addEventListener('click', handle_click); - addEventListener('popstate', handle_popstate); -} - -export function select_target(url) { - if (url.origin !== location.origin) return null; - if (!url.pathname.startsWith(base_url)) return null; - - let path = url.pathname.slice(base_url.length); - - if (path === '') { - path = '/'; - } - - // avoid accidental clashes between server routes and page routes - if (ignore.some(pattern => pattern.test(path))) return; - - for (let i = 0; i < routes.length; i += 1) { - const route = routes[i]; - - const match = route.pattern.exec(path); - - if (match) { - const query = new URLSearchParams(url.search); - const part = route.parts[route.parts.length - 1]; - const params = part.params ? part.params(match) : {}; - - const page = { host: location.host, path, query, params }; - - return { href: url.href, route, match, page }; - } - } -} - -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 || event.altKey) return; - if (event.defaultPrevented) return; - - const a = find_anchor(event.target); - if (!a) return; - - if (!a.href) 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 ? a.href.baseVal : a.href); - - if (href === location.href) { - if (!location.hash) 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 === location.pathname && url.search === location.search) return; - - const target = select_target(url); - if (target) { - const noscroll = a.hasAttribute('sapper:noscroll'); - navigate(target, null, noscroll, url.hash); - event.preventDefault(); - _history.pushState({ id: cid }, '', url.href); - } -} - -function which(event) { - return event.which === null ? event.button : event.which; -} - -function scroll_state() { - return { - x: pageXOffset, - y: pageYOffset - }; -} - -function handle_popstate(event) { - scroll_history[cid] = scroll_state(); - - if (event.state) { - const url = new URL(location.href); - const target = select_target(url); - if (target) { - navigate(target, event.state.id); - } else { - // eslint-disable-next-line - location.href = location.href; // nosonar - } - } else { - // hashchange - set_uid(uid + 1); - set_cid(uid); - _history.replaceState({ id: cid }, '', location.href); - } -} - -export async function navigate( - dest, - id, - noscroll, - hash -) { - const popstate = !!id; - if (popstate) { - cid = id; - } else { - const current_scroll = scroll_state(); - - // clicked on a link. preserve scroll state - scroll_history[cid] = current_scroll; - - cid = id = ++uid; - scroll_history[cid] = noscroll ? current_scroll : { x: 0, y: 0 }; - } - - await handle_target(dest); - if (document.activeElement && document.activeElement instanceof HTMLElement) { - document.activeElement.blur(); - } - - if (!noscroll) { - let scroll = scroll_history[id]; - - let deep_linked; - if (hash) { - // scroll is an element id (from a hash), we need to compute y. - deep_linked = document.getElementById(hash.slice(1)); - - if (deep_linked) { - scroll = { - x: 0, - y: deep_linked.getBoundingClientRect().top + scrollY - }; - } - } - - scroll_history[cid] = scroll; - if (popstate || deep_linked) { - scrollTo(scroll.x, scroll.y); - } else { - scrollTo(0, 0); - } - } -} diff --git a/packages/kit/src/runtime/navigation/prefetch/index.js b/packages/kit/src/runtime/navigation/prefetch/index.js deleted file mode 100644 index d6192b1dbb8e..000000000000 --- a/packages/kit/src/runtime/navigation/prefetch/index.js +++ /dev/null @@ -1,47 +0,0 @@ -import { hydrate_target } from '../start'; // TODO does this belong here? -import { select_target } from '../internal'; -import { find_anchor, get_base_uri } from '../utils'; - -let prefetching = null; - -let mousemove_timeout; - -export function start() { - addEventListener('touchstart', trigger_prefetch); - addEventListener('mousemove', handle_mousemove); -} - -export default function prefetch(href) { - const target = select_target(new URL(href, get_base_uri(document))); - - if (target) { - if (!prefetching || href !== prefetching.href) { - prefetching = { href, promise: hydrate_target(target) }; - } - - return prefetching.promise; - } -} - -export function get_prefetched(target) { - if (prefetching && prefetching.href === target.href) { - return prefetching.promise; - } else { - return hydrate_target(target); - } -} - -function trigger_prefetch(event) { - const a = find_anchor(event.target); - - if (a && a.rel === 'prefetch') { - prefetch(a.href); - } -} - -function handle_mousemove(event) { - clearTimeout(mousemove_timeout); - mousemove_timeout = setTimeout(() => { - trigger_prefetch(event); - }, 20); -} diff --git a/packages/kit/src/runtime/navigation/prefetchRoutes/index.js b/packages/kit/src/runtime/navigation/prefetchRoutes/index.js deleted file mode 100644 index ee6243379d4c..000000000000 --- a/packages/kit/src/runtime/navigation/prefetchRoutes/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import { components, routes } from 'MANIFEST'; - -export default async function prefetchRoutes(pathnames) { - const path_routes = pathnames - ? routes.filter((route) => pathnames.some((pathname) => route.pattern.test(pathname))) - : routes; - - const promises = path_routes.map((r) => Promise.all(r.parts.map((p) => p && components[p.i]()))); - - await Promise.all(promises); -} diff --git a/packages/kit/src/runtime/navigation/start/index.js b/packages/kit/src/runtime/navigation/start/index.js deleted file mode 100644 index 64eb63703cf3..000000000000 --- a/packages/kit/src/runtime/navigation/start/index.js +++ /dev/null @@ -1,262 +0,0 @@ -import { writable } from 'svelte/store'; -import { init as init_router, load_current_page, select_target } from '../internal'; -import { get_prefetched, start as start_prefetching } from '../prefetch'; -import goto from '../goto'; -import { page_store } from './page_store'; -import { layout, ErrorComponent, components } from 'MANIFEST'; -import root from 'ROOT'; - -let ready = false; -let root_component; -let current_token; -let initial_preloaded_data; -let root_preloaded; -let current_branch = []; -let current_query = '{}'; - -const stores = { - page: page_store({}), - preloading: writable(false), - session: writable(null) -}; - -let $session; -let session_dirty; - -stores.session.subscribe(async (value) => { - $session = value; - - if (!ready) return; - session_dirty = true; - - const dest = select_target(new URL(location.href)); - - const token = (current_token = {}); - const { redirect, props, branch } = await hydrate_target(dest); - if (token !== current_token) return; // a secondary navigation happened while we were loading - - if (redirect) { - await goto(redirect.location, { replaceState: true }); - } else { - await render(branch, props, buildPageContext(props, dest.page)); - } -}); - -export let target; -export function set_target(node) { - target = node; -} - -export default async function start(opts) { - set_target(opts.target); - - init_router(opts.baseUrl, handle_target); - - start_prefetching(); - - initial_preloaded_data = opts.preloaded; - root_preloaded = initial_preloaded_data[0]; - - stores.session.set(opts.session); - - if (opts.error) { - return handle_error(opts); - } - - return load_current_page(); -} - -function handle_error({ session, preloaded, status, error }) { - const { host, pathname, search } = location; - - const props = { - error, - status, - session, - level0: { - props: root_preloaded - }, - level1: { - props: { - status, - error - }, - component: ErrorComponent - }, - segments: preloaded - }; - const query = new URLSearchParams(search); - render([], props, { host, path: pathname, query, params: {}, error }); -} - -function buildPageContext(props, page) { - const { error } = props; - - return { error, ...page }; -} - -async function handle_target(dest) { - if (root_component) stores.preloading.set(true); - - const hydrating = get_prefetched(dest); - - const token = (current_token = {}); - const hydrated_target = await hydrating; - const { redirect } = hydrated_target; - if (token !== current_token) return; // a secondary navigation happened while we were loading - - if (redirect) { - await goto(redirect.location, { replaceState: true }); - } else { - const { props, branch } = hydrated_target; - await render(branch, props, buildPageContext(props, dest.page)); - } -} - -async function render(branch, props, page) { - stores.page.set(page); - stores.preloading.set(false); - - if (root_component) { - root_component.$set(props); - } else { - props.stores = { - page: { subscribe: stores.page.subscribe }, - preloading: { subscribe: stores.preloading.subscribe }, - session: stores.session - }; - props.level0 = { - props: await root_preloaded - }; - props.notify = stores.page.notify; - - root_component = new root({ - target, - props, - hydrate: true - }); - } - - current_branch = branch; - current_query = JSON.stringify(page.query); // TODO this is no good — URLSearchParams can't be serialized like that - ready = true; - session_dirty = false; -} - -function part_changed(i, segment, match, stringified_query) { - // TODO only check query string changes for preload functions - // that do in fact depend on it (using static analysis or - // runtime instrumentation) - if (stringified_query !== current_query) return true; - - const previous = current_branch[i]; - - if (!previous) return false; - if (segment !== previous.segment) return true; - if (previous.match) { - if (JSON.stringify(previous.match.slice(1, i + 2)) !== JSON.stringify(match.slice(1, i + 2))) { - return true; - } - } -} - -export async function hydrate_target(dest) { - const { route, page } = dest; - const segments = page.path.split('/').filter(Boolean); - - let redirect = null; - - const props = { error: null, status: 200, segments: [segments[0]] }; - - const preload_context = { - fetch: (url, opts) => fetch(url, opts), - redirect: (statusCode, location) => { - if (redirect && (redirect.statusCode !== statusCode || redirect.location !== location)) { - throw new Error('Conflicting redirects'); - } - redirect = { statusCode, location }; - }, - error: (status, error) => { - props.error = typeof error === 'string' ? new Error(error) : error; - props.status = status; - } - }; - - if (!root_preloaded) { - root_preloaded = - (layout.preload - ? layout.preload.call( - preload_context, - { - host: page.host, - path: page.path, - query: page.query, - params: {} - }, - $session - ) - : {}); - } - - let branch; - let l = 1; - - try { - const stringified_query = JSON.stringify(page.query); - const match = route.pattern.exec(page.path); - - let segment_dirty = false; - - branch = await Promise.all( - route.parts.map(async (part, i) => { - const segment = segments[i]; - - if (part_changed(i, segment, match, stringified_query)) segment_dirty = true; - - props.segments[l] = segments[i + 1]; // TODO make this less confusing - if (!part) return { segment }; - - const j = l++; - - if ( - !session_dirty && - !segment_dirty && - current_branch[i] && - current_branch[i].part === part.i - ) { - return current_branch[i]; - } - - segment_dirty = false; - - const { default: component, preload } = await components[part.i](); - - let preloaded; - if (ready || !initial_preloaded_data[i + 1]) { - preloaded = preload - ? await preload.call( - preload_context, - { - host: page.host, - path: page.path, - query: page.query, - params: part.params ? part.params(dest.match) : {} - }, - $session - ) - : {}; - } else { - preloaded = initial_preloaded_data[i + 1]; - } - - return (props[`level${j}`] = { component, props: preloaded, segment, match, part: part.i }); - }) - ); - } catch (error) { - props.error = error; - props.status = 500; - branch = []; - } - - return { redirect, props, branch }; -} diff --git a/packages/kit/src/runtime/navigation/start/page_store.js b/packages/kit/src/runtime/navigation/start/page_store.js deleted file mode 100644 index 9df1822c244a..000000000000 --- a/packages/kit/src/runtime/navigation/start/page_store.js +++ /dev/null @@ -1,28 +0,0 @@ -import { writable } from 'svelte/store'; - -/** Callback to inform of a value updates. */ -export function page_store(value) { - const store = writable(value); - let ready = true; - - function notify() { - ready = true; - store.update((val) => val); - } - - function set(new_value) { - ready = false; - store.set(new_value); - } - - function subscribe(run) { - let old_value; - return store.subscribe((new_value) => { - if (old_value === undefined || (ready && new_value !== old_value)) { - run((old_value = new_value)); - } - }); - } - - return { notify, set, subscribe }; -} diff --git a/packages/snowpack-config/snowpack.config.js b/packages/snowpack-config/snowpack.config.js index d3290f1ebcd6..8c9a1366af87 100644 --- a/packages/snowpack-config/snowpack.config.js +++ b/packages/snowpack-config/snowpack.config.js @@ -25,6 +25,6 @@ module.exports = { '.svelte/assets': '/_app/assets' }, alias: { - $app: './.svelte/assets/app' + $app: './.svelte/assets/runtime/app' } }; diff --git a/test/apps/basics/src/routes/$layout.svelte b/test/apps/basics/src/routes/$layout.svelte index 23ffdc3c9dca..793b485cb309 100644 --- a/test/apps/basics/src/routes/$layout.svelte +++ b/test/apps/basics/src/routes/$layout.svelte @@ -1,11 +1,11 @@ - + - \ No newline at end of file +
Custom layout
\ No newline at end of file diff --git a/test/runner.js b/test/runner.js index 32f26daa3fe2..6738b8a31a43 100644 --- a/test/runner.js +++ b/test/runner.js @@ -8,7 +8,7 @@ import * as assert from 'uvu/assert'; async function setup({ port }) { const browser = await chromium.launch(); const page = await browser.newPage(); - const defaultTimeout = 2000; + const defaultTimeout = 500; const text = async (selector) => page.textContent(selector, { timeout: defaultTimeout }); const wait_for_text = async (selector, expectedValue) => { From 0118b2aca638e6b5cd4db68451a6aa1c86b78b0f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 1 Dec 2020 20:16:06 -0500 Subject: [PATCH 02/15] most tests passing --- packages/kit/src/core/create_app.js | 18 +- packages/kit/src/renderer/page.js | 17 +- .../kit/src/runtime/app/navigation/index.js | 21 ++- .../src/runtime/internal/renderer/index.js | 161 +++++++++--------- .../kit/src/runtime/internal/router/index.js | 4 +- packages/kit/src/runtime/internal/utils.js | 11 -- .../basics/src/routes/routing/__tests__.js | 9 +- test/runner.js | 2 +- 8 files changed, 132 insertions(+), 111 deletions(-) diff --git a/packages/kit/src/core/create_app.js b/packages/kit/src/core/create_app.js index 67bceb05d214..2b452862bea9 100644 --- a/packages/kit/src/core/create_app.js +++ b/packages/kit/src/core/create_app.js @@ -146,7 +146,7 @@ function generate_app(manifest_data) { return ` diff --git a/packages/kit/src/renderer/page.js b/packages/kit/src/renderer/page.js index dd8ecee67253..8679efd5e939 100644 --- a/packages/kit/src/renderer/page.js +++ b/packages/kit/src/renderer/page.js @@ -160,16 +160,17 @@ async function get_response({ status, error, stores: { - page: readable({ - host: request.host, - path: request.path, - query: request.query, - params, - error - }, noop), - preloading: readable(null, noop), + page: writable(null), + preloading: writable(false), session: writable(session) }, + page: { + host: request.host, + path: request.path, + query: request.query, + params, + error + }, layout_props: preloaded[0], components: parts.slice(1).map(part => part.component) }; diff --git a/packages/kit/src/runtime/app/navigation/index.js b/packages/kit/src/runtime/app/navigation/index.js index a0fb6adb3914..3c5ae3f69c0c 100644 --- a/packages/kit/src/runtime/app/navigation/index.js +++ b/packages/kit/src/runtime/app/navigation/index.js @@ -1,11 +1,26 @@ import { router, renderer } from '../../internal/singletons'; +function get_base_uri(window_document) { + let baseURI = window_document.baseURI; + + if (!baseURI) { + const baseTags = window_document.getElementsByTagName('base'); + baseURI = baseTags.length ? baseTags[0].href : window_document.URL; + } + + return baseURI; +} + export function goto(href, { noscroll = false, replaceState = false } = {}) { - const page = router.select(new URL(href, get_base_uri(document))); + const url = new URL(href, get_base_uri(document)); + const page = router.select(url); if (page) { - // history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href); - return router.navigate(page, null, { noscroll, replaceState }); + // TODO this logic probably belongs inside router? cid should be private + history[replaceState ? 'replaceState' : 'pushState']({ id: router.cid }, '', href); + + // TODO shouldn't need to pass the hash here + return router.navigate(page, null, noscroll, url.hash); } location.href = href; diff --git a/packages/kit/src/runtime/internal/renderer/index.js b/packages/kit/src/runtime/internal/renderer/index.js index f3aeadc9406f..7ce411a53b1d 100644 --- a/packages/kit/src/runtime/internal/renderer/index.js +++ b/packages/kit/src/runtime/internal/renderer/index.js @@ -1,33 +1,6 @@ import { writable } from 'svelte/store'; import { find_anchor } from '../utils'; -// TODO this seems weird -function page_store(value) { - const store = writable(value); - let ready = true; - - function notify() { - ready = true; - store.update((val) => val); - } - - function set(new_value) { - ready = false; - store.set(new_value); - } - - function subscribe(run) { - let old_value; - return store.subscribe((new_value) => { - if (old_value === undefined || (ready && new_value !== old_value)) { - run((old_value = new_value)); - } - }); - } - - return { notify, set, subscribe }; -} - export class Renderer { constructor({ Root, @@ -49,7 +22,7 @@ export class Renderer { }; this.stores = { - page: page_store({}), + page: writable({}), preloading: writable(false), session: writable(session) }; @@ -102,53 +75,73 @@ export class Renderer { ready = true; } - async start(page) { - const props = { - stores: this.stores, - error: this.initial.error, - status: this.initial.status, - notify: this.stores.page.notify, // TODO this is weird - layout_props: this.initial.preloaded[0], // TODO or call preload, if serialisation failed - components: [] - }; - - let params = {}; - - if (!this.initial.error) { - try { - const promises = []; + async augment_props(props, page) { + try { + const promises = []; - const match = page.route.pattern.exec(page.page.path); + const match = page.route.pattern.exec(page.page.path); - page.route.parts.forEach(([loader, get_params], i) => { - const part_params = params = get_params ? get_params(match) : {}; + page.route.parts.forEach(([loader, get_params], i) => { + const part_params = props.params = get_params ? get_params(match) : {}; - promises.push(loader().then(async mod => { - props.components[i] = mod.default; + promises.push(loader().then(async mod => { + props.components[i] = mod.default; - if (mod.preload) { - props[`props_${i}`] = this.initial.preloaded[i + 1] || ( - await mod.preload({ + if (mod.preload) { + props[`props_${i}`] = (this.initial && this.initial.preloaded[i + 1]) || ( + await mod.preload.call( + { + fetch: (url, opts) => { + // TODO resolve against target URL? + return fetch(url, opts); + }, + redirect: (status, location) => { + // TODO handle redirects somehow + }, + error: (status, error) => { + if (typeof error === 'string') { + error = new Error(error); + } + + error.status = status; + throw error; + } + }, + { // TODO tidy this up ...page.page, params: part_params - }, this.$session) - ); - } - })); - }); - - await Promise.all(promises); - } catch (error) { - props.error = error; - props.status = error.status || 500; - } + }, + this.$session + ) + ); + } + })); + }); + + await Promise.all(promises); + } catch (error) { + props.error = error; + props.status = error.status || 500; } + } + + async start(page) { + const props = { + stores: this.stores, + error: this.initial.error, + status: this.initial.status, + layout_props: this.initial.preloaded[0], // TODO or call layout preload, if serialisation failed + components: [], + page: { + ...page.page, // TODO ugh + params: null + } + }; - this.stores.page.set({ - ...page.page, - params - }); // TODO need to rename some stuff + if (!this.initial.error) { + await this.augment_props(props, page); + } this.root = new this.Root({ target: this.target, @@ -162,37 +155,43 @@ export class Renderer { this.initial = null; } - async error(error, status) { - this.root.$set({ - error, - status - }); - - this.path = []; - } - async render(page) { - throw new Error('nope'); const token = this.token = {}; - this.stores.page.set(page); - this.stores.preloading.set(false); + this.stores.preloading.set(true); const props = { error: null, - status: 200 + status: 200, + components: [], + page: { + ...page.page, // TODO ugh + params: null + } }; - // TODO level1, level2 etc + await this.augment_props(props, page); if (this.token === token) { // check render wasn't aborted this.root.$set(props); // TODO set this.path (path through route DAG) so we can avoid updating unchanged branches // TODO set this.query, for the same reason + + this.stores.preloading.set(false); } } - async hydrate({ route, page, match }) { + // TODO is this used? + async error(error, status) { + this.root.$set({ + error, + status + }); + + this.path = []; + } + + async hydrate({ route, page }) { const segments = page.path.split('/').filter(Boolean); let redirect = null; diff --git a/packages/kit/src/runtime/internal/router/index.js b/packages/kit/src/runtime/internal/router/index.js index bb4cda2f7014..13edf183bcbc 100644 --- a/packages/kit/src/runtime/internal/router/index.js +++ b/packages/kit/src/runtime/internal/router/index.js @@ -19,6 +19,7 @@ export class Router { this.uid = 1; this.cid = null; + this.scroll_history = {}; this.history = window.history || { pushState: () => {}, @@ -93,7 +94,7 @@ export class Router { }); addEventListener('popstate', event => { - this.scroll_history[cid] = scroll_state(); + this.scroll_history[this.cid] = scroll_state(); if (event.state) { const url = new URL(location.href); @@ -116,6 +117,7 @@ export class Router { const { hash, href } = location; this.history.replaceState({ id: this.uid }, '', href); + this.scroll_history[this.uid] = scroll_state(); const page = this.select(new URL(location.href)); // if (page) return this.navigate(page, this.uid, true, hash); diff --git a/packages/kit/src/runtime/internal/utils.js b/packages/kit/src/runtime/internal/utils.js index 9d783122a9ee..c88eb542a17f 100644 --- a/packages/kit/src/runtime/internal/utils.js +++ b/packages/kit/src/runtime/internal/utils.js @@ -1,14 +1,3 @@ -export function get_base_uri(window_document) { - let baseURI = window_document.baseURI; - - if (!baseURI) { - const baseTags = window_document.getElementsByTagName('base'); - baseURI = baseTags.length ? baseTags[0].href : window_document.URL; - } - - return baseURI; -} - export function find_anchor(node) { while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG
elements have a lowercase name return node; diff --git a/test/apps/basics/src/routes/routing/__tests__.js b/test/apps/basics/src/routes/routing/__tests__.js index 5dc74e0a1ec8..bdacdb8bfac1 100644 --- a/test/apps/basics/src/routes/routing/__tests__.js +++ b/test/apps/basics/src/routes/routing/__tests__.js @@ -43,8 +43,13 @@ export default function (test) { }); test('navigates to a new page without reloading', async ({ - visit, text, prefetch_routes, capture_requests, click, wait_for_function - }) => { + visit, + text, + prefetch_routes, + capture_requests, + click, + wait_for_function + }) => { await visit('/routing/'); await prefetch_routes().catch(e => { diff --git a/test/runner.js b/test/runner.js index 6738b8a31a43..9f18ee62c563 100644 --- a/test/runner.js +++ b/test/runner.js @@ -52,7 +52,7 @@ async function setup({ port }) { // these are assumed to have been put in the global scope by the layout goto: (url) => page.evaluate((url) => goto(url), url), prefetch: (url) => page.evaluate((url) => prefetch(url), url), - click: (selector, options) => page.click(selector, options), + click: (selector, options) => page.click(selector, { timeout: defaultTimeout, ...options }), prefetch_routes: () => page.evaluate(() => prefetchRoutes()), wait_for_text, wait_for_selector: (selector, options) => From 04c40e5a6a8028abe21b59795b7c91f1bb2d6d81 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 1 Dec 2020 21:35:54 -0500 Subject: [PATCH 03/15] fix some stuff --- packages/kit/src/core/create_app.js | 19 +- packages/kit/src/renderer/page.js | 5 +- .../kit/src/runtime/app/navigation/index.js | 12 +- .../src/runtime/internal/renderer/index.js | 238 ++++++++++-------- packages/kit/src/runtime/internal/start.js | 3 +- 5 files changed, 148 insertions(+), 129 deletions(-) diff --git a/packages/kit/src/core/create_app.js b/packages/kit/src/core/create_app.js index 2b452862bea9..7a9f22988fc9 100644 --- a/packages/kit/src/core/create_app.js +++ b/packages/kit/src/core/create_app.js @@ -103,6 +103,8 @@ function generate_client_manifest(manifest_data) { } return ` + import * as layout from ${JSON.stringify(manifest_data.layout.url)}; + const components = ${components}; export const pages = ${pages}; @@ -110,6 +112,8 @@ function generate_client_manifest(manifest_data) { export const ignore = [ ${endpoints_to_ignore.map(route => route.pattern).join(',\n\t\t\t')} ]; + + export { layout }; ` .replace(/^\t{2}/gm, '') .trim(); @@ -123,15 +127,15 @@ function generate_app(manifest_data) { ); const levels = []; - for (let i = 0; i < max_depth; i += 1) { + for (let i = 0; i <= max_depth; i += 1) { levels.push(i); } - let l = max_depth - 1; + let l = max_depth; let pyramid = ``; - while (l-- > 0) { + while (l-- > 1) { pyramid = ` {#if components[${l + 1}]} @@ -147,20 +151,21 @@ function generate_app(manifest_data) { - + {#if error} {:else} diff --git a/packages/kit/src/renderer/page.js b/packages/kit/src/renderer/page.js index 8679efd5e939..4dc54468e17d 100644 --- a/packages/kit/src/renderer/page.js +++ b/packages/kit/src/renderer/page.js @@ -171,13 +171,12 @@ async function get_response({ params, error }, - layout_props: preloaded[0], - components: parts.slice(1).map(part => part.component) + components: parts.map(part => part.component) }; // leveln (instead of levels[n]) makes it easy to avoid // unnecessary updates for layout components - parts.slice(1).forEach((part, i) => { + parts.forEach((part, i) => { props[`props_${i}`] = part.props; }); diff --git a/packages/kit/src/runtime/app/navigation/index.js b/packages/kit/src/runtime/app/navigation/index.js index 3c5ae3f69c0c..26fdee0f1680 100644 --- a/packages/kit/src/runtime/app/navigation/index.js +++ b/packages/kit/src/runtime/app/navigation/index.js @@ -30,17 +30,7 @@ export function goto(href, { noscroll = false, replaceState = false } = {}) { } export function prefetch(href) { - const page = router.select(new URL(href, get_base_uri(document))); - - return renderer.prefetch(page); - - // if (page) { - // if (!prefetching || href !== prefetching.href) { - // prefetching = { href, promise: hydrate_target(page) }; - // } - - // return prefetching.promise; - // } + return renderer.prefetch(new URL(href, get_base_uri(document))); } export async function prefetchRoutes(pathnames) { diff --git a/packages/kit/src/runtime/internal/renderer/index.js b/packages/kit/src/runtime/internal/renderer/index.js index 7ce411a53b1d..6d76636a6bac 100644 --- a/packages/kit/src/runtime/internal/renderer/index.js +++ b/packages/kit/src/runtime/internal/renderer/index.js @@ -4,6 +4,7 @@ import { find_anchor } from '../utils'; export class Renderer { constructor({ Root, + layout, target, error, status, @@ -11,6 +12,7 @@ export class Renderer { session }) { this.Root = Root; + this.layout = layout; // TODO ideally we wouldn't need to store these... this.target = target; @@ -21,6 +23,13 @@ export class Renderer { status }; + this.current_branch = []; + + this.prefetching = { + href: null, + promise: null + }; + this.stores = { page: writable({}), preloading: writable(false), @@ -75,72 +84,74 @@ export class Renderer { ready = true; } - async augment_props(props, page) { - try { - const promises = []; - - const match = page.route.pattern.exec(page.page.path); - - page.route.parts.forEach(([loader, get_params], i) => { - const part_params = props.params = get_params ? get_params(match) : {}; - - promises.push(loader().then(async mod => { - props.components[i] = mod.default; - - if (mod.preload) { - props[`props_${i}`] = (this.initial && this.initial.preloaded[i + 1]) || ( - await mod.preload.call( - { - fetch: (url, opts) => { - // TODO resolve against target URL? - return fetch(url, opts); - }, - redirect: (status, location) => { - // TODO handle redirects somehow - }, - error: (status, error) => { - if (typeof error === 'string') { - error = new Error(error); - } - - error.status = status; - throw error; - } - }, - { - // TODO tidy this up - ...page.page, - params: part_params - }, - this.$session - ) - ); - } - })); - }); - - await Promise.all(promises); - } catch (error) { - props.error = error; - props.status = error.status || 500; - } - } + // async augment_props(props, page) { + // try { + // const promises = []; + + // const match = page.route.pattern.exec(page.page.path); + + // page.route.parts.forEach(([loader, get_params], i) => { + // const part_params = props.params = get_params ? get_params(match) : {}; + + // promises.push(loader().then(async mod => { + // props.components[i] = mod.default; + + // if (mod.preload) { + // props[`props_${i}`] = (this.initial && this.initial.preloaded[i + 1]) || ( + // await mod.preload.call( + // { + // fetch: (url, opts) => { + // // TODO resolve against target URL? + // return fetch(url, opts); + // }, + // redirect: (status, location) => { + // // TODO handle redirects somehow + // }, + // error: (status, error) => { + // if (typeof error === 'string') { + // error = new Error(error); + // } + + // error.status = status; + // throw error; + // } + // }, + // { + // // TODO tidy this up + // ...page.page, + // params: part_params + // }, + // this.$session + // ) + // ); + // } + // })); + // }); + + // await Promise.all(promises); + // } catch (error) { + // props.error = error; + // props.status = error.status || 500; + // } + // } async start(page) { const props = { stores: this.stores, error: this.initial.error, - status: this.initial.status, - layout_props: this.initial.preloaded[0], // TODO or call layout preload, if serialisation failed - components: [], - page: { - ...page.page, // TODO ugh - params: null - } + status: this.initial.status }; if (!this.initial.error) { - await this.augment_props(props, page); + const hydrated = await this.hydrate(page); + + if (hydrated.redirect) { + throw new Error('TODO client-side redirects'); + } + + Object.assign(props, hydrated.props); + this.current_branch = hydrated.branch; + this.current_query = hydrated.query; // TODO } this.root = new this.Root({ @@ -160,22 +171,13 @@ export class Renderer { this.stores.preloading.set(true); - const props = { - error: null, - status: 200, - components: [], - page: { - ...page.page, // TODO ugh - params: null - } - }; - - await this.augment_props(props, page); + const hydrated = await this.hydrate(page); if (this.token === token) { // check render wasn't aborted - this.root.$set(props); - // TODO set this.path (path through route DAG) so we can avoid updating unchanged branches - // TODO set this.query, for the same reason + this.root.$set(hydrated.props); + + this.current_branch = hydrated.branch; + this.current_query = hydrated.query; // TODO this.stores.preloading.set(false); } @@ -196,7 +198,15 @@ export class Renderer { let redirect = null; - const props = { error: null, status: 200, segments: [segments[0]] }; + const props = { + error: null, + status: 200, + components: [], + page: { + ...page, + params: null + } + }; const preload_context = { fetch: (url, opts) => fetch(url, opts), @@ -212,57 +222,54 @@ export class Renderer { } }; - if (!root_preloaded) { - root_preloaded = - (layout.preload - ? layout.preload.call( - preload_context, - { - host: page.host, - path: page.path, - query: page.query, - params: {} - }, - $session - ) - : {}); - } + const query = page.query.toString(); let branch; - let l = 1; try { - const stringified_query = JSON.stringify(page.query); const match = route.pattern.exec(page.path); let segment_dirty = false; - branch = await Promise.all( - route.parts.map(async (part, i) => { - const segment = segments[i]; + const part_changed = (i, segment, match) => { + // TODO only check query string changes for preload functions + // that do in fact depend on it (using static analysis or + // runtime instrumentation). Ditto for session + if (query !== this.current_query) return true; - if (part_changed(i, segment, match, stringified_query)) segment_dirty = true; + const previous = this.current_branch[i]; + + if (!previous) return false; + if (segment !== previous.segment) return true; + if (previous.match) { + // TODO what the hell is this + if (JSON.stringify(previous.match.slice(1, i + 2)) !== JSON.stringify(match.slice(1, i + 2))) { + return true; + } + } + }; - props.segments[l] = segments[i + 1]; // TODO make this less confusing - if (!part) return { segment }; + branch = await Promise.all( + [[() => this.layout, () => {}], ...route.parts].map(async (part, i) => { + const segment = segments[i]; - const j = l++; + if (part_changed(i, segment, match)) segment_dirty = true; if ( - !session_dirty && + !this.session_dirty && !segment_dirty && - current_branch[i] && - current_branch[i].part === part.i + this.current_branch[i] && + this.current_branch[i].part === part.i ) { - return current_branch[i]; + return this.current_branch[i]; } segment_dirty = false; - const { default: component, preload } = await components[part.i](); + const { default: component, preload } = await part[0](); let preloaded; - if (ready || !initial_preloaded_data[i + 1]) { + if (!this.initial || !this.initial.preloaded[i]) { preloaded = preload ? await preload.call( preload_context, @@ -272,14 +279,17 @@ export class Renderer { query: page.query, params: part[1] ? part[1](match) : {} }, - $session + this.$session ) : {}; } else { - preloaded = initial_preloaded_data[i + 1]; + preloaded = this.initial.preloaded[i]; } - return (props[`level${j}`] = { component, props: preloaded, segment, match, part: part.i }); + props.components[i] = component; + props[`props_${i}`] = preloaded; + + return { component, props: preloaded, segment, match, part: part.i }; }) ); } catch (error) { @@ -288,6 +298,20 @@ export class Renderer { branch = []; } - return { redirect, props, branch }; + return { redirect, props, branch, query }; + } + + async prefetch(url) { + const page = this.router.select(url); + + if (page) { + if (url.href !== this.prefetching.href) { + this.prefetching = { href: url.href, promise: this.hydrate(page) }; + } + + return this.prefetching.promise; + } else { + throw new Error(`Could not prefetch ${url.href}`); + } } } \ No newline at end of file diff --git a/packages/kit/src/runtime/internal/start.js b/packages/kit/src/runtime/internal/start.js index 955101a90b48..47bdcb407587 100644 --- a/packages/kit/src/runtime/internal/start.js +++ b/packages/kit/src/runtime/internal/start.js @@ -1,5 +1,5 @@ import Root from 'ROOT'; -import { pages, ignore } from 'MANIFEST'; +import { pages, ignore, layout } from 'MANIFEST'; import { Router } from './router'; import { Renderer } from './renderer'; import { init } from './singletons'; @@ -20,6 +20,7 @@ export async function start({ const renderer = new Renderer({ Root, + layout, target, preloaded, error, From a62de85e95b0f25b48bd3205479097875e7b8fac Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 1 Dec 2020 21:46:39 -0500 Subject: [PATCH 04/15] fix --- packages/kit/src/runtime/internal/renderer/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/runtime/internal/renderer/index.js b/packages/kit/src/runtime/internal/renderer/index.js index 6d76636a6bac..111c602b10d3 100644 --- a/packages/kit/src/runtime/internal/renderer/index.js +++ b/packages/kit/src/runtime/internal/renderer/index.js @@ -259,7 +259,7 @@ export class Renderer { !this.session_dirty && !segment_dirty && this.current_branch[i] && - this.current_branch[i].part === part.i + this.current_branch[i].part === part[0] ) { return this.current_branch[i]; } @@ -289,7 +289,7 @@ export class Renderer { props.components[i] = component; props[`props_${i}`] = preloaded; - return { component, props: preloaded, segment, match, part: part.i }; + return { component, props: preloaded, segment, match, part: part[0] }; }) ); } catch (error) { From b4c0dc47f0fe4e0b69497505d671e9444afecdd2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 1 Dec 2020 23:06:39 -0500 Subject: [PATCH 05/15] all tests but one passing --- packages/kit/src/core/create_app.js | 10 +--- packages/kit/src/runtime/app/stores/index.js | 14 ++++- .../src/runtime/internal/renderer/index.js | 53 +++++++++---------- .../src/routes/routing/[...rest]/index.svelte | 2 +- .../apps/basics/src/routes/store/__tests__.js | 5 +- .../apps/basics/src/routes/store/index.svelte | 11 +++- test/runner.js | 1 + 7 files changed, 55 insertions(+), 41 deletions(-) diff --git a/packages/kit/src/core/create_app.js b/packages/kit/src/core/create_app.js index 7a9f22988fc9..3b6963349b4e 100644 --- a/packages/kit/src/core/create_app.js +++ b/packages/kit/src/core/create_app.js @@ -166,15 +166,7 @@ function generate_app(manifest_data) { const Layout = components[0]; - setContext('__svelte__', { - page: { - subscribe: stores.page.subscribe - }, - preloading: { - subscribe: stores.preloading.subscribe - }, - session: stores.session - }); + setContext('__svelte__', stores); $: stores.page.set(page); diff --git a/packages/kit/src/runtime/app/stores/index.js b/packages/kit/src/runtime/app/stores/index.js index 3abf1d61c46d..278647620253 100644 --- a/packages/kit/src/runtime/app/stores/index.js +++ b/packages/kit/src/runtime/app/stores/index.js @@ -3,7 +3,19 @@ import { getContext } from 'svelte'; // const ssr = (import.meta as any).env.SSR; const ssr = typeof window === 'undefined'; // TODO why doesn't previous line work in build? -export const getStores = () => getContext('__svelte__'); +export const getStores = () => { + const stores = getContext('__svelte__'); + + return { + page: { + subscribe: stores.page.subscribe + }, + preloading: { + subscribe: stores.preloading.subscribe + }, + session: stores.session + } +}; export const page = { subscribe(fn) { diff --git a/packages/kit/src/runtime/internal/renderer/index.js b/packages/kit/src/runtime/internal/renderer/index.js index 111c602b10d3..8fd00ea581bb 100644 --- a/packages/kit/src/runtime/internal/renderer/index.js +++ b/packages/kit/src/runtime/internal/renderer/index.js @@ -67,19 +67,8 @@ export class Renderer { if (!ready) return; this.session_dirty = true; - await this.render(this.page); - - const dest = select_target(new URL(location.href)); - - const token = (current_token = {}); - const { redirect, props, branch } = await hydrate_target(dest); - if (token !== current_token) return; // a secondary navigation happened while we were loading - - if (redirect) { - await goto(redirect.location, { replaceState: true }); - } else { - await render(branch, props, buildPageContext(props, dest.page)); - } + const page = this.router.select(new URL(location.href)); + this.render(page); }); ready = true; } @@ -151,7 +140,8 @@ export class Renderer { Object.assign(props, hydrated.props); this.current_branch = hydrated.branch; - this.current_query = hydrated.query; // TODO + this.current_query = hydrated.query; + this.current_path = hydrated.path; } this.root = new this.Root({ @@ -174,10 +164,11 @@ export class Renderer { const hydrated = await this.hydrate(page); if (this.token === token) { // check render wasn't aborted - this.root.$set(hydrated.props); - this.current_branch = hydrated.branch; - this.current_query = hydrated.query; // TODO + this.current_query = hydrated.query; + this.current_path = hydrated.path; + + this.root.$set(hydrated.props); this.stores.preloading.set(false); } @@ -201,11 +192,7 @@ export class Renderer { const props = { error: null, status: 200, - components: [], - page: { - ...page, - params: null - } + components: [] }; const preload_context = { @@ -250,7 +237,7 @@ export class Renderer { }; branch = await Promise.all( - [[() => this.layout, () => {}], ...route.parts].map(async (part, i) => { + [[() => this.layout, () => {}], ...route.parts].map(async ([loader, get_params], i) => { const segment = segments[i]; if (part_changed(i, segment, match)) segment_dirty = true; @@ -259,14 +246,16 @@ export class Renderer { !this.session_dirty && !segment_dirty && this.current_branch[i] && - this.current_branch[i].part === part[0] + this.current_branch[i].loader === loader ) { return this.current_branch[i]; } segment_dirty = false; - const { default: component, preload } = await part[0](); + const { default: component, preload } = await loader(); + + const params = get_params ? get_params(match) : {}; let preloaded; if (!this.initial || !this.initial.preloaded[i]) { @@ -277,7 +266,7 @@ export class Renderer { host: page.host, path: page.path, query: page.query, - params: part[1] ? part[1](match) : {} + params }, this.$session ) @@ -289,16 +278,24 @@ export class Renderer { props.components[i] = component; props[`props_${i}`] = preloaded; - return { component, props: preloaded, segment, match, part: part[0] }; + return { component, params, props: preloaded, segment, match, loader }; }) ); + + if (page.path !== this.current_path) { + console.trace('>>>here', page.path, this.current_path); + props.page = { + ...page, + params: branch[branch.length - 1].params + }; + } } catch (error) { props.error = error; props.status = 500; branch = []; } - return { redirect, props, branch, query }; + return { redirect, props, branch, query, path: page.path }; } async prefetch(url) { diff --git a/test/apps/basics/src/routes/routing/[...rest]/index.svelte b/test/apps/basics/src/routes/routing/[...rest]/index.svelte index dbe813c3a083..af2c76315ed7 100644 --- a/test/apps/basics/src/routes/routing/[...rest]/index.svelte +++ b/test/apps/basics/src/routes/routing/[...rest]/index.svelte @@ -1,5 +1,5 @@

Test

Called {call_count} time

results + +{#if $page.path === '/store/result'} + {console.log(window.oops = 'this should not happen')} +{/if} \ No newline at end of file diff --git a/test/runner.js b/test/runner.js index 9f18ee62c563..db0b586938d8 100644 --- a/test/runner.js +++ b/test/runner.js @@ -43,6 +43,7 @@ async function setup({ port }) { return { base, + page, visit: path => page.goto(base + path), contains: async str => (await page.innerHTML('body')).includes(str), html: async selector => await page.innerHTML(selector, { timeout: defaultTimeout }), From 90a3d66e91c876ce16f16c2818381ed72410c578 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 1 Dec 2020 23:52:58 -0500 Subject: [PATCH 06/15] get build tests running again --- packages/kit/src/api/build/index.js | 2 +- packages/kit/src/runtime/internal/renderer/index.js | 1 - test/apps/basics/src/routes/middleware/{index.js => index_.js} | 0 test/apps/basics/src/routes/store/index.svelte | 1 - 4 files changed, 1 insertion(+), 3 deletions(-) rename test/apps/basics/src/routes/middleware/{index.js => index_.js} (100%) diff --git a/packages/kit/src/api/build/index.js b/packages/kit/src/api/build/index.js index 0e26cac84701..947bc8a7a05d 100644 --- a/packages/kit/src/api/build/index.js +++ b/packages/kit/src/api/build/index.js @@ -87,7 +87,7 @@ export async function build(config) { deps: {} }; - const entry = path.resolve(`${unoptimized}/client/_app/assets/app/navigation.js`); + const entry = path.resolve(`${unoptimized}/client/_app/assets/runtime/internal/start.js`); // https://github.com/snowpackjs/snowpack/discussions/1395 const re = /(\.\.\/)+_app\/assets\/app\//; diff --git a/packages/kit/src/runtime/internal/renderer/index.js b/packages/kit/src/runtime/internal/renderer/index.js index 8fd00ea581bb..89e8fa8eb67b 100644 --- a/packages/kit/src/runtime/internal/renderer/index.js +++ b/packages/kit/src/runtime/internal/renderer/index.js @@ -283,7 +283,6 @@ export class Renderer { ); if (page.path !== this.current_path) { - console.trace('>>>here', page.path, this.current_path); props.page = { ...page, params: branch[branch.length - 1].params diff --git a/test/apps/basics/src/routes/middleware/index.js b/test/apps/basics/src/routes/middleware/index_.js similarity index 100% rename from test/apps/basics/src/routes/middleware/index.js rename to test/apps/basics/src/routes/middleware/index_.js diff --git a/test/apps/basics/src/routes/store/index.svelte b/test/apps/basics/src/routes/store/index.svelte index 3bc68a985916..23f542539519 100644 --- a/test/apps/basics/src/routes/store/index.svelte +++ b/test/apps/basics/src/routes/store/index.svelte @@ -11,7 +11,6 @@ }); const unsubscribe = page.subscribe($page => { - console.trace('call_count++', $page); call_count++; session.set(call_count); }); From 335f59fccc613569718b7197d8c4b1c68c94022f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 1 Dec 2020 23:58:35 -0500 Subject: [PATCH 07/15] remove unused code --- .../src/runtime/internal/renderer/index.js | 64 ------------------- 1 file changed, 64 deletions(-) diff --git a/packages/kit/src/runtime/internal/renderer/index.js b/packages/kit/src/runtime/internal/renderer/index.js index 89e8fa8eb67b..f8c04e693485 100644 --- a/packages/kit/src/runtime/internal/renderer/index.js +++ b/packages/kit/src/runtime/internal/renderer/index.js @@ -73,57 +73,6 @@ export class Renderer { ready = true; } - // async augment_props(props, page) { - // try { - // const promises = []; - - // const match = page.route.pattern.exec(page.page.path); - - // page.route.parts.forEach(([loader, get_params], i) => { - // const part_params = props.params = get_params ? get_params(match) : {}; - - // promises.push(loader().then(async mod => { - // props.components[i] = mod.default; - - // if (mod.preload) { - // props[`props_${i}`] = (this.initial && this.initial.preloaded[i + 1]) || ( - // await mod.preload.call( - // { - // fetch: (url, opts) => { - // // TODO resolve against target URL? - // return fetch(url, opts); - // }, - // redirect: (status, location) => { - // // TODO handle redirects somehow - // }, - // error: (status, error) => { - // if (typeof error === 'string') { - // error = new Error(error); - // } - - // error.status = status; - // throw error; - // } - // }, - // { - // // TODO tidy this up - // ...page.page, - // params: part_params - // }, - // this.$session - // ) - // ); - // } - // })); - // }); - - // await Promise.all(promises); - // } catch (error) { - // props.error = error; - // props.status = error.status || 500; - // } - // } - async start(page) { const props = { stores: this.stores, @@ -150,9 +99,6 @@ export class Renderer { hydrate: true }); - // TODO set this.path (path through route DAG) so we can avoid updating unchanged branches - // TODO set this.query, for the same reason - this.initial = null; } @@ -174,16 +120,6 @@ export class Renderer { } } - // TODO is this used? - async error(error, status) { - this.root.$set({ - error, - status - }); - - this.path = []; - } - async hydrate({ route, page }) { const segments = page.path.split('/').filter(Boolean); From 7eda9774ab7f4d37efdd2f852a0a26308181831d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 2 Dec 2020 00:01:08 -0500 Subject: [PATCH 08/15] get_params is optional --- packages/kit/src/runtime/internal/renderer/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/runtime/internal/renderer/index.js b/packages/kit/src/runtime/internal/renderer/index.js index f8c04e693485..3aaa948f32f3 100644 --- a/packages/kit/src/runtime/internal/renderer/index.js +++ b/packages/kit/src/runtime/internal/renderer/index.js @@ -173,7 +173,7 @@ export class Renderer { }; branch = await Promise.all( - [[() => this.layout, () => {}], ...route.parts].map(async ([loader, get_params], i) => { + [[() => this.layout], ...route.parts].map(async ([loader, get_params], i) => { const segment = segments[i]; if (part_changed(i, segment, match)) segment_dirty = true; From 33f79f6537f3f1464f477ff427bf2f5134b6b705 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 2 Dec 2020 00:11:31 -0500 Subject: [PATCH 09/15] lint --- packages/kit/.eslintrc.json | 7 ++ packages/kit/package.json | 3 +- packages/kit/rollup.config.js | 2 +- packages/kit/src/api/build/index.js | 2 +- packages/kit/src/api/dev/loader.js | 2 +- packages/kit/src/api/index.js | 2 +- packages/kit/src/api/load_config/index.js | 2 +- packages/kit/src/api/start/index.js | 2 +- packages/kit/src/api/utils.js | 2 +- .../core/test/create_manifest_data.spec.js | 2 +- packages/kit/src/renderer/page.js | 4 +- .../kit/src/runtime/app/navigation/index.js | 2 +- packages/kit/src/runtime/app/stores/index.js | 2 +- .../src/runtime/internal/renderer/index.js | 12 +-- .../kit/src/runtime/internal/router/index.js | 8 +- .../kit/src/runtime/internal/singletons.js | 2 +- packages/kit/src/runtime/internal/start.js | 2 +- pnpm-lock.yaml | 88 ++++++++++++++++++- 18 files changed, 117 insertions(+), 29 deletions(-) create mode 100644 packages/kit/.eslintrc.json diff --git a/packages/kit/.eslintrc.json b/packages/kit/.eslintrc.json new file mode 100644 index 000000000000..11dde8fbdc5e --- /dev/null +++ b/packages/kit/.eslintrc.json @@ -0,0 +1,7 @@ +{ + "root": true, + "extends": "@sveltejs", + "parserOptions": { + "sourceType": "module" + } +} \ No newline at end of file diff --git a/packages/kit/package.json b/packages/kit/package.json index 61dd81552d4a..cfed59bedf82 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -17,6 +17,7 @@ "@types/node": "^14.11.10", "@types/rimraf": "^3.0.0", "@types/sade": "^1.7.2", + "eslint": "^7.14.0", "esm": "^3.2.25", "estree-walker": "^2.0.1", "kleur": "^4.1.3", @@ -43,7 +44,7 @@ "scripts": { "dev": "rollup -cw", "build": "rollup -c", - "lint": "eslint --ignore-pattern node_modules/ --ignore-pattern dist/ \"**/*.{ts,mjs,js,svelte}\" && npm run check-format", + "lint": "eslint --ignore-pattern node_modules/ --ignore-pattern dist/ --ignore-pattern assets/ \"**/*.{ts,mjs,js,svelte}\" && npm run check-format", "format": "prettier --write . --config ../../.prettierrc --ignore-path .gitignore", "check-format": "prettier --check . --config ../../.prettierrc --ignore-path .gitignore", "prepublishOnly": "npm run build", diff --git a/packages/kit/rollup.config.js b/packages/kit/rollup.config.js index 66fa4c932e26..c0472f11723a 100644 --- a/packages/kit/rollup.config.js +++ b/packages/kit/rollup.config.js @@ -71,4 +71,4 @@ export default [ ], preserveEntrySignatures: true } -]; \ No newline at end of file +]; diff --git a/packages/kit/src/api/build/index.js b/packages/kit/src/api/build/index.js index 947bc8a7a05d..2fb97df1b6e9 100644 --- a/packages/kit/src/api/build/index.js +++ b/packages/kit/src/api/build/index.js @@ -67,7 +67,7 @@ export async function build(config) { fs.writeFileSync(setup_file, ''); } - const mount = `--mount.${config.paths.routes}=/_app/routes --mount.${config.paths.setup}=/_app/setup` + const mount = `--mount.${config.paths.routes}=/_app/routes --mount.${config.paths.setup}=/_app/setup`; await exec(`node ${snowpack_bin} build ${mount} --out=${unoptimized}/server --ssr`); log.success('server'); diff --git a/packages/kit/src/api/dev/loader.js b/packages/kit/src/api/dev/loader.js index d07e2b08fb73..dd46617eb9bc 100644 --- a/packages/kit/src/api/dev/loader.js +++ b/packages/kit/src/api/dev/loader.js @@ -160,7 +160,7 @@ export default function loader(snowpack, config) { if (node.type === 'MetaProperty' && node.meta.name === 'import') { code.overwrite(node.start, node.end, '__importmeta__'); } else if (node.type === 'ImportExpression') { - code.overwrite(node.start, node.start + 6, `__import__`); + code.overwrite(node.start, node.start + 6, '__import__'); } } }); diff --git a/packages/kit/src/api/index.js b/packages/kit/src/api/index.js index 6047755520f2..fc504eb68e8c 100644 --- a/packages/kit/src/api/index.js +++ b/packages/kit/src/api/index.js @@ -1,4 +1,4 @@ export { dev } from './dev'; export { build } from './build'; export { start } from './start'; -export { load_config } from './load_config'; \ No newline at end of file +export { load_config } from './load_config'; diff --git a/packages/kit/src/api/load_config/index.js b/packages/kit/src/api/load_config/index.js index 0c935659cfae..902195ac0ff0 100644 --- a/packages/kit/src/api/load_config/index.js +++ b/packages/kit/src/api/load_config/index.js @@ -21,4 +21,4 @@ export function load_config({ cwd = process.cwd() } = {}) { ...config.paths } }; -} \ No newline at end of file +} diff --git a/packages/kit/src/api/start/index.js b/packages/kit/src/api/start/index.js index ea850a2f6701..599f7bf81c20 100644 --- a/packages/kit/src/api/start/index.js +++ b/packages/kit/src/api/start/index.js @@ -53,4 +53,4 @@ export function start({ port }) { fulfil(server); }); }); -} \ No newline at end of file +} diff --git a/packages/kit/src/api/utils.js b/packages/kit/src/api/utils.js index 9730600d8b12..3ad3357a981d 100644 --- a/packages/kit/src/api/utils.js +++ b/packages/kit/src/api/utils.js @@ -2,5 +2,5 @@ import { resolve } from 'path'; import { copy } from '@sveltejs/app-utils/files'; export function copy_assets() { - copy(resolve(__dirname, `../assets`), '.svelte/assets'); + copy(resolve(__dirname, '../assets'), '.svelte/assets'); } diff --git a/packages/kit/src/core/test/create_manifest_data.spec.js b/packages/kit/src/core/test/create_manifest_data.spec.js index 771eefc35cd4..bef31ada404c 100644 --- a/packages/kit/src/core/test/create_manifest_data.spec.js +++ b/packages/kit/src/core/test/create_manifest_data.spec.js @@ -280,4 +280,4 @@ test('works with custom extensions' , () => { ]); }); -test.run(); \ No newline at end of file +test.run(); diff --git a/packages/kit/src/renderer/page.js b/packages/kit/src/renderer/page.js index 4dc54468e17d..b5663ca9f365 100644 --- a/packages/kit/src/renderer/page.js +++ b/packages/kit/src/renderer/page.js @@ -2,12 +2,10 @@ import { createReadStream, existsSync } from 'fs'; import * as mime from 'mime'; import fetch, { Response } from 'node-fetch'; -import { readable, writable } from 'svelte/store'; +import { writable } from 'svelte/store'; import { parse, resolve, URLSearchParams } from 'url'; import { render } from './index'; -const noop = () => {}; - async function get_response({ request, options, diff --git a/packages/kit/src/runtime/app/navigation/index.js b/packages/kit/src/runtime/app/navigation/index.js index 26fdee0f1680..8bcea06b98a8 100644 --- a/packages/kit/src/runtime/app/navigation/index.js +++ b/packages/kit/src/runtime/app/navigation/index.js @@ -41,4 +41,4 @@ export async function prefetchRoutes(pathnames) { const promises = path_routes.map((r) => Promise.all(r.parts.map((p) => p[0]()))); await Promise.all(promises); -} \ No newline at end of file +} diff --git a/packages/kit/src/runtime/app/stores/index.js b/packages/kit/src/runtime/app/stores/index.js index 278647620253..64982b7c95d3 100644 --- a/packages/kit/src/runtime/app/stores/index.js +++ b/packages/kit/src/runtime/app/stores/index.js @@ -14,7 +14,7 @@ export const getStores = () => { subscribe: stores.preloading.subscribe }, session: stores.session - } + }; }; export const page = { diff --git a/packages/kit/src/runtime/internal/renderer/index.js b/packages/kit/src/runtime/internal/renderer/index.js index 3aaa948f32f3..f963c1f59176 100644 --- a/packages/kit/src/runtime/internal/renderer/index.js +++ b/packages/kit/src/runtime/internal/renderer/index.js @@ -41,21 +41,21 @@ export class Renderer { this.root = null; - function trigger_prefetch(event) { + const trigger_prefetch = (event) => { const a = find_anchor(event.target); if (a && a.rel === 'prefetch') { // TODO make this svelte-prefetch or something - prefetch(a.href); + this.prefetch(new URL(a.href)); } - } + }; let mousemove_timeout; - function handle_mousemove(event) { + const handle_mousemove = (event) => { clearTimeout(mousemove_timeout); mousemove_timeout = setTimeout(() => { trigger_prefetch(event); }, 20); - } + }; addEventListener('touchstart', trigger_prefetch); addEventListener('mousemove', handle_mousemove); @@ -246,4 +246,4 @@ export class Renderer { throw new Error(`Could not prefetch ${url.href}`); } } -} \ No newline at end of file +} diff --git a/packages/kit/src/runtime/internal/router/index.js b/packages/kit/src/runtime/internal/router/index.js index 13edf183bcbc..0888a0cac7ef 100644 --- a/packages/kit/src/runtime/internal/router/index.js +++ b/packages/kit/src/runtime/internal/router/index.js @@ -1,4 +1,4 @@ -import { find_anchor } from "../utils"; +import { find_anchor } from '../utils'; function which(event) { return event.which === null ? event.button : event.which; @@ -114,9 +114,7 @@ export class Router { }); // load current page - const { hash, href } = location; - - this.history.replaceState({ id: this.uid }, '', href); + this.history.replaceState({ id: this.uid }, '', location.href); this.scroll_history[this.uid] = scroll_state(); const page = this.select(new URL(location.href)); @@ -201,4 +199,4 @@ export class Router { } } } -} \ No newline at end of file +} diff --git a/packages/kit/src/runtime/internal/singletons.js b/packages/kit/src/runtime/internal/singletons.js index a1c69c4be56b..454c334888cb 100644 --- a/packages/kit/src/runtime/internal/singletons.js +++ b/packages/kit/src/runtime/internal/singletons.js @@ -4,4 +4,4 @@ export let renderer; export function init(opts) { router = opts.router; renderer = opts.renderer; -} \ No newline at end of file +} diff --git a/packages/kit/src/runtime/internal/start.js b/packages/kit/src/runtime/internal/start.js index 47bdcb407587..97b0bda2c780 100644 --- a/packages/kit/src/runtime/internal/start.js +++ b/packages/kit/src/runtime/internal/start.js @@ -31,4 +31,4 @@ export async function start({ init({ router, renderer }); await router.init({ renderer }); -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be89eda95fe8..5594056d507d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -169,6 +169,7 @@ importers: '@types/node': 14.11.10 '@types/rimraf': 3.0.0 '@types/sade': 1.7.2 + eslint: 7.14.0 esm: 3.2.25 estree-walker: 2.0.1 kleur: 4.1.3 @@ -189,6 +190,7 @@ importers: '@types/rimraf': ^3.0.0 '@types/sade': ^1.7.2 cheap-watch: ^1.0.2 + eslint: ^7.14.0 esm: ^3.2.25 estree-walker: ^2.0.1 http-proxy: ^1.18.1 @@ -461,6 +463,23 @@ packages: node: ^10.12.0 || >=12.0.0 resolution: integrity: sha512-4YVwPkANLeNtRjMekzux1ci8hIaH5eGKktGqR0d3LWsKNn5B2X/1Z6Trxy7jQXl9EBGE6Yj02O+t09FMeRllaA== + /@eslint/eslintrc/0.2.1: + dependencies: + ajv: 6.12.6 + debug: 4.3.1 + espree: 7.3.0 + globals: 12.4.0 + ignore: 4.0.6 + import-fresh: 3.2.2 + js-yaml: 3.14.0 + lodash: 4.17.20 + minimatch: 3.0.4 + strip-json-comments: 3.1.1 + dev: true + engines: + node: ^10.12.0 || >=12.0.0 + resolution: + integrity: sha512-XRUeBZ5zBWLYgSANMpThFddrZZkEbGHgUdt5UJjZfnlN9BGCiUBrf+nvbRupSjMvqzwnQN0qwCmOxITt1cfywA== /@manypkg/find-root/1.1.0: dependencies: '@babel/runtime': 7.12.1 @@ -1768,6 +1787,51 @@ packages: hasBin: true resolution: integrity: sha512-G9+qtYVCHaDi1ZuWzBsOWo2wSwd70TXnU6UHA3cTYHp7gCTXZcpggWFoUVAMRarg68qtPoNfFbzPh+VdOgmwmw== + /eslint/7.14.0: + dependencies: + '@babel/code-frame': 7.10.4 + '@eslint/eslintrc': 0.2.1 + ajv: 6.12.6 + chalk: 4.1.0 + cross-spawn: 7.0.3 + debug: 4.3.1 + doctrine: 3.0.0 + enquirer: 2.3.6 + eslint-scope: 5.1.1 + eslint-utils: 2.1.0 + eslint-visitor-keys: 2.0.0 + espree: 7.3.0 + esquery: 1.3.1 + esutils: 2.0.3 + file-entry-cache: 5.0.1 + functional-red-black-tree: 1.0.1 + glob-parent: 5.1.1 + globals: 12.4.0 + ignore: 4.0.6 + import-fresh: 3.2.2 + imurmurhash: 0.1.4 + is-glob: 4.0.1 + js-yaml: 3.14.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash: 4.17.20 + minimatch: 3.0.4 + natural-compare: 1.4.0 + optionator: 0.9.1 + progress: 2.0.3 + regexpp: 3.1.0 + semver: 7.3.4 + strip-ansi: 6.0.0 + strip-json-comments: 3.1.1 + table: 5.4.6 + text-table: 0.2.0 + v8-compile-cache: 2.2.0 + dev: true + engines: + node: ^10.12.0 || >=12.0.0 + hasBin: true + resolution: + integrity: sha512-5YubdnPXrlrYAFCKybPuHIAH++PINe1pmKNc5wQRB9HSbqIK1ywAnntE3Wwua4giKu0bjligf1gLF6qxMGOYRA== /esm/3.2.25: dev: true engines: @@ -2268,6 +2332,15 @@ packages: node: '>=6' resolution: integrity: sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== + /import-fresh/3.2.2: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: true + engines: + node: '>=6' + resolution: + integrity: sha512-cTPNrlvJT6twpYy+YmKUKrTSjWFs3bjYjAhCwm+z4EOCubZxAuO+hHpRN64TqjEaYSHs7tJAE0w1CKMGmsG/lw== /imurmurhash/0.1.4: engines: node: '>=0.8.19' @@ -2604,7 +2677,6 @@ packages: /lru-cache/6.0.0: dependencies: yallist: 4.0.0 - dev: false engines: node: '>=10' resolution: @@ -3555,6 +3627,15 @@ packages: hasBin: true resolution: integrity: sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== + /semver/7.3.4: + dependencies: + lru-cache: 6.0.0 + dev: true + engines: + node: '>=10' + hasBin: true + resolution: + integrity: sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== /serialize-javascript/4.0.0: dependencies: randombytes: 2.1.0 @@ -4114,6 +4195,10 @@ packages: dev: true resolution: integrity: sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ== + /v8-compile-cache/2.2.0: + dev: true + resolution: + integrity: sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q== /validate-npm-package-license/3.0.4: dependencies: spdx-correct: 3.1.1 @@ -4233,7 +4318,6 @@ packages: resolution: integrity: sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= /yallist/4.0.0: - dev: false resolution: integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== /yaml/1.10.0: From 06de9e5856ff684d7dae9bf37b91728e2e2e55ce Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 2 Dec 2020 00:16:18 -0500 Subject: [PATCH 10/15] optional chaining --- packages/kit/src/runtime/internal/renderer/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/kit/src/runtime/internal/renderer/index.js b/packages/kit/src/runtime/internal/renderer/index.js index f963c1f59176..1ddf4b628197 100644 --- a/packages/kit/src/runtime/internal/renderer/index.js +++ b/packages/kit/src/runtime/internal/renderer/index.js @@ -181,8 +181,7 @@ export class Renderer { if ( !this.session_dirty && !segment_dirty && - this.current_branch[i] && - this.current_branch[i].loader === loader + this.current_branch[i]?.loader === loader ) { return this.current_branch[i]; } From 8ffca7afe19a88998a0b5a802bdce8e4e30420b4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 2 Dec 2020 00:19:04 -0500 Subject: [PATCH 11/15] more optional chaining stuff --- packages/kit/src/runtime/internal/renderer/index.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/kit/src/runtime/internal/renderer/index.js b/packages/kit/src/runtime/internal/renderer/index.js index 1ddf4b628197..d67c32dc3b65 100644 --- a/packages/kit/src/runtime/internal/renderer/index.js +++ b/packages/kit/src/runtime/internal/renderer/index.js @@ -192,9 +192,8 @@ export class Renderer { const params = get_params ? get_params(match) : {}; - let preloaded; - if (!this.initial || !this.initial.preloaded[i]) { - preloaded = preload + const preloaded = this.initial?.preloaded[i] || ( + preload ? await preload.call( preload_context, { @@ -205,10 +204,8 @@ export class Renderer { }, this.$session ) - : {}; - } else { - preloaded = this.initial.preloaded[i]; - } + : {} + ); props.components[i] = component; props[`props_${i}`] = preloaded; From e9e4d5ffefe013c91029ca7fa4c76b8d7dce388f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 2 Dec 2020 00:38:08 -0500 Subject: [PATCH 12/15] prevent preload rerunning in more situations --- .../src/runtime/internal/renderer/index.js | 68 +++++++++---------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/packages/kit/src/runtime/internal/renderer/index.js b/packages/kit/src/runtime/internal/renderer/index.js index d67c32dc3b65..6d0ce506b03c 100644 --- a/packages/kit/src/runtime/internal/renderer/index.js +++ b/packages/kit/src/runtime/internal/renderer/index.js @@ -121,7 +121,7 @@ export class Renderer { } async hydrate({ route, page }) { - const segments = page.path.split('/').filter(Boolean); + const segments = page.path.split('/'); let redirect = null; @@ -152,54 +152,41 @@ export class Renderer { try { const match = route.pattern.exec(page.path); - let segment_dirty = false; - - const part_changed = (i, segment, match) => { - // TODO only check query string changes for preload functions - // that do in fact depend on it (using static analysis or - // runtime instrumentation). Ditto for session - if (query !== this.current_query) return true; - - const previous = this.current_branch[i]; - - if (!previous) return false; - if (segment !== previous.segment) return true; - if (previous.match) { - // TODO what the hell is this - if (JSON.stringify(previous.match.slice(1, i + 2)) !== JSON.stringify(match.slice(1, i + 2))) { - return true; - } - } - }; - branch = await Promise.all( [[() => this.layout], ...route.parts].map(async ([loader, get_params], i) => { - const segment = segments[i]; - - if (part_changed(i, segment, match)) segment_dirty = true; - - if ( - !this.session_dirty && - !segment_dirty && - this.current_branch[i]?.loader === loader - ) { - return this.current_branch[i]; + const params = get_params ? get_params(match) : {}; + const stringified_params = JSON.stringify(params); + + const previous = this.current_branch[i]; + if (previous) { + const changed = ( + (previous.loader !== loader) || + (previous.uses_session && this.session_dirty) || + (previous.uses_query && query_dirty) || + (previous.stringified_params !== stringified_params) + ); + + if (!changed) { + return previous; + } } - segment_dirty = false; - const { default: component, preload } = await loader(); - const params = get_params ? get_params(match) : {}; + const uses_session = preload && preload.length > 1; + let uses_query = false; const preloaded = this.initial?.preloaded[i] || ( preload ? await preload.call( preload_context, { + get query() { + uses_query = true; + return page.query; + }, host: page.host, path: page.path, - query: page.query, params }, this.$session @@ -210,7 +197,16 @@ export class Renderer { props.components[i] = component; props[`props_${i}`] = preloaded; - return { component, params, props: preloaded, segment, match, loader }; + return { + component, + params, + stringified_params, + props: preloaded, + match, + loader, + uses_session, + uses_query + }; }) ); From 6a9c2f1b583f5b461ea07fb53484fd1283fcef60 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 2 Dec 2020 08:56:47 -0500 Subject: [PATCH 13/15] tidy up --- packages/kit/src/renderer/page.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/kit/src/renderer/page.js b/packages/kit/src/renderer/page.js index b5663ca9f365..32d9cc7ccbf9 100644 --- a/packages/kit/src/renderer/page.js +++ b/packages/kit/src/renderer/page.js @@ -16,8 +16,6 @@ async function get_response({ }) { let redirected; - const segments = request.path.split('/').filter(Boolean); - const base = ''; // TODO const dependencies = {}; @@ -181,9 +179,8 @@ async function get_response({ const serialized_preloads = `[${preloaded .map((data) => try_serialize(data, (err) => { - const path = '/' + segments.join('/'); console.error( - `Failed to serialize preloaded data to transmit to the client at the ${path} route: ${err.message}` + `Failed to serialize preloaded data to transmit to the client at the ${request.path} route: ${err.message}` ); console.warn( 'The client will re-render over the server-rendered page fresh instead of continuing where it left off. See https://sapper.svelte.dev/docs#Return_value for more information' From 0d7fe5a98f72e198c81cebefd395148018ac8a48 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 2 Dec 2020 09:04:43 -0500 Subject: [PATCH 14/15] all tests passing --- packages/kit/src/core/create_app.js | 3 +- .../src/runtime/internal/renderer/index.js | 41 +++++++++++++++---- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/packages/kit/src/core/create_app.js b/packages/kit/src/core/create_app.js index 3b6963349b4e..4b8e3b4fb1e3 100644 --- a/packages/kit/src/core/create_app.js +++ b/packages/kit/src/core/create_app.js @@ -150,7 +150,7 @@ function generate_app(manifest_data) { return ` diff --git a/packages/kit/src/runtime/internal/renderer/index.js b/packages/kit/src/runtime/internal/renderer/index.js index 6d0ce506b03c..77b5440ec76a 100644 --- a/packages/kit/src/runtime/internal/renderer/index.js +++ b/packages/kit/src/runtime/internal/renderer/index.js @@ -1,6 +1,32 @@ import { writable } from 'svelte/store'; import { find_anchor } from '../utils'; +function page_store(value) { + const store = writable(value); + let ready = true; + + function notify() { + ready = true; + store.update((val) => val); + } + + function set(new_value) { + ready = false; + store.set(new_value); + } + + function subscribe(run) { + let old_value; + return store.subscribe((new_value) => { + if (old_value === undefined || (ready && new_value !== old_value)) { + run((old_value = new_value)); + } + }); + } + + return { notify, set, subscribe }; +} + export class Renderer { constructor({ Root, @@ -13,6 +39,7 @@ export class Renderer { }) { this.Root = Root; this.layout = layout; + this.layout_loader = () => layout; // TODO ideally we wouldn't need to store these... this.target = target; @@ -31,7 +58,7 @@ export class Renderer { }; this.stores = { - page: writable({}), + page: page_store({}), preloading: writable(false), session: writable(session) }; @@ -121,8 +148,6 @@ export class Renderer { } async hydrate({ route, page }) { - const segments = page.path.split('/'); - let redirect = null; const props = { @@ -133,11 +158,11 @@ export class Renderer { const preload_context = { fetch: (url, opts) => fetch(url, opts), - redirect: (statusCode, location) => { - if (redirect && (redirect.statusCode !== statusCode || redirect.location !== location)) { + redirect: (status, location) => { + if (redirect && (redirect.status !== status || redirect.location !== location)) { throw new Error('Conflicting redirects'); } - redirect = { statusCode, location }; + redirect = { status, location }; }, error: (status, error) => { props.error = typeof error === 'string' ? new Error(error) : error; @@ -146,6 +171,7 @@ export class Renderer { }; const query = page.query.toString(); + const query_dirty = query !== this.current_query; let branch; @@ -153,7 +179,7 @@ export class Renderer { const match = route.pattern.exec(page.path); branch = await Promise.all( - [[() => this.layout], ...route.parts].map(async ([loader, get_params], i) => { + [[this.layout_loader], ...route.parts].map(async ([loader, get_params], i) => { const params = get_params ? get_params(match) : {}; const stringified_params = JSON.stringify(params); @@ -167,6 +193,7 @@ export class Renderer { ); if (!changed) { + props.components[i] = previous.component; return previous; } } From 85618ec48b18eecb5ef64882a23ae2e98b82e828 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 2 Dec 2020 09:07:12 -0500 Subject: [PATCH 15/15] fix manifest data tests --- .../core/test/create_manifest_data.spec.js | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/kit/src/core/test/create_manifest_data.spec.js b/packages/kit/src/core/test/create_manifest_data.spec.js index bef31ada404c..36b3526551e6 100644 --- a/packages/kit/src/core/test/create_manifest_data.spec.js +++ b/packages/kit/src/core/test/create_manifest_data.spec.js @@ -47,7 +47,6 @@ test('creates routes', () => { path: null, pattern: /^\/blog\/([^/]+?)\/?$/, parts: [ - null, { component: blog_$slug, params: ['slug'] } ] } @@ -119,16 +118,16 @@ test('sorts routes correctly', () => { ['index.svelte'], ['about.svelte'], ['post/index.svelte'], - [null, 'post/bar.svelte'], - [null, 'post/foo.svelte'], - [null, 'post/f[xx].svelte'], - [null, 'post/[id([0-9-a-z]{3,})].svelte'], - [null, 'post/[id].svelte'], + ['post/bar.svelte'], + ['post/foo.svelte'], + ['post/f[xx].svelte'], + ['post/[id([0-9-a-z]{3,})].svelte'], + ['post/[id].svelte'], ['[wildcard].svelte'], - [null, null, null, '[...spread]/deep/[...deep_spread]/xyz.svelte'], - [null, null, '[...spread]/deep/[...deep_spread]/index.svelte'], - [null, '[...spread]/deep/index.svelte'], - [null, '[...spread]/abc.svelte'], + ['[...spread]/deep/[...deep_spread]/xyz.svelte'], + ['[...spread]/deep/[...deep_spread]/index.svelte'], + ['[...spread]/deep/index.svelte'], + ['[...spread]/abc.svelte'], ['[...spread]/index.svelte'] ]); }); @@ -256,7 +255,6 @@ test('works with custom extensions' , () => { path: null, pattern: /^\/blog\/([^/]+?)\/?$/, parts: [ - null, { component: blog_$slug, params: ['slug'] } ] }