diff --git a/src/core/create_manifests.ts b/src/core/create_manifests.ts index 79e37ea..da6f41d 100644 --- a/src/core/create_manifests.ts +++ b/src/core/create_manifests.ts @@ -5,7 +5,7 @@ import { Page, PageComponent, ManifestData } from '../interfaces'; const app = fs.readFileSync(path.resolve(__dirname, '../templates/App.html'), 'utf-8'); const internal = fs.readFileSync(path.resolve(__dirname, '../templates/internal.mjs'), 'utf-8'); -const layout = ``; +const layout = fs.readFileSync(path.resolve(__dirname, '../templates/layout.html'), 'utf-8'); export function create_main_manifests({ bundler, diff --git a/templates/internal.mjs b/templates/internal.mjs index 454d657..0eba064 100644 --- a/templates/internal.mjs +++ b/templates/internal.mjs @@ -1 +1,8 @@ +import { writable } from 'svelte/store'; + +export const stores = { + preloading: writable(null), + page: writable(null) +}; + export const CONTEXT_KEY = {}; \ No newline at end of file diff --git a/templates/layout.html b/templates/layout.html new file mode 100644 index 0000000..8c0dcbb --- /dev/null +++ b/templates/layout.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/src/app/app.ts b/templates/src/app/app.ts index d6c8e0a..832b46b 100644 --- a/templates/src/app/app.ts +++ b/templates/src/app/app.ts @@ -1,5 +1,5 @@ import App from '@sapper/App.html'; -import { preloading, page } from '../shared/stores'; +import { stores } from '@sapper/internal'; import Root, * as RootStatic from '__ROOT__'; import ErrorComponent from '__ERROR__'; import { @@ -10,7 +10,8 @@ import { ComponentLoader, ComponentConstructor, RootProps, - Page + Page, + PageData } from './types'; import goto from './goto'; @@ -25,20 +26,9 @@ let current_token: {}; let root_preload: Promise; let root_data: any; -const root_props: RootProps = { - path: null, - params: null, - query: null, - child: { - segment: null, - component: null, - props: {} - } -}; - export let prefetching: { href: string; - promise: Promise<{ redirect?: Redirect, data?: any, nullable_depth?: number, new_segments?: any }>; + promise: Promise<{ redirect?: Redirect, data?: any, new_segments?: any }>; } = null; export function set_prefetching(href, promise) { prefetching = { href, promise }; @@ -111,7 +101,7 @@ export function scroll_state() { }; } -export function navigate(target: Target, id: number, noscroll?: boolean, hash?: string): Promise { +export async function navigate(target: Target, id: number, noscroll?: boolean, hash?: string): Promise { let scroll: ScrollPosition; if (id) { // popstate or initial navigation @@ -129,7 +119,7 @@ export function navigate(target: Target, id: number, noscroll?: boolean, hash?: cid = id; if (root_component) { - preloading.set({ + stores.preloading.set({ // TODO path, params, query }); } @@ -141,38 +131,22 @@ export function navigate(target: Target, id: number, noscroll?: boolean, hash?: const token = current_token = {}; - return loaded.then(({ redirect, data, nullable_depth, new_segments }) => { - if (redirect) { - return goto(redirect.location, { replaceState: true }); - } - if (new_segments) { - segments = new_segments; - } - render(data, nullable_depth, scroll_history[id], noscroll, hash, token); - if (document.activeElement) document.activeElement.blur(); - }); + const { redirect, page, data, new_segments, results } = await loaded; + + if (redirect) return goto(redirect.location, { replaceState: true }); + if (new_segments) segments = new_segments; + + await render(results, data, page, scroll_history[id], noscroll, hash, token); + if (document.activeElement) document.activeElement.blur(); } -async function render(props: any, nullable_depth: number, scroll: ScrollPosition, noscroll: boolean, hash: string, token: {}) { +async function render(results: any[], props: any, page: PageData, scroll: ScrollPosition, noscroll: boolean, hash: string, token: {}) { if (current_token !== token) return; - preloading.set(null); + stores.page.set(page); + stores.preloading.set(null); if (root_component) { - // first, clear out highest-level root component - let level = props.child; - for (let i = 0; i < nullable_depth; i += 1) { - if (i === nullable_depth) break; - level = level.props.child; - } - - const { component } = level; - level.component = null; - root_component.props = props; - - // then render new stuff - // TODO do we need to call `flush` before doing this? - level.component = component; root_component.props = props; } else { // first load — remove SSR'd contents @@ -185,7 +159,7 @@ async function render(props: any, nullable_depth: number, scroll: ScrollPosition detach(end); } - Object.assign(props, root_data); + Object.assign(props, root_data); // TODO what is root_data, do we still need it? root_component = new App({ target, @@ -215,14 +189,16 @@ async function render(props: any, nullable_depth: number, scroll: ScrollPosition if (scroll) scrollTo(scroll.x, scroll.y); } - Object.assign(root_props, props); + previous_thingummy = results; ready = true; } +let previous_thingummy = []; + export function prepare_page(target: Target): Promise<{ redirect?: Redirect; data?: any; - nullable_depth?: number; + page: PageData }> { const { page, path, query } = target; const new_segments = path.split('/').filter(Boolean); @@ -240,9 +216,9 @@ export function prepare_page(target: Target): Promise<{ let redirect: Redirect = null; let error: { statusCode: number, message: Error | string } = null; + let page_data: PageData; const preload_context = { - store, fetch: (url: string, opts?: any) => fetch(url, opts), redirect: (statusCode: number, location: string) => { if (redirect && (redirect.statusCode !== statusCode || redirect.location !== location)) { @@ -267,11 +243,13 @@ export function prepare_page(target: Target): Promise<{ } return Promise.all(page.parts.map((part, i) => { - if (i < changed_from) return null; + const segment = new_segments[i]; + + if (i < changed_from || !part) return previous_thingummy[i]; if (!part) return null; return load_component(components[part.i]).then(({ default: Component, preload }) => { - const req = { + page_data = { path, query, params: part.params ? part.params(target.match) : {} @@ -280,7 +258,7 @@ export function prepare_page(target: Target): Promise<{ let preloaded; if (ready || !initial_data.preloaded[i + 1]) { preloaded = preload - ? preload.call(preload_context, req) + ? preload.call(preload_context, page_data) : {}; } else { preloaded = initial_data.preloaded[i + 1]; @@ -304,72 +282,55 @@ export function prepare_page(target: Target): Promise<{ } }).then(results => { if (redirect) { - return { redirect, new_segments }; + return { redirect, new_segments, page: null }; } - const get_params = page.parts[page.parts.length - 1].params || (() => ({})); - const params = get_params(target.match); + const deepest = page.parts[page.parts.length - 1]; + + const page_data = { + path, + query, + params: deepest.params ? deepest.params(target.match) : {} + }; if (error) { - const props = { - path, - query, - params, - error: typeof error.message === 'string' ? new Error(error.message) : error.message, - status: error.statusCode - }; - return { - nullable_depth: 0, new_segments, - data: Object.assign({}, props, { + page: page_data, + data: { child: { component: ErrorComponent, - props + props: { + error: typeof error.message === 'string' ? new Error(error.message) : error.message, + status: error.statusCode + } } - }) + } }; } - const props = { path, query, error: null, status: null }; - const data = { - path, - child: Object.assign({}, root_props.child, { + const props = { + child: { segment: new_segments[0] - }) + } }; - if (changed(query, root_props.query)) data.query = query; - if (changed(params, root_props.params)) data.params = params; - let level = data.child; - let nullable_depth = 0; + let level = props.child; for (let i = 0; i < page.parts.length; i += 1) { const part = page.parts[i]; if (!part) continue; - const get_params = part.params || (() => ({})); - - if (i < changed_from) { - level.props.path = path; - level.props.query = query; - level.props.child = Object.assign({}, level.props.child); - - nullable_depth += 1; - } else { - level.component = results[i].Component; - level.props = Object.assign({}, level.props, props, { - params: get_params(target.match), - }, results[i].preloaded); - - level.props.child = {}; - } + level.component = results[i].Component; + level.props = Object.assign({}, results[i].preloaded, { + child: {} + }); level = level.props.child; level.segment = new_segments[i + 1]; } - return { data, nullable_depth, new_segments }; + return { data: props, new_segments, page: page_data, results }; }); } @@ -402,8 +363,4 @@ export function load_component(component: ComponentLoader): Promise<{ function detach(node: Node) { node.parentNode.removeChild(node); -} - -function changed(a: Record, b: Record) { - return JSON.stringify(a) !== JSON.stringify(b); -} +} \ No newline at end of file diff --git a/templates/src/app/index.ts b/templates/src/app/index.ts index 5fbc232..46d3610 100644 --- a/templates/src/app/index.ts +++ b/templates/src/app/index.ts @@ -1,6 +1,5 @@ import { getContext } from 'svelte'; -import { CONTEXT_KEY } from '@sapper/internal'; -import * as stores from '../shared/stores'; +import { CONTEXT_KEY, stores } from '@sapper/internal'; export const preloading = { subscribe: stores.preloading.subscribe }; export const page = { subscribe: stores.page.subscribe }; diff --git a/templates/src/app/types.ts b/templates/src/app/types.ts index d3388c3..da3f7ad 100644 --- a/templates/src/app/types.ts +++ b/templates/src/app/types.ts @@ -65,4 +65,10 @@ export type Redirect = { export type Store = { get: () => any; -} \ No newline at end of file +} + +export type PageData = { + path: string; + params: Record; + query: Record; +}; \ No newline at end of file diff --git a/templates/src/server/middleware/get_page_handler.ts b/templates/src/server/middleware/get_page_handler.ts index a2ccc19..c695180 100644 --- a/templates/src/server/middleware/get_page_handler.ts +++ b/templates/src/server/middleware/get_page_handler.ts @@ -4,9 +4,9 @@ import cookie from 'cookie'; import devalue from 'devalue'; import fetch from 'node-fetch'; import URL from 'url'; -import * as stores from '../../shared/stores'; import { build_dir, dev, src_dir, IGNORE } from '../placeholders'; import { Manifest, Page, Props, Req, Res } from './types'; +import { stores } from '@sapper/internal'; import App from '@sapper/App.html'; export function get_page_handler( @@ -135,6 +135,7 @@ export function get_page_handler( let preloaded; let match; + let params; try { const root_preloaded = manifest.root_preload @@ -147,16 +148,20 @@ export function get_page_handler( match = error ? null : page.pattern.exec(req.path); + let toPreload = [root_preloaded]; if (!isSWIndexHtml) { toPreload = toPreload.concat(page.parts.map(part => { if (!part) return null; + // the deepest level is used below, to initialise the store + params = part.params ? part.params(match) : {}; + return part.preload ? part.preload.call(preload_context, { path: req.path, query: req.query, - params: part.params ? part.params(match) : {} + params }) : {}; })) @@ -186,60 +191,46 @@ export function get_page_handler( const segments = req.path.split('/').filter(Boolean); - const props: Props = { - path: req.path, - query: req.query, - params: {}, - child: null - }; - - if (error) { - props.error = error instanceof Error ? error : { message: error }; - props.status = status; - } - - const data = Object.assign({}, props, preloaded[0], { - params: {}, + const props = Object.assign({}, preloaded[0], { child: { - segment: segments[0] + segment: segments[0], + props: {} } }); - let level = data.child; - if (isSWIndexHtml) { - level.props = Object.assign({}, props, { - params: {} - }) - } else { + let level = props.child; + if (!isSWIndexHtml) { for (let i = 0; i < page.parts.length; i += 1) { const part = page.parts[i]; if (!part) continue; - const get_params = part.params || (() => ({})); - Object.assign(level, { component: part.component, - props: Object.assign({}, props, { - params: get_params(match) - }, preloaded[i + 1]) + props: Object.assign({}, preloaded[i + 1]) }); level.props.child = { - segment: segments[i + 1] + segment: segments[i + 1], + props: {} }; level = level.props.child; } } + if (error) { + props.child.props.error = error instanceof Error ? error : { message: error }; + props.child.props.status = status; + } + stores.page.set({ path: req.path, query: req.query, - params: req.params + params: params }); const { html, head, css } = App.render({ Root: manifest.root, - props: data, + props: props, session }); @@ -313,6 +304,7 @@ export function get_page_handler( res.statusCode = status; res.end(body); } catch(err) { + console.log(err); if (error) { // we encountered an error while rendering the error page — oops res.statusCode = 500; diff --git a/templates/src/server/middleware/types.ts b/templates/src/server/middleware/types.ts index 6d7597f..b76b75c 100644 --- a/templates/src/server/middleware/types.ts +++ b/templates/src/server/middleware/types.ts @@ -12,6 +12,7 @@ export type Page = { name: string; component: Component; params?: (match: RegExpMatchArray) => Record; + preload?: (data: any) => any | Promise; }> }; @@ -19,6 +20,7 @@ export type Manifest = { server_routes: ServerRoute[]; pages: Page[]; root: Component; + root_preload?: (data: any) => any | Promise; error: Component; } @@ -29,9 +31,6 @@ export type Store = { }; export type Props = { - path: string; - query: Record; - params: Record; error?: { message: string }; status?: number; child: { @@ -64,6 +63,5 @@ interface Component { head: string; css: { code: string, map: any }; html: string - }, - preload: (data: any) => any | Promise + } } \ No newline at end of file diff --git a/templates/src/shared/stores.ts b/templates/src/shared/stores.ts deleted file mode 100644 index f66a43b..0000000 --- a/templates/src/shared/stores.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { writable } from 'svelte/store'; - -export const preloading = writable(null); -export const page = writable(null); \ No newline at end of file diff --git a/test/apps/basics/src/routes/[slug].html b/test/apps/basics/src/routes/[slug].html index ade66de..e31f6d9 100644 --- a/test/apps/basics/src/routes/[slug].html +++ b/test/apps/basics/src/routes/[slug].html @@ -1 +1,5 @@ -

{params.slug.toUpperCase()}

\ No newline at end of file + + +

{$page.params.slug.toUpperCase()}

\ No newline at end of file diff --git a/test/apps/basics/src/routes/echo-query/index.html b/test/apps/basics/src/routes/echo-query/index.html index aa09b35..614bb4d 100644 --- a/test/apps/basics/src/routes/echo-query/index.html +++ b/test/apps/basics/src/routes/echo-query/index.html @@ -1 +1,5 @@ -

{JSON.stringify(query)}

\ No newline at end of file + + +

{JSON.stringify($page.query)}

\ No newline at end of file diff --git a/test/apps/encoding/src/routes/echo/page/[slug].html b/test/apps/encoding/src/routes/echo/page/[slug].html index deead40..e2c9491 100644 --- a/test/apps/encoding/src/routes/echo/page/[slug].html +++ b/test/apps/encoding/src/routes/echo/page/[slug].html @@ -7,8 +7,8 @@ -

{slug} {JSON.stringify(query)}

\ No newline at end of file +

{slug} {JSON.stringify($page.query)}

\ No newline at end of file diff --git a/test/apps/layout/src/client.js b/test/apps/layout/src/client.js index 6cce7e6..c492517 100644 --- a/test/apps/layout/src/client.js +++ b/test/apps/layout/src/client.js @@ -2,6 +2,11 @@ import * as sapper from '@sapper/app'; window.start = () => sapper.start({ target: document.querySelector('#sapper') +}).catch(err => { + console.error(`OH NO! ${err.message}`); + throw err; +}).then(() => { + console.log(`STARTED`); }); window.prefetchRoutes = () => sapper.prefetchRoutes(); diff --git a/test/apps/layout/src/routes/[x]/[y]/[z].html b/test/apps/layout/src/routes/[x]/[y]/[z].html index b51089c..38c5575 100644 --- a/test/apps/layout/src/routes/[x]/[y]/[z].html +++ b/test/apps/layout/src/routes/[x]/[y]/[z].html @@ -9,10 +9,10 @@ -z: {segment} {count} +z: {$page.params.z} {count} click me \ No newline at end of file diff --git a/test/apps/layout/src/routes/[x]/[y]/_layout.html b/test/apps/layout/src/routes/[x]/[y]/_layout.html index f60dd63..401f654 100644 --- a/test/apps/layout/src/routes/[x]/[y]/_layout.html +++ b/test/apps/layout/src/routes/[x]/[y]/_layout.html @@ -9,13 +9,13 @@ -y: {segment} {count} +y: {$page.params.y} {count} child segment: {child.segment} \ No newline at end of file diff --git a/test/apps/layout/test.ts b/test/apps/layout/test.ts index 36eda4e..c088432 100644 --- a/test/apps/layout/test.ts +++ b/test/apps/layout/test.ts @@ -26,10 +26,18 @@ describe('layout', function() { it('only recreates components when necessary', async () => { await page.goto(`${base}/foo/bar/baz`); - await start(); const text1 = String(await page.evaluate(() => document.querySelector('#sapper').textContent)); - assert.deepEqual(text1.split('\n').filter(Boolean), [ + assert.deepEqual(text1.split('\n').filter(Boolean).map(str => str.trim()), [ + 'y: bar 1', + 'z: baz 1', + 'click me', + 'child segment: baz' + ]); + + await start(); + const text2 = String(await page.evaluate(() => document.querySelector('#sapper').textContent)); + assert.deepEqual(text2.split('\n').filter(Boolean).map(str => str.trim()), [ 'y: bar 1', 'z: baz 1', 'click me', @@ -39,8 +47,8 @@ describe('layout', function() { await page.click('[href="foo/bar/qux"]'); await wait(50); - const text2 = String(await page.evaluate(() => document.querySelector('#sapper').textContent)); - assert.deepEqual(text2.split('\n').filter(Boolean), [ + const text3 = String(await page.evaluate(() => document.querySelector('#sapper').textContent)); + assert.deepEqual(text3.split('\n').filter(Boolean).map(str => str.trim()), [ 'y: bar 1', 'z: qux 2', 'click me', diff --git a/test/apps/preloading/src/routes/prefetch/[slug]/index.html b/test/apps/preloading/src/routes/prefetch/[slug]/index.html index d13188c..6eebc4c 100644 --- a/test/apps/preloading/src/routes/prefetch/[slug]/index.html +++ b/test/apps/preloading/src/routes/prefetch/[slug]/index.html @@ -1 +1,5 @@ -

{params.slug}

+ + +

{$page.params.slug}