Skip to content
This repository has been archived by the owner on Jan 11, 2023. It is now read-only.

Serialize preloaded data for initial page and serve to client #87

Merged
merged 11 commits into from
Jan 14, 2018
33 changes: 26 additions & 7 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const fs = require('fs');
const path = require('path');
const serialize = require('serialize-javascript');
const route_manager = require('./route_manager.js');
const templates = require('./templates.js');
const create_app = require('./utils/create_app.js');
Expand Down Expand Up @@ -144,21 +145,31 @@ function get_route_handler(fn) {

if (mod.preload) {
const promise = Promise.resolve(mod.preload(req)).then(preloaded => {
const serialized = try_serialize(preloaded);
Object.assign(data, preloaded);
return mod.render(data);

return { rendered: mod.render(data), serialized };
});

return templates.stream(res, 200, {
main: client.main_file,
html: promise.then(rendered => rendered.html),
head: promise.then(({ head }) => `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`),
styles: promise.then(({ css }) => (css && css.code ? `<style>${css.code}</style>` : ''))
scripts: promise.then(({ serialized }) => {
const main = `<script src='${client.main_file}'></script>`;

if (serialized) {
return `<script>__SAPPER__ = { preloaded: ${serialized} };</script>${main}`;
}

return main;
}),
html: promise.then(({ rendered }) => rendered.html),
head: promise.then(({ rendered }) => `<noscript id='sapper-head-start'></noscript>${rendered.head}<noscript id='sapper-head-end'></noscript>`),
styles: promise.then(({ rendered }) => (rendered.css && rendered.css.code ? `<style>${rendered.css.code}</style>` : ''))
});
} else {
const { html, head, css } = mod.render(data);

const page = templates.render(200, {
main: client.main_file,
scripts: `<script src='${client.main_file}'></script>`,
html,
head: `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`,
styles: (css && css.code ? `<style>${css.code}</style>` : '')
Expand Down Expand Up @@ -221,7 +232,7 @@ function get_not_found_handler(fn) {
title: 'Not found',
status: 404,
method: req.method,
main: asset_cache.client.main_file,
scripts: `<script src='${asset_cache.client.main_file}'></script>`,
url: req.url
}));
};
Expand Down Expand Up @@ -249,4 +260,12 @@ function compose_handlers(handlers) {

function read_json(file) {
return JSON.parse(fs.readFileSync(file, 'utf-8'));
}

function try_serialize(data) {
try {
return serialize(data);
} catch (err) {
return null;
}
}
31 changes: 30 additions & 1 deletion lib/templates.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,47 @@
const fs = require('fs');
const glob = require('glob');
const chalk = require('chalk');
const chokidar = require('chokidar');
const framer = require('code-frame');
const { locate } = require('locate-character');
const { dev } = require('./config.js');

let templates;

function error(e) {
if (e.title) console.error(chalk.bold.red(e.title));
if (e.body) console.error(chalk.red(e.body));
if (e.url) console.error(chalk.cyan(e.url));
if (e.frame) console.error(chalk.grey(e.frame));

process.exit(1);
}

function create_templates() {
templates = glob.sync('*.html', { cwd: 'templates' })
.map(file => {
const template = fs.readFileSync(`templates/${file}`, 'utf-8');
const status = file.replace('.html', '').toLowerCase();

if (!/^[0-9x]{3}$/.test(status)) {
throw new Error(`Bad template — should be a valid status code like 404.html, or a wildcard like 2xx.html`);
error({
title: `templates/${file}`,
body: `Bad template — should be a valid status code like 404.html, or a wildcard like 2xx.html`
});
}

const index = template.indexOf('%sapper.main%');
if (index !== -1) {
// TODO remove this in a future version
const { line, column } = locate(template, index, { offsetLine: 1 });
const frame = framer(template, line, column);

error({
title: `templates/${file}`,
body: `<script src='%sapper.main%'> has been removed — use %sapper.scripts% (without the <script> tag) instead`,
url: 'https://github.com/sveltejs/sapper/issues/86',
frame
});
}

const specificity = (
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@
"dependencies": {
"chalk": "^2.3.0",
"chokidar": "^1.7.0",
"code-frame": "^5.0.0",
"escape-html": "^1.0.3",
"locate-character": "^2.0.5",
"mkdirp": "^0.5.1",
"relative": "^3.0.2",
"require-relative": "^0.8.7",
"rimraf": "^2.6.2",
"serialize-javascript": "^1.4.0",
"webpack": "^3.10.0",
"webpack-hot-middleware": "^2.21.0"
},
Expand Down
20 changes: 13 additions & 7 deletions src/runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,16 @@ function render(Component: ComponentConstructor, data: any, scroll: ScrollPositi
}
}

function prepare_route(Component, data) {
return Promise.resolve(
Component.preload ? Component.preload(data) : {}
).then(preloaded => {
function prepare_route(Component: ComponentConstructor, data: RouteData) {
if (!Component.preload) {
return { Component, data };
}

if (!component && window.__SAPPER__ && window.__SAPPER__.preloaded) {
return { Component, data: Object.assign(data, window.__SAPPER__.preloaded) };
}

return Promise.resolve(Component.preload(data)).then(preloaded => {
Object.assign(data, preloaded)
return { Component, data };
});
Expand Down Expand Up @@ -176,10 +182,10 @@ export function prefetch(href: string) {
}

function handle_touchstart_mouseover(event: MouseEvent | TouchEvent) {
const a: HTMLAnchorElement = <HTMLAnchorElement>findAnchor(<Node>event.target);
if (!a || a.rel !== 'prefetch') return;
const a: HTMLAnchorElement = <HTMLAnchorElement>findAnchor(<Node>event.target);
if (!a || a.rel !== 'prefetch') return;

prefetch(a.href);
prefetch(a.href);
}

let inited: boolean;
Expand Down
4 changes: 2 additions & 2 deletions test/app/routes/show-url.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<script>
export default {
preload({ url }) {
return { url };
if (url) return { url };
}
};
</script>
</script>
2 changes: 1 addition & 1 deletion test/app/templates/2xx.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@
<!-- Sapper creates a <script> tag containing `templates/main.js`
and anything else it needs to hydrate the app and
initialise the router -->
<script src='%sapper.main%'></script>
%sapper.scripts%
</body>
</html>
87 changes: 30 additions & 57 deletions test/common/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ const fetch = require('node-fetch');
run('production');
run('development');

Nightmare.action('page', {
title(done) {
this.evaluate_now(() => document.querySelector('h1').textContent, done);
}
});

function run(env) {
describe(`env=${env}`, function () {
this.timeout(20000);
Expand Down Expand Up @@ -150,55 +156,38 @@ function run(env) {
});

it('serves /', () => {
return nightmare
.goto(base)
.evaluate(() => document.querySelector('h1').textContent)
.then(title => {
assert.equal(title, 'Great success!');
});
return nightmare.goto(base).page.title().then(title => {
assert.equal(title, 'Great success!');
});
});

it('serves static route', () => {
return nightmare
.goto(`${base}/about`)
.evaluate(() => document.querySelector('h1').textContent)
.then(title => {
assert.equal(title, 'About this site');
});
return nightmare.goto(`${base}/about`).page.title().then(title => {
assert.equal(title, 'About this site');
});
});

it('serves dynamic route', () => {
return nightmare
.goto(`${base}/blog/what-is-sapper`)
.evaluate(() => document.querySelector('h1').textContent)
.then(title => {
assert.equal(title, 'What is Sapper?');
});
return nightmare.goto(`${base}/blog/what-is-sapper`).page.title().then(title => {
assert.equal(title, 'What is Sapper?');
});
});

it('navigates to a new page without reloading', () => {
let requests;
return nightmare
.goto(base).wait(() => window.READY).wait(200)
return nightmare.goto(base).wait(() => window.READY).wait(200)
.then(() => {
return capture(() => {
return nightmare.click('a[href="/about"]');
});
return capture(() => nightmare.click('a[href="/about"]'));
})
.then(reqs => {
requests = reqs;

.then(requests => {
assert.deepEqual(requests.map(r => r.url), []);
return nightmare.path();
})
.then(path => {
assert.equal(path, '/about');

return nightmare.evaluate(() => document.title);
return nightmare.title();
})
.then(title => {
assert.equal(title, 'About');

assert.deepEqual(requests.map(r => r.url), []);
});
});

Expand All @@ -209,7 +198,7 @@ function run(env) {
.click('.goto')
.wait(() => window.location.pathname === '/blog/what-is-sapper')
.wait(100)
.then(() => nightmare.evaluate(() => document.title))
.title()
.then(title => {
assert.equal(title, 'What is Sapper?');
});
Expand Down Expand Up @@ -281,47 +270,31 @@ function run(env) {
.then(() => nightmare.path())
.then(path => {
assert.equal(path, '/about');

return nightmare.evaluate(() => document.querySelector('h1').textContent);
return nightmare.title();
})
.then(header_text => {
assert.equal(header_text, 'About this site');

.then(title => {
assert.equal(title, 'About');
return nightmare.evaluate(() => window.fulfil({})).wait(100);
})
.then(() => nightmare.path())
.then(path => {
assert.equal(path, '/about');

return nightmare.evaluate(() => document.querySelector('h1').textContent);
return nightmare.title();
})
.then(header_text => {
assert.equal(header_text, 'About this site');

return nightmare.evaluate(() => window.fulfil({})).wait(100);
.then(title => {
assert.equal(title, 'About');
});
});

it('passes entire request object to preload', () => {
return nightmare
.goto(`${base}/show-url`)
.wait(() => window.READY)
.evaluate(() => document.querySelector('p').innerHTML)
.then(html => {
.end().then(html => {
assert.equal(html, `URL is /show-url`);
});
});

it('calls a delete handler', () => {
return nightmare
.goto(`${base}/delete-test`)
.wait(() => window.READY)
.click('.del')
.wait(() => window.deleted)
.evaluate(() => window.deleted.id)
.then(id => {
assert.equal(id, 42);
});
});
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome stuff!

Is this test supposed to be removed?

You know this better than me, but is it a good idea to have both package-lock.json and yarn.lock in the same repository?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ack! well spotted, thanks — must have got lost when I was trying to merge some stuff. have added it back.

Bloody lockfiles. Removing yarn.lock

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain the reason for removing lock files? I always commit mine. (Yarn being the preferred tool in my builds.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lock files are used to get consistent installs across machines. Having 2 separate lock files in the same repository violates this out of the gate. Having either yarn.lock or package-lock.json in the repository is a good idea, though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean to say Sapper only supports NPM? That would make more sense. But there's nothing wrong with committing Yarn and NPM lock files—it is just more effort.

Since Yarn has been pretty reliable almost since inception, I barely noticed the release of NPM 5, and just now had to go read up on the difference, including the lock files.

If this ticket is ever closed yarnpkg/yarn#3614 then it may be that Yarn folks won't need to fret about frameworks, etc., that rely solely on NPM, but until then, we should state it more explicitly in the docs that Sapper relies on NPM; use Yarn at your own expense (YMMV).

ALTERNATIVELY, you can commit both lock files and explicitly support both, but give the Sapper team a little more work to do. I could easily see y'all doing the first, with a well-worded explanation somewhere.


describe('headers', () => {
Expand Down Expand Up @@ -355,4 +328,4 @@ function exec(cmd) {
fulfil();
});
});
}
}
Loading