Compare commits

...

26 Commits

Author SHA1 Message Date
Rich Harris
9a936669c6 -> v0.2.9 2017-12-23 15:01:49 -05:00
Rich Harris
0226bd90c6 dont write service-worker.js and index.html to disk in prod mode 2017-12-23 15:01:29 -05:00
Rich Harris
e1926e1bcb -> v0.2.8 2017-12-23 13:16:58 -05:00
Rich Harris
db1c1f332a Add goto function (#46)
* WIP

* programmatic navigation

* get tests working

* wait longer
2017-12-23 13:15:40 -05:00
Rich Harris
e8d510b261 dont use /tmp, it fails when now server wakes after a sleep 2017-12-23 12:27:51 -05:00
Rich Harris
f8e237b265 make invalidation message look less like an error 2017-12-23 10:55:55 -05:00
Rich Harris
68c2f2e388 linting 2017-12-22 09:19:32 -05:00
Rich Harris
0bcb61650b use nightmare for testing 2017-12-21 16:48:53 -05:00
Rich Harris
43a12a8331 -> v0.2.7 2017-12-21 13:40:10 -05:00
Rich Harris
f0feab5738 fix typo 2017-12-21 13:39:13 -05:00
Rich Harris
e9203b4d71 empty .sapper when building 2017-12-21 12:01:53 -05:00
Rich Harris
8e79e706e6 -> v0.2.6 2017-12-21 11:56:04 -05:00
Rich Harris
4b495f44fd Merge pull request #42 from sveltejs/gh-40
render main.js back to templates, to allow relative imports
2017-12-21 11:54:53 -05:00
Rich Harris
222a750b7b render main.js back to templates, to allow relative imports - fixes #40 2017-12-21 11:38:03 -05:00
Rich Harris
5b214c964c -> v0.2.5 2017-12-21 08:59:00 -05:00
Rich Harris
95f99fd378 Merge pull request #39 from sveltejs/windows-nested-routes
use /, not path.sep, when creating routes
2017-12-21 08:55:41 -05:00
Rich Harris
1bed4b0670 use /, not path.sep, when creating routes 2017-12-21 08:46:40 -05:00
Rich Harris
9d4890913a Merge pull request #37 from sveltejs/gh-33
error if routes clash
2017-12-20 21:00:04 -05:00
Rich Harris
f50d3c4262 error if routes clash - fixes #33 2017-12-20 20:46:28 -05:00
Rich Harris
8925e541d5 Merge pull request #36 from sveltejs/gh-19
stream responses
2017-12-20 18:50:06 -05:00
Rich Harris
a48afb77d3 stream responses - fixes #19 2017-12-20 18:39:39 -05:00
Rich Harris
45e845ee92 Merge pull request #35 from sveltejs/gh-2
add preload header for main.js and current route
2017-12-20 18:13:07 -05:00
Rich Harris
492f024d2a add preload header for main.js and current route - fixes #2 2017-12-20 18:04:11 -05:00
Rich Harris
8d40992cf1 Merge pull request #34 from sveltejs/gh-13
Rebundle when routes/templates change
2017-12-20 17:51:04 -05:00
Rich Harris
4232f75b19 rebundle when main.js changes 2017-12-20 17:36:22 -05:00
Rich Harris
fefb0d96d7 rebundle when routes and templates change 2017-12-20 17:34:23 -05:00
34 changed files with 2894 additions and 4532 deletions

View File

@@ -1,7 +1,6 @@
{ {
"root": true, "root": true,
"rules": { "rules": {
"indent": [ 2, "tab", { "SwitchCase": 1 } ],
"semi": [ 2, "always" ], "semi": [ 2, "always" ],
"space-before-blocks": [ 2, "always" ], "space-before-blocks": [ 2, "always" ],
"no-mixed-spaces-and-tabs": [ 2, "smart-tabs" ], "no-mixed-spaces-and-tabs": [ 2, "smart-tabs" ],
@@ -33,7 +32,7 @@
"plugin:import/warnings" "plugin:import/warnings"
], ],
"parserOptions": { "parserOptions": {
"ecmaVersion": 6, "ecmaVersion": 8,
"sourceType": "module" "sourceType": "module"
} }
} }

View File

@@ -1,10 +1,21 @@
sudo: false sudo: false
language: node_js language: node_js
node_js: node_js:
- "stable" - "stable"
env: env:
global: global:
- BUILD_TIMEOUT=10000 - BUILD_TIMEOUT=10000
addons:
apt:
packages:
- xvfb
install: install:
- export DISPLAY=':99.0'
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
- npm install - npm install
- (cd test/app && npm install) - (cd test/app && npm install)

View File

@@ -1,5 +1,30 @@
# sapper changelog # sapper changelog
## 0.2.9
* Don't write files to disk in prod mode
## 0.2.8
* Add `goto` function ([#29](https://github.com/sveltejs/sapper/issues/29))
* Don't use `/tmp` as destination in Now environments
## 0.2.7
* Fix streaming bug
## 0.2.6
* Render main.js back to templates, to allow relative imports ([#40](https://github.com/sveltejs/sapper/issues/40))
## 0.2.5
* Fix nested routes on Windows ([#39](https://github.com/sveltejs/sapper/pull/39))
* Rebundle when routes and main.js change ([#34](https://github.com/sveltejs/sapper/pull/34))
* Add `Link...preload` headers for JavaScript assets ([#2](https://github.com/sveltejs/sapper/issues/2))
* Stream document up to first dynamic content ([#19](https://github.com/sveltejs/sapper/issues/19))
* Error if routes clash ([#33](https://github.com/sveltejs/sapper/issues/33))
## 0.2.4 ## 0.2.4
* Posixify path to HMR client * Posixify path to HMR client

View File

@@ -1,14 +1,15 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const glob = require('glob');
const mkdirp = require('mkdirp'); const mkdirp = require('mkdirp');
const rimraf = require('rimraf');
const { client, server } = require('./utils/compilers.js'); const { client, server } = require('./utils/compilers.js');
const create_app = require('./utils/create_app.js'); const create_app = require('./utils/create_app.js');
const generate_asset_cache = require('./utils/generate_asset_cache.js'); const generate_asset_cache = require('./utils/generate_asset_cache.js');
const { dest } = require('./config.js'); const { dest } = require('./config.js');
module.exports = () => { module.exports = () => {
mkdirp(dest); mkdirp.sync(dest);
rimraf.sync(path.join(dest, '**/*'));
// create main.js and server-routes.js // create main.js and server-routes.js
create_app(); create_app();

View File

@@ -8,14 +8,14 @@ exports.templates = path.resolve(process.env.SAPPER_TEMPLATES || 'templates');
exports.src = path.resolve(process.env.SAPPER_ROUTES || 'routes'); exports.src = path.resolve(process.env.SAPPER_ROUTES || 'routes');
exports.dest = path.resolve( exports.dest = path.resolve(process.env.SAPPER_DEST || '.sapper');
process.env.NOW ? '/tmp' :
process.env.SAPPER_DEST || '.sapper'
);
if (exports.dev) { if (exports.dev) {
mkdirp(exports.dest); mkdirp.sync(exports.dest);
rimraf.sync(path.join(exports.dest, '**/*')); rimraf.sync(path.join(exports.dest, '**/*'));
} }
exports.server_routes = path.resolve(exports.dest, 'server-routes.js'); exports.entry = {
client: path.resolve(exports.templates, '.main.rendered.js'),
server: path.resolve(exports.dest, 'server-entry.js')
};

View File

@@ -1,9 +1,5 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const glob = require('glob');
const rimraf = require('rimraf');
const mkdirp = require('mkdirp');
const webpack = require('webpack');
const route_manager = require('./route_manager.js'); const route_manager = require('./route_manager.js');
const templates = require('./templates.js'); const templates = require('./templates.js');
const create_app = require('./utils/create_app.js'); const create_app = require('./utils/create_app.js');
@@ -11,18 +7,16 @@ const create_watcher = require('./utils/create_watcher.js');
const compilers = require('./utils/compilers.js'); const compilers = require('./utils/compilers.js');
const generate_asset_cache = require('./utils/generate_asset_cache.js'); const generate_asset_cache = require('./utils/generate_asset_cache.js');
const escape_html = require('escape-html'); const escape_html = require('escape-html');
const { src, dest, dev } = require('./config.js'); const { dest, dev } = require('./config.js');
function connect_dev() { function connect_dev() {
// create main.js and server-routes.js
// TODO update on changes
create_app(); create_app();
const watcher = create_watcher(); const watcher = create_watcher();
let asset_cache; let asset_cache;
return compose_handlers([ const middleware = compose_handlers([
require('webpack-hot-middleware')(compilers.client, { require('webpack-hot-middleware')(compilers.client, {
reload: true, reload: true,
path: '/__webpack_hmr', path: '/__webpack_hmr',
@@ -59,8 +53,15 @@ function connect_dev() {
get_route_handler(() => asset_cache), get_route_handler(() => asset_cache),
not_found get_not_found_handler(() => asset_cache)
]); ]);
middleware.close = () => {
watcher.close();
// TODO shut down chokidar
};
return middleware;
} }
function connect_prod() { function connect_prod() {
@@ -69,7 +70,7 @@ function connect_prod() {
read_json(path.join(dest, 'stats.server.json')) read_json(path.join(dest, 'stats.server.json'))
); );
return compose_handlers([ const middleware = compose_handlers([
set_req_pathname, set_req_pathname,
get_asset_handler({ get_asset_handler({
@@ -95,8 +96,14 @@ function connect_prod() {
get_route_handler(() => asset_cache), get_route_handler(() => asset_cache),
not_found get_not_found_handler(() => asset_cache)
]); ]);
// here for API consistency between dev, and prod, but
// doesn't actually need to do anything
middleware.close = () => {};
return middleware;
} }
module.exports = dev ? connect_dev : connect_prod; module.exports = dev ? connect_dev : connect_prod;
@@ -137,20 +144,36 @@ function get_route_handler(fn) {
const mod = require(server.entry)[route.id]; const mod = require(server.entry)[route.id];
if (route.type === 'page') { if (route.type === 'page') {
let data = { params: req.params, query: req.query }; // preload main.js and current route
if (mod.preload) data = Object.assign(data, await mod.preload(data)); // TODO detect other stuff we can preload? images, CSS, fonts?
res.set('Link', `<${client.main_file}>;rel="preload";as="script", <${client.routes[route.id]}>;rel="preload";as="script"`);
const { html, head, css } = mod.render(data); const data = { params: req.params, query: req.query };
const page = templates.render(200, { if (mod.preload) {
main: client.main_file, const promise = Promise.resolve(mod.preload(data)).then(preloaded => {
html, Object.assign(data, preloaded);
head: `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`, return mod.render(data);
styles: (css && css.code ? `<style>${css.code}</style>` : '') });
});
res.status(200); await templates.stream(res, 200, {
res.end(page); 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>` : ''))
});
} else {
const { html, head, css } = mod.render(data);
const page = templates.render(200, {
main: client.main_file,
html,
head: `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`,
styles: (css && css.code ? `<style>${css.code}</style>` : '')
});
res.end(page);
}
} }
else { else {
@@ -164,23 +187,30 @@ function get_route_handler(fn) {
next(); next();
} catch(err) { } catch(err) {
res.status(500).end(templates.render(500, { res.status(500);
res.end(templates.render(500, {
title: (err && err.name) || 'Internal server error', title: (err && err.name) || 'Internal server error',
url, url,
error: escape_html(err && (err.details || err.message || err) || 'Unknown error'), error: escape_html(err && (err.details || err.message || err) || 'Unknown error'),
stack: err && err.stack.split('\n').slice(1).join('\n') stack: err && err.stack.split('\n').slice(1).join('\n')
})); }));
} }
} };
} }
function not_found(req, res) { function get_not_found_handler(fn) {
res.status(404).end(templates.render(404, { return function handle_not_found(req, res) {
title: 'Not found', const asset_cache = fn();
status: 404,
method: req.method, res.status(404);
url: req.url res.end(templates.render(404, {
})); title: 'Not found',
status: 404,
method: req.method,
main: asset_cache.client.main_file,
url: req.url
}));
};
} }
function compose_handlers(handlers) { function compose_handlers(handlers) {
@@ -200,7 +230,7 @@ function compose_handlers(handlers) {
} }
go(); go();
} };
} }
function read_json(file) { function read_json(file) {

View File

@@ -1,16 +1,31 @@
const glob = require('glob'); const glob = require('glob');
const chokidar = require('chokidar');
const create_routes = require('./utils/create_routes.js'); const create_routes = require('./utils/create_routes.js');
const { src } = require('./config.js'); const { src, dev } = require('./config.js');
const route_manager = { const callbacks = [];
routes: create_routes(
glob.sync('**/*.+(html|js|mjs)', { cwd: src })
),
onchange(fn) { exports.onchange = fn => {
// TODO in dev mode, keep this updated, and allow callbacks.push(fn);
// webpack compiler etc to hook into it
}
}; };
module.exports = route_manager; function update() {
exports.routes = create_routes(
glob.sync('**/*.+(html|js|mjs)', { cwd: src })
);
callbacks.forEach(fn => fn());
}
update();
if (dev) {
const watcher = chokidar.watch(`${src}/**/*.+(html|js|mjs)`, {
ignoreInitial: true,
persistent: false
});
watcher.on('add', update);
watcher.on('change', update);
watcher.on('unlink', update);
}

View File

@@ -1,34 +1,79 @@
const fs = require('fs'); const fs = require('fs');
const glob = require('glob'); const glob = require('glob');
const chokidar = require('chokidar');
const { dev } = require('./config.js');
const templates = glob.sync('*.html', { cwd: 'templates' }) let templates;
.map(file => {
const template = fs.readFileSync(`templates/${file}`, 'utf-8');
const status = file.replace('.html', '').toLowerCase();
if (!/^[0-9x]{3}$/.test(status)) { function create_templates() {
throw new Error(`Bad template — should be a valid status code like 404.html, or a wildcard like 2xx.html`); templates = glob.sync('*.html', { cwd: 'templates' })
} .map(file => {
const template = fs.readFileSync(`templates/${file}`, 'utf-8');
const status = file.replace('.html', '').toLowerCase();
const specificity = ( if (!/^[0-9x]{3}$/.test(status)) {
(status[0] === 'x' ? 0 : 4) + throw new Error(`Bad template — should be a valid status code like 404.html, or a wildcard like 2xx.html`);
(status[1] === 'x' ? 0 : 2) +
(status[2] === 'x' ? 0 : 1)
);
const pattern = new RegExp(`^${status.split('').map(d => d === 'x' ? '\\d' : d).join('')}$`);
return {
test: status => pattern.test(status),
specificity,
render(data) {
return template.replace(/%sapper\.(\w+)%/g, (match, key) => {
return key in data ? data[key] : '';
});
} }
}
}) const specificity = (
.sort((a, b) => b.specificity - a.specificity); (status[0] === 'x' ? 0 : 4) +
(status[1] === 'x' ? 0 : 2) +
(status[2] === 'x' ? 0 : 1)
);
const pattern = new RegExp(`^${status.split('').map(d => d === 'x' ? '\\d' : d).join('')}$`);
return {
test: status => pattern.test(status),
specificity,
render: data => {
return template.replace(/%sapper\.(\w+)%/g, (match, key) => {
return key in data ? data[key] : '';
});
},
stream: async (res, data) => {
let i = 0;
do {
const start = template.indexOf('%sapper', i);
if (start === -1) {
res.end(template.slice(i));
return;
}
res.write(template.slice(i, start));
const end = template.indexOf('%', start + 1);
if (end === -1) {
throw new Error(`Bad template`); // TODO validate ahead of time
}
const tag = template.slice(start + 1, end);
const match = /sapper\.(\w+)/.exec(tag);
if (!match || !(match[1] in data)) throw new Error(`Bad template`); // TODO ditto
res.write(await data[match[1]]);
i = end + 1;
} while (i < template.length);
}
};
})
.sort((a, b) => b.specificity - a.specificity);
}
create_templates();
if (dev) {
const watcher = chokidar.watch('templates/**.html', {
ignoreInitial: true,
persistent: false
});
watcher.on('add', create_templates);
watcher.on('change', create_templates);
watcher.on('unlink', create_templates);
}
exports.render = (status, data) => { exports.render = (status, data) => {
const template = templates.find(template => template.test(status)); const template = templates.find(template => template.test(status));
@@ -37,7 +82,9 @@ exports.render = (status, data) => {
return `Missing template for status code ${status}`; return `Missing template for status code ${status}`;
}; };
exports.onchange = fn => { exports.stream = (res, status, data) => {
// TODO in dev mode, keep this updated, and allow const template = templates.find(template => template.test(status));
// webpack compiler etc to hook into it if (template) return template.stream(res, data);
return `Missing template for status code ${status}`;
}; };

View File

@@ -1,13 +1,14 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const chokidar = require('chokidar');
const route_manager = require('../route_manager.js'); const route_manager = require('../route_manager.js');
const { src, dest, server_routes, dev } = require('../config.js'); const { src, entry, dev } = require('../config.js');
function posixify(file) { function posixify(file) {
return file.replace(/[\/\\]/g, '/'); return file.replace(/[/\\]/g, '/');
} }
module.exports = function create_app() { function create_app() {
const { routes } = route_manager; const { routes } = route_manager;
function create_client_main() { function create_client_main() {
@@ -37,13 +38,11 @@ module.exports = function create_app() {
main += `\n\nimport('${hmr_client}?path=/__webpack_hmr&timeout=20000'); if (module.hot) module.hot.accept();` main += `\n\nimport('${hmr_client}?path=/__webpack_hmr&timeout=20000'); if (module.hot) module.hot.accept();`
} }
const file = path.resolve(dest, 'main.js'); fs.writeFileSync(entry.client, main);
fs.writeFileSync(file, main);
// need to fudge the mtime, because webpack is soft in the head // need to fudge the mtime, because webpack is soft in the head
const { atime, mtime } = fs.statSync(file); const { atime, mtime } = fs.statSync(entry.client);
fs.utimesSync(file, new Date(atime.getTime() - 999999), new Date(mtime.getTime() - 999999)); fs.utimesSync(entry.client, new Date(atime.getTime() - 999999), new Date(mtime.getTime() - 999999));
} }
function create_server_routes() { function create_server_routes() {
@@ -58,13 +57,27 @@ module.exports = function create_app() {
const exports = `export { ${routes.map(route => route.id)} };`; const exports = `export { ${routes.map(route => route.id)} };`;
fs.writeFileSync(server_routes, `${imports}\n\n${exports}`); fs.writeFileSync(entry.server, `${imports}\n\n${exports}`);
const { atime, mtime } = fs.statSync(server_routes); const { atime, mtime } = fs.statSync(entry.server);
fs.utimesSync(server_routes, new Date(atime.getTime() - 999999), new Date(mtime.getTime() - 999999)); fs.utimesSync(entry.server, new Date(atime.getTime() - 999999), new Date(mtime.getTime() - 999999));
} }
// TODO in dev mode, watch files
create_client_main(); create_client_main();
create_server_routes(); create_server_routes();
}; }
if (dev) {
route_manager.onchange(create_app);
const watcher = chokidar.watch(`templates/main.js`, {
ignoreInitial: true,
persistent: false
});
watcher.on('add', create_app);
watcher.on('change', create_app);
watcher.on('unlink', create_app);
}
module.exports = create_app;

View File

@@ -1,11 +1,11 @@
const path = require('path'); const path = require('path');
module.exports = function create_matchers(files) { module.exports = function create_matchers(files) {
return files const routes = files
.map(file => { .map(file => {
if (/(^|\/|\\)_/.test(file)) return; if (/(^|\/|\\)_/.test(file)) return;
const parts = file.replace(/\.(html|js|mjs)$/, '').split(path.sep); const parts = file.replace(/\.(html|js|mjs)$/, '').split('/'); // glob output is always posix-style
if (parts[parts.length - 1] === 'index') parts.pop(); if (parts[parts.length - 1] === 'index') parts.pop();
const id = ( const id = (
@@ -47,9 +47,31 @@ module.exports = function create_matchers(files) {
}) })
.filter(Boolean) .filter(Boolean)
.sort((a, b) => { .sort((a, b) => {
return ( let same = true;
(a.dynamic.length - b.dynamic.length) || // match static paths first
(b.parts.length - a.parts.length) // match longer paths first for (let i = 0; true; i += 1) {
); const a_part = a.parts[i];
const b_part = b.parts[i];
if (!a_part && !b_part) {
if (same) throw new Error(`The ${a.file} and ${b.file} routes clash`);
return 0;
}
if (!a_part) return 1;
if (!b_part) return -1;
const a_is_dynamic = a_part[0] === '[';
const b_is_dynamic = b_part[0] === '[';
if (a_is_dynamic === b_is_dynamic) {
if (!a_is_dynamic) same = false;
continue;
}
return a_is_dynamic ? 1 : -1;
}
}); });
}
return routes;
};

View File

@@ -1,7 +1,9 @@
const fs = require('fs');
const path = require('path'); const path = require('path');
const chalk = require('chalk'); const chalk = require('chalk');
const compilers = require('./compilers.js'); const compilers = require('./compilers.js');
const generate_asset_cache = require('./generate_asset_cache.js'); const generate_asset_cache = require('./generate_asset_cache.js');
const { dest } = require('../config.js');
function deferred() { function deferred() {
const d = {}; const d = {};
@@ -24,21 +26,23 @@ module.exports = function create_watcher() {
deferreds.client.promise, deferreds.client.promise,
deferreds.server.promise deferreds.server.promise
]).then(([client_stats, server_stats]) => { ]).then(([client_stats, server_stats]) => {
const client_info = client_stats.toJson();
fs.writeFileSync(path.join(dest, 'stats.client.json'), JSON.stringify(client_info, null, ' '));
const server_info = server_stats.toJson();
fs.writeFileSync(path.join(dest, 'stats.server.json'), JSON.stringify(server_info, null, ' '));
return generate_asset_cache( return generate_asset_cache(
client_stats.toJson(), client_stats.toJson(),
server_stats.toJson() server_stats.toJson()
); );
}); });
watcher = {
ready: invalidate()
};
function watch_compiler(type) { function watch_compiler(type) {
const compiler = compilers[type]; const compiler = compilers[type];
compiler.plugin('invalid', filename => { compiler.plugin('invalid', filename => {
console.log(chalk.red(`${type} bundle invalidated, file changed: ${chalk.bold(filename)}`)); console.log(chalk.cyan(`${type} bundle invalidated, file changed: ${chalk.bold(filename)}`));
deferreds[type] = deferred(); deferreds[type] = deferred();
watcher.ready = invalidate(); watcher.ready = invalidate();
}); });
@@ -47,7 +51,7 @@ module.exports = function create_watcher() {
deferreds[type].reject(err); deferreds[type].reject(err);
}); });
compiler.watch({}, (err, stats) => { return compiler.watch({}, (err, stats) => {
if (stats.hasErrors()) { if (stats.hasErrors()) {
deferreds[type].reject(stats.toJson().errors[0]); deferreds[type].reject(stats.toJson().errors[0]);
} else { } else {
@@ -56,8 +60,16 @@ module.exports = function create_watcher() {
}); });
} }
watch_compiler('client'); const watcher = {
watch_compiler('server'); ready: invalidate(),
client: watch_compiler('client'),
server: watch_compiler('server'),
close: () => {
watcher.client.close();
watcher.server.close();
}
};
return watcher; return watcher;
}; };

View File

@@ -3,17 +3,24 @@ const path = require('path');
const glob = require('glob'); const glob = require('glob');
const templates = require('../templates.js'); const templates = require('../templates.js');
const route_manager = require('../route_manager.js'); const route_manager = require('../route_manager.js');
const { dest } = require('../config.js'); const { dest, dev } = require('../config.js');
function ensure_array(thing) {
return Array.isArray(thing) ? thing : [thing]; // omg webpack what the HELL are you doing
}
module.exports = function generate_asset_cache(clientInfo, serverInfo) { module.exports = function generate_asset_cache(clientInfo, serverInfo) {
const main_file = `/client/${clientInfo.assetsByChunkName.main}`; const main_file = `/client/${ensure_array(clientInfo.assetsByChunkName.main)[0]}`;
const chunk_files = clientInfo.assets.map(chunk => `/client/${chunk.name}`); const chunk_files = clientInfo.assets.map(chunk => `/client/${chunk.name}`);
const service_worker = generate_service_worker(chunk_files); const service_worker = generate_service_worker(chunk_files);
const index = generate_index(main_file); const index = generate_index(main_file);
fs.writeFileSync(path.join(dest, 'service-worker.js'), service_worker); if (dev) {
fs.writeFileSync(path.join(dest, 'index.html'), index); fs.writeFileSync(path.join(dest, 'service-worker.js'), service_worker);
fs.writeFileSync(path.join(dest, 'index.html'), index);
}
return { return {
client: { client: {
@@ -26,12 +33,17 @@ module.exports = function generate_asset_cache(clientInfo, serverInfo) {
return lookup; return lookup;
}, {}), }, {}),
routes: route_manager.routes.reduce((lookup, route) => {
lookup[route.id] = `/client/${ensure_array(clientInfo.assetsByChunkName[route.id])[0]}`;
return lookup;
}, {}),
index, index,
service_worker service_worker
}, },
server: { server: {
entry: path.resolve(dest, 'server', serverInfo.assetsByChunkName.server_routes) entry: path.resolve(dest, 'server', serverInfo.assetsByChunkName.main)
} }
}; };
}; };

View File

@@ -1,2 +1,3 @@
--recursive --recursive
test/unit/**/*.js test/unit/**/*.js
test/common/test.js

3239
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "sapper", "name": "sapper",
"version": "0.2.4", "version": "0.2.9",
"description": "Military-grade apps, engineered by Svelte", "description": "Military-grade apps, engineered by Svelte",
"main": "lib/index.js", "main": "lib/index.js",
"bin": { "bin": {
@@ -11,6 +11,7 @@
}, },
"dependencies": { "dependencies": {
"chalk": "^2.3.0", "chalk": "^2.3.0",
"chokidar": "^1.7.0",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"relative": "^3.0.2", "relative": "^3.0.2",
@@ -20,22 +21,23 @@
"webpack-hot-middleware": "^2.21.0" "webpack-hot-middleware": "^2.21.0"
}, },
"devDependencies": { "devDependencies": {
"cypress": "^1.3.0", "css-loader": "^0.28.7",
"eslint": "^4.13.1", "eslint": "^4.13.1",
"eslint-plugin-import": "^2.8.0",
"express": "^4.16.2",
"get-port": "^3.2.0",
"mocha": "^4.0.1", "mocha": "^4.0.1",
"nightmare": "^2.10.0",
"node-fetch": "^1.7.3",
"npm-run-all": "^4.1.2", "npm-run-all": "^4.1.2",
"style-loader": "^0.19.1",
"svelte": "^1.49.1",
"svelte-loader": "^2.3.2",
"wait-on": "^2.0.2" "wait-on": "^2.0.2"
}, },
"scripts": { "scripts": {
"cy:open": "cypress open", "cy:open": "cypress open",
"test": "run-s test:unit test:dev test:prod", "test": "mocha --opts mocha.opts"
"test:unit": "mocha --opts mocha.opts",
"test:dev": "run-p --race test:launch:dev cy:run:dev",
"test:launch:dev": "node test/launch.js --dev",
"cy:run:dev": "wait-on http://localhost:3000 && cypress run -s test/cypress/integration/dev.js",
"test:prod": "run-p --race test:launch:prod cy:run:prod",
"test:launch:prod": "node test/launch.js --prod",
"cy:run:prod": "wait-on http://localhost:3000 && cypress run -s test/cypress/integration/prod.js"
}, },
"repository": "https://github.com/sveltejs/sapper", "repository": "https://github.com/sveltejs/sapper",
"keywords": [ "keywords": [

View File

@@ -6,12 +6,17 @@ export let component;
let target; let target;
let routes; let routes;
const history = typeof window !== 'undefined' ? window.history : {
pushState: () => {},
replaceState: () => {},
};
const scroll_history = {}; const scroll_history = {};
let uid = 1; let uid = 1;
let cid; let cid;
if ('scrollRestoration' in history) { if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual' history.scrollRestoration = 'manual';
} }
function select_route(url) { function select_route(url) {
@@ -39,7 +44,7 @@ function render(Component, data, scroll) {
} else { } else {
// first load — remove SSR'd <head> contents // first load — remove SSR'd <head> contents
const start = document.querySelector('#sapper-head-start'); const start = document.querySelector('#sapper-head-start');
let end = document.querySelector('#sapper-head-end'); const end = document.querySelector('#sapper-head-end');
if (start && end) { if (start && end) {
while (start.nextSibling !== end) detach(start.nextSibling); while (start.nextSibling !== end) detach(start.nextSibling);
@@ -75,8 +80,6 @@ function navigate(url, id) {
id = cid = ++uid; id = cid = ++uid;
scroll_history[cid] = { x: 0, y: 0 }; scroll_history[cid] = { x: 0, y: 0 };
history.pushState({ id }, '', url.href);
} }
selected.route.load().then(mod => { selected.route.load().then(mod => {
@@ -123,6 +126,7 @@ function handle_click(event) {
if (navigate(url, null)) { if (navigate(url, null)) {
event.preventDefault(); event.preventDefault();
history.pushState({ id: cid }, '', url.href);
} }
} }
@@ -173,7 +177,7 @@ export function init(_target, _routes) {
inited = true; inited = true;
} }
const scroll = scroll_history[uid] = scroll_state(); scroll_history[uid] = scroll_state();
history.replaceState({ id: uid }, '', window.location.href); history.replaceState({ id: uid }, '', window.location.href);
navigate(new URL(window.location), uid); navigate(new URL(window.location), uid);
@@ -189,4 +193,12 @@ function scroll_state() {
x: window.scrollX, x: window.scrollX,
y: window.scrollY y: window.scrollY
}; };
}
export function goto(href, opts = {}) {
if (navigate(new URL(href, window.location.href))) {
if (history) history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
} else {
window.location.href = href;
}
} }

1
test/app/.gitignore vendored
View File

@@ -3,3 +3,4 @@ node_modules
.sapper .sapper
yarn.lock yarn.lock
cypress/screenshots cypress/screenshots
templates/.*

View File

@@ -6,14 +6,21 @@
<h1>About this site</h1> <h1>About this site</h1>
<p>This is the 'about' page. There's not much here.</p> <p>This is the 'about' page. There's not much here.</p>
<button on:click='goto("/blog/what-is-sapper")'>What is Sapper?</button>
</Layout> </Layout>
<script> <script>
import Layout from './_components/Layout.html'; import Layout from './_components/Layout.html';
import { goto } from '../../../runtime/app.js';
export default { export default {
components: { components: {
Layout Layout
},
methods: {
goto
} }
}; };
</script> </script>

View File

@@ -10,9 +10,9 @@
<link rel='icon' type='image/png' href='/favicon.png'> <link rel='icon' type='image/png' href='/favicon.png'>
<script> <script>
if ('serviceWorker' in navigator) { // if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js'); // navigator.serviceWorker.register('/service-worker.js');
} // }
</script> </script>
<!-- Sapper generates a <style> tag containing critical CSS <!-- Sapper generates a <style> tag containing critical CSS

View File

@@ -1,10 +1,6 @@
import { init } from '__app__'; import { init } from '../../../runtime/app.js';
// `routes` is an array of route objects injected by Sapper // `routes` is an array of route objects injected by Sapper
init(document.querySelector('#sapper'), __routes__); init(document.querySelector('#sapper'), __routes__);
// if (__dev__) { window.READY = true;
// // Enable hot-module reloading
// import('sapper/webpack/hmr');
// if (module.hot) module.hot.accept();
// }

View File

@@ -1,7 +1,5 @@
const config = require('../../webpack/config.js'); const config = require('../../webpack/config.js');
const webpack = require('webpack'); const webpack = require('webpack');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
module.exports = { module.exports = {
entry: config.client.entry(), entry: config.client.entry(),
@@ -24,27 +22,18 @@ module.exports = {
} }
} }
}, },
config.dev && { {
test: /\.css$/, test: /\.css$/,
use: [ use: [
{ loader: "style-loader" }, { loader: "style-loader" },
{ loader: "css-loader" } { loader: "css-loader" }
] ]
},
!config.dev && {
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [{ loader: 'css-loader', options: { sourceMap: config.dev } }]
})
} }
].filter(Boolean) ].filter(Boolean)
}, },
plugins: [ plugins: [
config.dev && new webpack.HotModuleReplacementPlugin(), config.dev && new webpack.HotModuleReplacementPlugin(),
!config.dev && new ExtractTextPlugin('main.css'), !config.dev && new webpack.optimize.ModuleConcatenationPlugin()
!config.dev && new webpack.optimize.ModuleConcatenationPlugin(),
!config.dev && new UglifyJSPlugin()
].filter(Boolean), ].filter(Boolean),
devtool: config.dev ? 'inline-source-map' : false devtool: config.dev ? 'inline-source-map' : false
}; };

View File

@@ -1,7 +1,4 @@
const config = require('../../webpack/config.js'); const config = require('../../webpack/config.js');
const webpack = require('webpack');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
module.exports = { module.exports = {
entry: config.server.entry(), entry: config.server.entry(),

234
test/common/test.js Normal file
View File

@@ -0,0 +1,234 @@
const path = require('path');
const assert = require('assert');
const express = require('express');
const serve = require('serve-static');
const Nightmare = require('nightmare');
const getPort = require('get-port');
const fetch = require('node-fetch');
run('production');
run('development');
function run(env) {
describe(`env=${env}`, function () {
this.timeout(5000);
let PORT;
let server;
let nightmare;
let middleware;
let capture;
let base;
function get(url) {
return new Promise(fulfil => {
const req = {
url,
method: 'GET'
};
const result = {
headers: {},
body: ''
};
const res = {
set: (headers, value) => {
if (typeof headers === 'string') {
return res.set({ [headers]: value });
}
Object.assign(result.headers, headers);
},
status: code => {
result.status = code;
},
write: data => {
result.body += data;
},
end: data => {
result.body += data;
fulfil(result);
}
};
middleware(req, res, () => {
fulfil(result);
});
});
}
before(async () => {
process.chdir(path.resolve(__dirname, '../app'));
process.env.NODE_ENV = env;
if (env === 'production') {
const cli = path.resolve(__dirname, '../../cli/index.js');
await exec(`${cli} build`);
}
const resolved = require.resolve('../..');
delete require.cache[resolved];
const sapper = require(resolved);
PORT = await getPort();
base = `http://localhost:${PORT}`;
global.fetch = (url, opts) => {
if (url[0] === '/') url = `${base}${url}`;
return fetch(url, opts);
};
let captured;
capture = async fn => {
const result = captured = [];
await fn();
captured = null;
return result;
};
const app = express();
app.use(serve('assets'));
app.use((req, res, next) => {
if (captured) captured.push(req);
next();
});
middleware = sapper();
app.use(middleware);
return new Promise((fulfil, reject) => {
server = app.listen(PORT, err => {
if (err) reject(err);
else fulfil();
});
});
});
after(() => {
server.close();
middleware.close();
// give a chance to clean up
return new Promise(fulfil => setTimeout(fulfil, 500));
});
describe('basic functionality', () => {
beforeEach(() => {
nightmare = new Nightmare();
nightmare.on('console', (type, ...args) => {
console[type](...args);
});
nightmare.on('page', (type, ...args) => {
if (type === 'error') {
console.error(args[1]);
} else {
console.warn(type, args);
}
});
});
afterEach(async () => {
await nightmare.end();
});
it('serves /', async () => {
const title = await nightmare
.goto(base)
.evaluate(() => document.querySelector('h1').textContent);
assert.equal(title, 'Great success!');
});
it('serves static route', async () => {
const title = await nightmare
.goto(`${base}/about`)
.evaluate(() => document.querySelector('h1').textContent);
assert.equal(title, 'About this site');
});
it('serves dynamic route', async () => {
const title = await nightmare
.goto(`${base}/blog/what-is-sapper`)
.evaluate(() => document.querySelector('h1').textContent);
assert.equal(title, 'What is Sapper?');
});
it('navigates to a new page without reloading', async () => {
await nightmare.goto(base);
const requests = await capture(async () => {
await nightmare.click('a[href="/about"]');
});
assert.equal(
await nightmare.path(),
'/about'
);
assert.equal(
await nightmare.evaluate(() => document.title),
'About'
);
assert.deepEqual(requests.map(r => r.url), []);
});
it('navigates programmatically', async () => {
await nightmare
.goto(`${base}/about`)
.wait(() => window.READY)
.click('button')
.wait(() => window.location.pathname === '/blog/what-is-sapper')
.wait(100);
assert.equal(
await nightmare.evaluate(() => document.title),
'What is Sapper?'
);
});
});
describe('headers', () => {
it('sets Content-Type and Link...preload headers', async () => {
const { headers } = await get('/');
assert.equal(
headers['Content-Type'],
'text/html'
);
assert.ok(
/<\/client\/main.\w+\.js>;rel="preload";as="script", <\/client\/_.\d+.\w+.js>;rel="preload";as="script"/.test(headers['Link']),
headers['Link']
);
});
});
});
}
function exec(cmd) {
return new Promise((fulfil, reject) => {
require('child_process').exec(cmd, (err, stdout, stderr) => {
if (err) {
process.stdout.write(stdout);
process.stderr.write(stderr);
return reject(err);
}
fulfil();
});
});
}

View File

@@ -1,5 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@@ -1,19 +0,0 @@
describe('dev mode', () => {
beforeEach(() => {
cy.visit('/')
});
it('has the correct <h1>', () => {
cy.contains('h1', 'Great success!')
});
it('navigates to /about', () => {
cy.get('nav a').contains('about').click();
cy.url().should('include', '/about');
});
it('navigates to /blog', () => {
cy.get('nav a').contains('blog').click();
cy.url().should('include', '/blog');
});
});

View File

@@ -1,19 +0,0 @@
describe('prod mode', () => {
beforeEach(() => {
cy.visit('/')
});
it('has the correct <h1>', () => {
cy.contains('h1', 'Great success!')
});
it('navigates to /about', () => {
cy.get('nav a').contains('about').click();
cy.url().should('include', '/about');
});
it('navigates to /blog', () => {
cy.get('nav a').contains('blog').click();
cy.url().should('include', '/blog');
});
});

View File

@@ -1,17 +0,0 @@
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 KiB

View File

@@ -1,25 +0,0 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })

View File

@@ -1,20 +0,0 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@@ -1,75 +0,0 @@
const fs = require('fs');
const path = require('path');
const rimraf = require('rimraf');
const child_process = require('child_process');
// ensure sapper doesn't exist in app/node_modules
rimraf.sync(
path.join(__dirname, 'app/node_modules/sapper')
);
rimraf.sync(
path.join(__dirname, 'app/node_modules/.bin/sapper')
);
// create symlinks
fs.symlinkSync(
path.join(__dirname, '..'),
path.join(__dirname, 'app/node_modules/sapper')
);
fs.symlinkSync(
path.join(__dirname, '../cli/index.js'),
path.join(__dirname, 'app/node_modules/.bin/sapper')
);
const app_dir = path.join(__dirname, 'app');
function start_server() {
const server = child_process.spawn(process.execPath, ['server.js'], {
cwd: app_dir,
env: {
NODE_ENV: 'development'
},
stdio: 'pipe'
});
server.stdout.on('data', (data) => {
process.stdout.write(data);
});
server.stderr.on('data', (data) => {
process.stderr.write(data);
});
}
function launch() {
if (process.argv[2] === '--dev') {
start_server();
} else {
child_process.exec(`npm run build`, {
cwd: app_dir
}, (err, stdout, stderr) => {
if (err) throw err;
process.stdout.write(stdout);
process.stderr.write(stderr);
start_server();
});
}
}
// this is a terrible hack
if (process.env.APPVEYOR) {
child_process.exec(`npm install`, {
cwd: app_dir
}, (err, stdout, stderr) => {
if (err) throw err;
process.stdout.write(stdout);
process.stderr.write(stderr);
launch();
});
} else {
launch();
}

View File

@@ -5,15 +5,17 @@ const create_routes = require('../../lib/utils/create_routes.js');
describe('create_routes', () => { describe('create_routes', () => {
it('sorts routes correctly', () => { it('sorts routes correctly', () => {
const routes = create_routes(['index.html', 'about.html', '[wildcard].html', 'post/[id].html']); const routes = create_routes(['index.html', 'about.html', '[wildcard].html', 'post/foo.html', 'post/[id].html', 'post/bar.html']);
assert.deepEqual( assert.deepEqual(
routes.map(r => r.file), routes.map(r => r.file),
[ [
'about.html', 'post/foo.html',
'index.html', 'post/bar.html',
'post/[id].html', 'post/[id].html',
'[wildcard].html' 'about.html',
'[wildcard].html',
'index.html'
] ]
); );
}); });
@@ -48,4 +50,25 @@ describe('create_routes', () => {
] ]
); );
}); });
it('matches /foo/:bar before /:baz/qux', () => {
const a = create_routes(['foo/[bar].html', '[baz]/qux.html']);
const b = create_routes(['[baz]/qux.html', 'foo/[bar].html']);
assert.deepEqual(
a.map(r => r.file),
['foo/[bar].html', '[baz]/qux.html']
);
assert.deepEqual(
b.map(r => r.file),
['foo/[bar].html', '[baz]/qux.html']
);
});
it('fails if routes are indistinguishable', () => {
assert.throws(() => {
create_routes(['[foo].html', '[bar]/index.html']);
}, /The \[foo\].html and \[bar\]\/index.html routes clash/);
});
}); });

View File

@@ -1,6 +1,5 @@
const path = require('path'); const path = require('path');
const route_manager = require('../lib/route_manager.js'); const { src, dest, dev, entry } = require('../lib/config.js');
const { src, dest, dev, server_routes } = require('../lib/config.js');
module.exports = { module.exports = {
dev, dev,
@@ -8,7 +7,7 @@ module.exports = {
client: { client: {
entry: () => { entry: () => {
return { return {
main: `${dest}/main.js` main: entry.client
}; };
}, },
@@ -25,7 +24,7 @@ module.exports = {
server: { server: {
entry: () => { entry: () => {
return { return {
server_routes main: entry.server
} }
}, },

3235
yarn.lock

File diff suppressed because it is too large Load Diff