diff --git a/src/middleware.ts b/src/middleware.ts index 323bd9f..b78eb02 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -20,14 +20,7 @@ type RouteObject = { type: 'page' | 'route'; pattern: RegExp; params: (match: RegExpMatchArray) => Record; - module: { - render: (data: any, opts: { store: Store }) => { - head: string; - css: { code: string, map: any }; - html: string - }, - preload: (data: any) => any | Promise - }; + module: Component; error?: string; } @@ -47,7 +40,17 @@ interface Req extends ClientRequest { headers: Record; } -export default function middleware({ routes, store }: { +interface Component { + render: (data: any, opts: { store: Store }) => { + head: string; + css: { code: string, map: any }; + html: string + }, + preload: (data: any) => any | Promise +} + +export default function middleware({ App, routes, store }: { + App: Component, routes: RouteObject[], store: (req: Req) => Store }) { @@ -90,7 +93,7 @@ export default function middleware({ routes, store }: { cache_control: 'max-age=31536000' }), - get_route_handler(client_info.assets, routes, store) + get_route_handler(client_info.assets, App, routes, store) ].filter(Boolean)); return middleware; @@ -135,7 +138,7 @@ function serve({ prefix, pathname, cache_control }: { const resolved = Promise.resolve(); -function get_route_handler(chunks: Record, routes: RouteObject[], store_getter: (req: Req) => Store) { +function get_route_handler(chunks: Record, App: Component, routes: RouteObject[], store_getter: (req: Req) => Store) { const template = dev() ? () => fs.readFileSync(`${locations.app()}/template.html`, 'utf-8') : (str => () => str)(fs.readFileSync(`${locations.dest()}/template.html`, 'utf-8')); @@ -170,7 +173,7 @@ function get_route_handler(chunks: Record, routes: RouteObject[] res.setHeader('Link', link); const store = store_getter ? store_getter(req) : null; - const data = { params: req.params, query: req.query }; + const props = { params: req.params, query: req.query, path: req.path }; let redirect: { statusCode: number, location: string }; let error: { statusCode: number, message: Error | string }; @@ -240,9 +243,9 @@ function get_route_handler(chunks: Record, routes: RouteObject[] preloaded: mod.preload && try_serialize(preloaded), store: store && try_serialize(store.get()) }; - Object.assign(data, preloaded); + Object.assign(props, preloaded); - const { html, head, css } = mod.render(data, { + const { html, head, css } = App.render({ Page: mod, props }, { store }); diff --git a/src/runtime/index.ts b/src/runtime/index.ts index 5e643be..ca77786 100644 --- a/src/runtime/index.ts +++ b/src/runtime/index.ts @@ -3,6 +3,7 @@ import { Component, ComponentConstructor, Params, Query, Route, RouteData, Scrol const manifest = typeof window !== 'undefined' && window.__SAPPER__; +export let App: ComponentConstructor; export let component: Component; let target: Node; let store: Store; @@ -27,10 +28,10 @@ function select_route(url: URL): Target { if (url.origin !== window.location.origin) return null; if (!url.pathname.startsWith(manifest.baseUrl)) return null; - const pathname = url.pathname.slice(manifest.baseUrl.length); + const path = url.pathname.slice(manifest.baseUrl.length); for (const route of routes) { - const match = route.pattern.exec(pathname); + const match = route.pattern.exec(path); if (match) { if (route.ignore) return null; @@ -43,19 +44,18 @@ function select_route(url: URL): Target { query[key] = value || true; }) } - return { url, route, data: { params, query } }; + return { url, route, props: { params, query, path } }; } } } +let first_load = true; let current_token: {}; -function render(Component: ComponentConstructor, data: any, scroll: ScrollPosition, token: {}) { +function render(Page: ComponentConstructor, props: any, scroll: ScrollPosition, token: {}) { if (current_token !== token) return; - if (component) { - component.destroy(); - } else { + if (first_load) { // first load — remove SSR'd contents const start = document.querySelector('#sapper-head-start'); const end = document.querySelector('#sapper-head-end'); @@ -65,33 +65,43 @@ function render(Component: ComponentConstructor, data: any, scroll: ScrollPositi detach(start); detach(end); } - } - component = new Component({ - target, - data, - store, - hydrate: !component - }); + component = new App({ + target, + data: { + Page, + props + }, + store, + hydrate: true + }); + + first_load = false; + } else { + component.set({ + Page, + props + }); + } if (scroll) { window.scrollTo(scroll.x, scroll.y); } } -function prepare_route(Component: ComponentConstructor, data: RouteData) { +function prepare_route(Page: ComponentConstructor, props: RouteData) { let redirect: { statusCode: number, location: string } = null; let error: { statusCode: number, message: Error | string } = null; - if (!Component.preload) { - return { Component, data, redirect, error }; + if (!Page.preload) { + return { Page, props, redirect, error }; } if (!component && manifest.preloaded) { - return { Component, data: Object.assign(data, manifest.preloaded), redirect, error }; + return { Page, props: Object.assign(props, manifest.preloaded), redirect, error }; } - return Promise.resolve(Component.preload.call({ + return Promise.resolve(Page.preload.call({ store, fetch: (url: string, opts?: any) => window.fetch(url, opts), redirect: (statusCode: number, location: string) => { @@ -100,7 +110,7 @@ function prepare_route(Component: ComponentConstructor, data: RouteData) { error: (statusCode: number, message: Error | string) => { error = { statusCode, message }; } - }, data)).catch(err => { + }, props)).catch(err => { error = { statusCode: 500, message: err }; }).then(preloaded => { if (error) { @@ -108,15 +118,15 @@ function prepare_route(Component: ComponentConstructor, data: RouteData) { ? errors['4xx'] : errors['5xx']; - return route.load().then(({ default: Component }: { default: ComponentConstructor }) => { + return route.load().then(({ default: Page }: { default: ComponentConstructor }) => { const err = error.message instanceof Error ? error.message : new Error(error.message); - Object.assign(data, { status: error.statusCode, error: err }); - return { Component, data, redirect: null }; + Object.assign(props, { status: error.statusCode, error: err }); + return { Page, props, redirect: null }; }); } - Object.assign(data, preloaded) - return { Component, data, redirect }; + Object.assign(props, preloaded) + return { Page, props, redirect }; }); } @@ -136,18 +146,18 @@ function navigate(target: Target, id: number) { const loaded = prefetching && prefetching.href === target.url.href ? prefetching.promise : - target.route.load().then(mod => prepare_route(mod.default, target.data)); + target.route.load().then(mod => prepare_route(mod.default, target.props)); prefetching = null; const token = current_token = {}; - return loaded.then(({ Component, data, redirect }) => { + return loaded.then(({ Page, props, redirect }) => { if (redirect) { return goto(redirect.location, { replaceState: true }); } - render(Component, data, scroll_history[id], token); + render(Page, props, scroll_history[id], token); }); } @@ -208,7 +218,7 @@ function handle_popstate(event: PopStateEvent) { let prefetching: { href: string; - promise: Promise<{ Component: ComponentConstructor, data: any }>; + promise: Promise<{ Page: ComponentConstructor, props: any }>; } = null; export function prefetch(href: string) { @@ -217,7 +227,7 @@ export function prefetch(href: string) { if (selected && (!prefetching || href !== prefetching.href)) { prefetching = { href, - promise: selected.route.load().then(mod => prepare_route(mod.default, selected.data)) + promise: selected.route.load().then(mod => prepare_route(mod.default, selected.props)) }; } } @@ -240,12 +250,13 @@ function trigger_prefetch(event: MouseEvent | TouchEvent) { let inited: boolean; -export function init(_target: Node, _routes: Route[], opts?: { store?: (data: any) => Store }) { - target = _target; - routes = _routes.filter(r => !r.error); +export function init(opts: { App: ComponentConstructor, target: Node, routes: Route[], store?: (data: any) => Store }) { + App = opts.App; + target = opts.target; + routes = opts.routes.filter(r => !r.error); errors = { - '4xx': _routes.find(r => r.error === '4xx'), - '5xx': _routes.find(r => r.error === '5xx') + '4xx': opts.routes.find(r => r.error === '4xx'), + '5xx': opts.routes.find(r => r.error === '5xx') }; if (opts && opts.store) { diff --git a/src/runtime/interfaces.ts b/src/runtime/interfaces.ts index 371ea2d..d141039 100644 --- a/src/runtime/interfaces.ts +++ b/src/runtime/interfaces.ts @@ -3,11 +3,11 @@ import { Store } from '../interfaces'; export { Store }; export type Params = Record; export type Query = Record; -export type RouteData = { params: Params, query: Query }; +export type RouteData = { params: Params, query: Query, path: string }; export interface ComponentConstructor { new (options: { target: Node, data: any, store: Store, hydrate: boolean }): Component; - preload: (data: { params: Params, query: Query }) => Promise; + preload: (props: { params: Params, query: Query }) => Promise; }; export interface Component { @@ -30,5 +30,5 @@ export type ScrollPosition = { export type Target = { url: URL; route: Route; - data: RouteData; + props: RouteData; }; \ No newline at end of file diff --git a/test/app/app/App.html b/test/app/app/App.html new file mode 100644 index 0000000..759f7ba --- /dev/null +++ b/test/app/app/App.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/app/app/client.js b/test/app/app/client.js index 44937a1..da89de9 100644 --- a/test/app/app/client.js +++ b/test/app/app/client.js @@ -1,9 +1,13 @@ import { init, prefetchRoutes } from '../../../runtime.js'; import { Store } from 'svelte/store.js'; import { routes } from './manifest/client.js'; +import App from './App.html'; window.init = () => { - return init(document.querySelector('#sapper'), routes, { + return init({ + target: document.querySelector('#sapper'), + App, + routes, store: data => new Store(data) }); }; diff --git a/test/app/app/server.js b/test/app/app/server.js index 01b2153..3cef2a2 100644 --- a/test/app/app/server.js +++ b/test/app/app/server.js @@ -5,6 +5,7 @@ import serve from 'serve-static'; import sapper from '../../../dist/middleware.ts.js'; import { Store } from 'svelte/store.js'; import { routes } from './manifest/server.js'; +import App from './App.html' let pending; let ended; @@ -86,6 +87,7 @@ const middlewares = [ }, sapper({ + App, routes, store: () => { return new Store({