From 9812cbd71ca4087d0ebacad255c94c9b5dc29a85 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 17 Mar 2018 13:45:59 -0400 Subject: [PATCH] add server- and client-side store management (#178) --- src/interfaces.ts | 4 ++++ src/middleware.ts | 35 +++++++++++++++++++++++++---------- src/runtime/index.ts | 10 ++++++++-- src/runtime/interfaces.ts | 5 ++++- test/app/app/client.js | 5 ++++- test/app/app/server.js | 10 +++++++++- test/app/routes/store.html | 1 + test/common/test.js | 12 ++++++++++++ 8 files changed, 67 insertions(+), 15 deletions(-) create mode 100644 test/app/routes/store.html diff --git a/src/interfaces.ts b/src/interfaces.ts index f2e656af6..ebc006ebc 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -12,4 +12,8 @@ export type Route = { export type Template = { render: (data: Record) => string; stream: (req, res, data: Record>) => void; +}; + +export type Store = { + get: () => any; }; \ No newline at end of file diff --git a/src/middleware.ts b/src/middleware.ts index cdfde823e..63ad84ac0 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -19,7 +19,7 @@ type RouteObject = { pattern: RegExp; params: (match: RegExpMatchArray) => Record; module: { - render: (data: any) => { + render: (data: any, opts: { store: Store }) => { head: string; css: { code: string, map: any }; html: string @@ -31,15 +31,22 @@ type RouteObject = { type Handler = (req: Req, res: ServerResponse, next: () => void) => void; +type Store = { + get: () => any +}; + interface Req extends ClientRequest { url: string; + baseUrl: string; + originalUrl: string; method: string; - pathname: string; + path: string; params: Record; } -export default function middleware({ routes }: { - routes: RouteObject[] +export default function middleware({ routes, store }: { + routes: RouteObject[], + store: (req: Req) => Store }) { const output = locations.dest(); @@ -75,7 +82,7 @@ export default function middleware({ routes }: { cache_control: 'max-age=31536000' }), - get_route_handler(client_info.assetsByChunkName, routes) + get_route_handler(client_info.assetsByChunkName, routes, store) ].filter(Boolean)); return middleware; @@ -120,7 +127,7 @@ function serve({ prefix, pathname, cache_control }: { const resolved = Promise.resolve(); -function get_route_handler(chunks: Record, routes: RouteObject[]) { +function get_route_handler(chunks: Record, 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')); @@ -142,6 +149,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 }; let redirect: { statusCode: number, location: string }; @@ -154,7 +162,8 @@ function get_route_handler(chunks: Record, routes: RouteObject[] }, error: (statusCode: number, message: Error | string) => { error = { statusCode, message }; - } + }, + store }, req) : {} ).catch(err => { error = { statusCode: 500, message: err }; @@ -172,10 +181,15 @@ function get_route_handler(chunks: Record, routes: RouteObject[] return; } - const serialized = try_serialize(preloaded); // TODO bail on non-POJOs + const serialized = { + preloaded: mod.preload && try_serialize(preloaded), + store: store && try_serialize(store.get()) + }; Object.assign(data, preloaded); - const { html, head, css } = mod.render(data); + const { html, head, css } = mod.render(data, { + store + }); let scripts = [] .concat(chunks.main) // chunks main might be an array. it might not! thanks, webpack @@ -184,7 +198,8 @@ function get_route_handler(chunks: Record, routes: RouteObject[] let inline_script = `__SAPPER__={${[ `baseUrl: "${req.baseUrl}"`, - mod.preload && serialized && `preloaded: ${serialized}`, + serialized.preloaded && `preloaded: ${serialized.preloaded}`, + serialized.store && `store: ${serialized.store}` ].filter(Boolean).join(',')}}` const has_service_worker = fs.existsSync(path.join(locations.dest(), 'service-worker.js')); diff --git a/src/runtime/index.ts b/src/runtime/index.ts index b1cdfe8db..bf7ab86f7 100644 --- a/src/runtime/index.ts +++ b/src/runtime/index.ts @@ -1,10 +1,11 @@ import { detach, findAnchor, scroll_state, which } from './utils'; -import { Component, ComponentConstructor, Params, Query, Route, RouteData, ScrollPosition, Target } from './interfaces'; +import { Component, ComponentConstructor, Params, Query, Route, RouteData, ScrollPosition, Store, Target } from './interfaces'; const manifest = typeof window !== 'undefined' && window.__SAPPER__; export let component: Component; let target: Node; +let store: Store; let routes: Route[]; let errors: { '4xx': Route, '5xx': Route }; @@ -69,6 +70,7 @@ function render(Component: ComponentConstructor, data: any, scroll: ScrollPositi component = new Component({ target, data, + store, hydrate: !component }); @@ -227,7 +229,7 @@ function handle_touchstart_mouseover(event: MouseEvent | TouchEvent) { let inited: boolean; -export function init(_target: Node, _routes: Route[]) { +export function init(_target: Node, _routes: Route[], opts?: { store?: (data: any) => Store }) { target = _target; routes = _routes.filter(r => !r.error); errors = { @@ -235,6 +237,10 @@ export function init(_target: Node, _routes: Route[]) { '5xx': _routes.find(r => r.error === '5xx') }; + if (opts && opts.store) { + store = opts.store(manifest.store); + } + if (!inited) { // this check makes HMR possible window.addEventListener('click', handle_click); window.addEventListener('popstate', handle_popstate); diff --git a/src/runtime/interfaces.ts b/src/runtime/interfaces.ts index b2f9574b4..371ea2d69 100644 --- a/src/runtime/interfaces.ts +++ b/src/runtime/interfaces.ts @@ -1,9 +1,12 @@ +import { Store } from '../interfaces'; + +export { Store }; export type Params = Record; export type Query = Record; export type RouteData = { params: Params, query: Query }; export interface ComponentConstructor { - new (options: { target: Node, data: any, hydrate: boolean }): Component; + new (options: { target: Node, data: any, store: Store, hydrate: boolean }): Component; preload: (data: { params: Params, query: Query }) => Promise; }; diff --git a/test/app/app/client.js b/test/app/app/client.js index bbe24c6db..44937a159 100644 --- a/test/app/app/client.js +++ b/test/app/app/client.js @@ -1,8 +1,11 @@ import { init, prefetchRoutes } from '../../../runtime.js'; +import { Store } from 'svelte/store.js'; import { routes } from './manifest/client.js'; window.init = () => { - return init(document.querySelector('#sapper'), routes); + return init(document.querySelector('#sapper'), routes, { + store: data => new Store(data) + }); }; window.prefetchRoutes = prefetchRoutes; \ No newline at end of file diff --git a/test/app/app/server.js b/test/app/app/server.js index d122bce09..3e7c46e5a 100644 --- a/test/app/app/server.js +++ b/test/app/app/server.js @@ -3,6 +3,7 @@ import { resolve } from 'url'; import express from 'express'; import serve from 'serve-static'; import sapper from '../../../dist/middleware.ts.js'; +import { Store } from 'svelte/store.js'; import { routes } from './manifest/server.js'; let pending; @@ -77,7 +78,14 @@ const middlewares = [ next(); }, - sapper({ routes }) + sapper({ + routes, + store: () => { + return new Store({ + title: 'Stored title' + }); + } + }) ]; if (BASEPATH) { diff --git a/test/app/routes/store.html b/test/app/routes/store.html new file mode 100644 index 000000000..bdbb15cc2 --- /dev/null +++ b/test/app/routes/store.html @@ -0,0 +1 @@ +

{{$title}}

\ No newline at end of file diff --git a/test/common/test.js b/test/common/test.js index 7899f6b77..c3626eed4 100644 --- a/test/common/test.js +++ b/test/common/test.js @@ -521,6 +521,18 @@ function run({ mode, basepath = '' }) { assert.equal(title, '42'); }); }); + + it('renders store props', () => { + return nightmare.goto(`${base}/store`) + .page.title() + .then(title => { + assert.equal(title, 'Stored title'); + return nightmare.init().page.title(); + }) + .then(title => { + assert.equal(title, 'Stored title'); + }); + }); }); describe('headers', () => {