mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-13 19:45:26 +00:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75aedf4663 | ||
|
|
c8366dec74 | ||
|
|
9a936669c6 | ||
|
|
0226bd90c6 | ||
|
|
e1926e1bcb | ||
|
|
db1c1f332a | ||
|
|
e8d510b261 | ||
|
|
f8e237b265 | ||
|
|
68c2f2e388 | ||
|
|
0bcb61650b | ||
|
|
43a12a8331 | ||
|
|
f0feab5738 | ||
|
|
e9203b4d71 | ||
|
|
8e79e706e6 | ||
|
|
4b495f44fd | ||
|
|
222a750b7b | ||
|
|
5b214c964c | ||
|
|
95f99fd378 | ||
|
|
1bed4b0670 | ||
|
|
9d4890913a | ||
|
|
f50d3c4262 | ||
|
|
8925e541d5 | ||
|
|
a48afb77d3 | ||
|
|
45e845ee92 | ||
|
|
492f024d2a | ||
|
|
8d40992cf1 | ||
|
|
4232f75b19 | ||
|
|
fefb0d96d7 | ||
|
|
cd91bf2ca4 |
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"root": true,
|
||||
"rules": {
|
||||
"indent": [ 2, "tab", { "SwitchCase": 1 } ],
|
||||
"semi": [ 2, "always" ],
|
||||
"space-before-blocks": [ 2, "always" ],
|
||||
"no-mixed-spaces-and-tabs": [ 2, "smart-tabs" ],
|
||||
@@ -33,7 +32,7 @@
|
||||
"plugin:import/warnings"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"ecmaVersion": 8,
|
||||
"sourceType": "module"
|
||||
}
|
||||
}
|
||||
|
||||
11
.travis.yml
11
.travis.yml
@@ -1,10 +1,21 @@
|
||||
sudo: false
|
||||
|
||||
language: node_js
|
||||
|
||||
node_js:
|
||||
- "stable"
|
||||
|
||||
env:
|
||||
global:
|
||||
- BUILD_TIMEOUT=10000
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- xvfb
|
||||
|
||||
install:
|
||||
- export DISPLAY=':99.0'
|
||||
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||
- npm install
|
||||
- (cd test/app && npm install)
|
||||
|
||||
33
CHANGELOG.md
33
CHANGELOG.md
@@ -1,5 +1,38 @@
|
||||
# sapper changelog
|
||||
|
||||
## 0.2.10
|
||||
|
||||
* Handle deep links correctly ([#44](https://github.com/sveltejs/sapper/issues/44))
|
||||
|
||||
## 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
|
||||
|
||||
* Posixify path to HMR client
|
||||
|
||||
## 0.2.3
|
||||
|
||||
* Posixify import paths, even on Windows ([#31](https://github.com/sveltejs/sapper/pull/31))
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const glob = require('glob');
|
||||
const mkdirp = require('mkdirp');
|
||||
const rimraf = require('rimraf');
|
||||
const { client, server } = require('./utils/compilers.js');
|
||||
const create_app = require('./utils/create_app.js');
|
||||
const generate_asset_cache = require('./utils/generate_asset_cache.js');
|
||||
const { dest } = require('./config.js');
|
||||
|
||||
module.exports = () => {
|
||||
mkdirp(dest);
|
||||
mkdirp.sync(dest);
|
||||
rimraf.sync(path.join(dest, '**/*'));
|
||||
|
||||
// create main.js and server-routes.js
|
||||
create_app();
|
||||
|
||||
@@ -8,14 +8,14 @@ exports.templates = path.resolve(process.env.SAPPER_TEMPLATES || 'templates');
|
||||
|
||||
exports.src = path.resolve(process.env.SAPPER_ROUTES || 'routes');
|
||||
|
||||
exports.dest = path.resolve(
|
||||
process.env.NOW ? '/tmp' :
|
||||
process.env.SAPPER_DEST || '.sapper'
|
||||
);
|
||||
exports.dest = path.resolve(process.env.SAPPER_DEST || '.sapper');
|
||||
|
||||
if (exports.dev) {
|
||||
mkdirp(exports.dest);
|
||||
mkdirp.sync(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')
|
||||
};
|
||||
94
lib/index.js
94
lib/index.js
@@ -1,9 +1,5 @@
|
||||
const fs = require('fs');
|
||||
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 templates = require('./templates.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 generate_asset_cache = require('./utils/generate_asset_cache.js');
|
||||
const escape_html = require('escape-html');
|
||||
const { src, dest, dev } = require('./config.js');
|
||||
const { dest, dev } = require('./config.js');
|
||||
|
||||
function connect_dev() {
|
||||
// create main.js and server-routes.js
|
||||
// TODO update on changes
|
||||
create_app();
|
||||
|
||||
const watcher = create_watcher();
|
||||
|
||||
let asset_cache;
|
||||
|
||||
return compose_handlers([
|
||||
const middleware = compose_handlers([
|
||||
require('webpack-hot-middleware')(compilers.client, {
|
||||
reload: true,
|
||||
path: '/__webpack_hmr',
|
||||
@@ -59,8 +53,15 @@ function connect_dev() {
|
||||
|
||||
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() {
|
||||
@@ -69,7 +70,7 @@ function connect_prod() {
|
||||
read_json(path.join(dest, 'stats.server.json'))
|
||||
);
|
||||
|
||||
return compose_handlers([
|
||||
const middleware = compose_handlers([
|
||||
set_req_pathname,
|
||||
|
||||
get_asset_handler({
|
||||
@@ -95,8 +96,14 @@ function connect_prod() {
|
||||
|
||||
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;
|
||||
@@ -137,20 +144,36 @@ function get_route_handler(fn) {
|
||||
const mod = require(server.entry)[route.id];
|
||||
|
||||
if (route.type === 'page') {
|
||||
let data = { params: req.params, query: req.query };
|
||||
if (mod.preload) data = Object.assign(data, await mod.preload(data));
|
||||
// preload main.js and current route
|
||||
// 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, {
|
||||
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>` : '')
|
||||
});
|
||||
if (mod.preload) {
|
||||
const promise = Promise.resolve(mod.preload(data)).then(preloaded => {
|
||||
Object.assign(data, preloaded);
|
||||
return mod.render(data);
|
||||
});
|
||||
|
||||
res.status(200);
|
||||
res.end(page);
|
||||
await 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>` : ''))
|
||||
});
|
||||
} 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 {
|
||||
@@ -164,23 +187,30 @@ function get_route_handler(fn) {
|
||||
|
||||
next();
|
||||
} catch(err) {
|
||||
res.status(500).end(templates.render(500, {
|
||||
res.status(500);
|
||||
res.end(templates.render(500, {
|
||||
title: (err && err.name) || 'Internal server error',
|
||||
url,
|
||||
error: escape_html(err && (err.details || err.message || err) || 'Unknown error'),
|
||||
stack: err && err.stack.split('\n').slice(1).join('\n')
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function not_found(req, res) {
|
||||
res.status(404).end(templates.render(404, {
|
||||
title: 'Not found',
|
||||
status: 404,
|
||||
method: req.method,
|
||||
url: req.url
|
||||
}));
|
||||
function get_not_found_handler(fn) {
|
||||
return function handle_not_found(req, res) {
|
||||
const asset_cache = fn();
|
||||
|
||||
res.status(404);
|
||||
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) {
|
||||
@@ -200,7 +230,7 @@ function compose_handlers(handlers) {
|
||||
}
|
||||
|
||||
go();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function read_json(file) {
|
||||
|
||||
@@ -1,16 +1,31 @@
|
||||
const glob = require('glob');
|
||||
const chokidar = require('chokidar');
|
||||
const create_routes = require('./utils/create_routes.js');
|
||||
const { src } = require('./config.js');
|
||||
const { src, dev } = require('./config.js');
|
||||
|
||||
const route_manager = {
|
||||
routes: create_routes(
|
||||
glob.sync('**/*.+(html|js|mjs)', { cwd: src })
|
||||
),
|
||||
const callbacks = [];
|
||||
|
||||
onchange(fn) {
|
||||
// TODO in dev mode, keep this updated, and allow
|
||||
// webpack compiler etc to hook into it
|
||||
}
|
||||
exports.onchange = fn => {
|
||||
callbacks.push(fn);
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
103
lib/templates.js
103
lib/templates.js
@@ -1,34 +1,79 @@
|
||||
const fs = require('fs');
|
||||
const glob = require('glob');
|
||||
const chokidar = require('chokidar');
|
||||
const { dev } = require('./config.js');
|
||||
|
||||
const templates = glob.sync('*.html', { cwd: 'templates' })
|
||||
.map(file => {
|
||||
const template = fs.readFileSync(`templates/${file}`, 'utf-8');
|
||||
const status = file.replace('.html', '').toLowerCase();
|
||||
let templates;
|
||||
|
||||
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`);
|
||||
}
|
||||
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();
|
||||
|
||||
const 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] : '';
|
||||
});
|
||||
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`);
|
||||
}
|
||||
}
|
||||
})
|
||||
.sort((a, b) => b.specificity - a.specificity);
|
||||
|
||||
const 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) => {
|
||||
const template = templates.find(template => template.test(status));
|
||||
@@ -37,7 +82,9 @@ exports.render = (status, data) => {
|
||||
return `Missing template for status code ${status}`;
|
||||
};
|
||||
|
||||
exports.onchange = fn => {
|
||||
// TODO in dev mode, keep this updated, and allow
|
||||
// webpack compiler etc to hook into it
|
||||
exports.stream = (res, status, data) => {
|
||||
const template = templates.find(template => template.test(status));
|
||||
if (template) return template.stream(res, data);
|
||||
|
||||
return `Missing template for status code ${status}`;
|
||||
};
|
||||
@@ -1,13 +1,14 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const chokidar = require('chokidar');
|
||||
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) {
|
||||
return file.replace(/[\/\\]/g, '/');
|
||||
return file.replace(/[/\\]/g, '/');
|
||||
}
|
||||
|
||||
module.exports = function create_app() {
|
||||
function create_app() {
|
||||
const { routes } = route_manager;
|
||||
|
||||
function create_client_main() {
|
||||
@@ -33,17 +34,15 @@ module.exports = function create_app() {
|
||||
.replace(/__dev__/g, String(dev));
|
||||
|
||||
if (dev) {
|
||||
const hmr_client = require.resolve(`webpack-hot-middleware/client`);
|
||||
const hmr_client = posixify(require.resolve(`webpack-hot-middleware/client`));
|
||||
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(file, main);
|
||||
fs.writeFileSync(entry.client, main);
|
||||
|
||||
// need to fudge the mtime, because webpack is soft in the head
|
||||
const { atime, mtime } = fs.statSync(file);
|
||||
fs.utimesSync(file, new Date(atime.getTime() - 999999), new Date(mtime.getTime() - 999999));
|
||||
const { atime, mtime } = fs.statSync(entry.client);
|
||||
fs.utimesSync(entry.client, new Date(atime.getTime() - 999999), new Date(mtime.getTime() - 999999));
|
||||
}
|
||||
|
||||
function create_server_routes() {
|
||||
@@ -58,13 +57,27 @@ module.exports = function create_app() {
|
||||
|
||||
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);
|
||||
fs.utimesSync(server_routes, new Date(atime.getTime() - 999999), new Date(mtime.getTime() - 999999));
|
||||
const { atime, mtime } = fs.statSync(entry.server);
|
||||
fs.utimesSync(entry.server, new Date(atime.getTime() - 999999), new Date(mtime.getTime() - 999999));
|
||||
}
|
||||
|
||||
// TODO in dev mode, watch files
|
||||
create_client_main();
|
||||
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;
|
||||
@@ -1,11 +1,11 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = function create_matchers(files) {
|
||||
return files
|
||||
const routes = files
|
||||
.map(file => {
|
||||
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();
|
||||
|
||||
const id = (
|
||||
@@ -47,9 +47,31 @@ module.exports = function create_matchers(files) {
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => {
|
||||
return (
|
||||
(a.dynamic.length - b.dynamic.length) || // match static paths first
|
||||
(b.parts.length - a.parts.length) // match longer paths first
|
||||
);
|
||||
let same = true;
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const chalk = require('chalk');
|
||||
const compilers = require('./compilers.js');
|
||||
const generate_asset_cache = require('./generate_asset_cache.js');
|
||||
const { dest } = require('../config.js');
|
||||
|
||||
function deferred() {
|
||||
const d = {};
|
||||
@@ -24,21 +26,23 @@ module.exports = function create_watcher() {
|
||||
deferreds.client.promise,
|
||||
deferreds.server.promise
|
||||
]).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(
|
||||
client_stats.toJson(),
|
||||
server_stats.toJson()
|
||||
);
|
||||
});
|
||||
|
||||
watcher = {
|
||||
ready: invalidate()
|
||||
};
|
||||
|
||||
function watch_compiler(type) {
|
||||
const compiler = compilers[type];
|
||||
|
||||
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();
|
||||
watcher.ready = invalidate();
|
||||
});
|
||||
@@ -47,7 +51,7 @@ module.exports = function create_watcher() {
|
||||
deferreds[type].reject(err);
|
||||
});
|
||||
|
||||
compiler.watch({}, (err, stats) => {
|
||||
return compiler.watch({}, (err, stats) => {
|
||||
if (stats.hasErrors()) {
|
||||
deferreds[type].reject(stats.toJson().errors[0]);
|
||||
} else {
|
||||
@@ -56,8 +60,16 @@ module.exports = function create_watcher() {
|
||||
});
|
||||
}
|
||||
|
||||
watch_compiler('client');
|
||||
watch_compiler('server');
|
||||
const watcher = {
|
||||
ready: invalidate(),
|
||||
client: watch_compiler('client'),
|
||||
server: watch_compiler('server'),
|
||||
|
||||
close: () => {
|
||||
watcher.client.close();
|
||||
watcher.server.close();
|
||||
}
|
||||
};
|
||||
|
||||
return watcher;
|
||||
};
|
||||
@@ -3,17 +3,24 @@ const path = require('path');
|
||||
const glob = require('glob');
|
||||
const templates = require('../templates.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) {
|
||||
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 service_worker = generate_service_worker(chunk_files);
|
||||
const index = generate_index(main_file);
|
||||
|
||||
fs.writeFileSync(path.join(dest, 'service-worker.js'), service_worker);
|
||||
fs.writeFileSync(path.join(dest, 'index.html'), index);
|
||||
if (dev) {
|
||||
fs.writeFileSync(path.join(dest, 'service-worker.js'), service_worker);
|
||||
fs.writeFileSync(path.join(dest, 'index.html'), index);
|
||||
}
|
||||
|
||||
return {
|
||||
client: {
|
||||
@@ -26,12 +33,17 @@ module.exports = function generate_asset_cache(clientInfo, serverInfo) {
|
||||
return lookup;
|
||||
}, {}),
|
||||
|
||||
routes: route_manager.routes.reduce((lookup, route) => {
|
||||
lookup[route.id] = `/client/${ensure_array(clientInfo.assetsByChunkName[route.id])[0]}`;
|
||||
return lookup;
|
||||
}, {}),
|
||||
|
||||
index,
|
||||
service_worker
|
||||
},
|
||||
|
||||
server: {
|
||||
entry: path.resolve(dest, 'server', serverInfo.assetsByChunkName.server_routes)
|
||||
entry: path.resolve(dest, 'server', serverInfo.assetsByChunkName.main)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
--recursive
|
||||
test/unit/**/*.js
|
||||
test/unit/**/*.js
|
||||
test/common/test.js
|
||||
3239
package-lock.json
generated
3239
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sapper",
|
||||
"version": "0.2.3",
|
||||
"version": "0.2.10",
|
||||
"description": "Military-grade apps, engineered by Svelte",
|
||||
"main": "lib/index.js",
|
||||
"bin": {
|
||||
@@ -11,6 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^2.3.0",
|
||||
"chokidar": "^1.7.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"mkdirp": "^0.5.1",
|
||||
"relative": "^3.0.2",
|
||||
@@ -20,22 +21,23 @@
|
||||
"webpack-hot-middleware": "^2.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cypress": "^1.3.0",
|
||||
"css-loader": "^0.28.7",
|
||||
"eslint": "^4.13.1",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"express": "^4.16.2",
|
||||
"get-port": "^3.2.0",
|
||||
"mocha": "^4.0.1",
|
||||
"nightmare": "^2.10.0",
|
||||
"node-fetch": "^1.7.3",
|
||||
"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"
|
||||
},
|
||||
"scripts": {
|
||||
"cy:open": "cypress open",
|
||||
"test": "run-s test:unit test:dev test:prod",
|
||||
"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"
|
||||
"test": "mocha --opts mocha.opts"
|
||||
},
|
||||
"repository": "https://github.com/sveltejs/sapper",
|
||||
"keywords": [
|
||||
|
||||
@@ -6,12 +6,17 @@ export let component;
|
||||
let target;
|
||||
let routes;
|
||||
|
||||
const history = typeof window !== 'undefined' ? window.history : {
|
||||
pushState: () => {},
|
||||
replaceState: () => {},
|
||||
};
|
||||
|
||||
const scroll_history = {};
|
||||
let uid = 1;
|
||||
let cid;
|
||||
|
||||
if ('scrollRestoration' in history) {
|
||||
history.scrollRestoration = 'manual'
|
||||
history.scrollRestoration = 'manual';
|
||||
}
|
||||
|
||||
function select_route(url) {
|
||||
@@ -39,7 +44,7 @@ function render(Component, data, scroll) {
|
||||
} else {
|
||||
// first load — remove SSR'd <head> contents
|
||||
const start = document.querySelector('#sapper-head-start');
|
||||
let end = document.querySelector('#sapper-head-end');
|
||||
const end = document.querySelector('#sapper-head-end');
|
||||
|
||||
if (start && end) {
|
||||
while (start.nextSibling !== end) detach(start.nextSibling);
|
||||
@@ -75,8 +80,6 @@ function navigate(url, id) {
|
||||
|
||||
id = cid = ++uid;
|
||||
scroll_history[cid] = { x: 0, y: 0 };
|
||||
|
||||
history.pushState({ id }, '', url.href);
|
||||
}
|
||||
|
||||
selected.route.load().then(mod => {
|
||||
@@ -123,6 +126,7 @@ function handle_click(event) {
|
||||
|
||||
if (navigate(url, null)) {
|
||||
event.preventDefault();
|
||||
history.pushState({ id: cid }, '', url.href);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,10 +177,17 @@ export function init(_target, _routes) {
|
||||
inited = true;
|
||||
}
|
||||
|
||||
const scroll = scroll_history[uid] = scroll_state();
|
||||
setTimeout(() => {
|
||||
const { hash, href } = window.location;
|
||||
|
||||
history.replaceState({ id: uid }, '', window.location.href);
|
||||
navigate(new URL(window.location), uid);
|
||||
const deep_linked = hash && document.querySelector(hash);
|
||||
scroll_history[uid] = deep_linked ?
|
||||
{ x: 0, y: deep_linked.getBoundingClientRect().top } :
|
||||
scroll_state();
|
||||
|
||||
history.replaceState({ id: uid }, '', href);
|
||||
navigate(new URL(window.location), uid);
|
||||
});
|
||||
}
|
||||
|
||||
function which(event) {
|
||||
@@ -189,4 +200,12 @@ function scroll_state() {
|
||||
x: window.scrollX,
|
||||
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
1
test/app/.gitignore
vendored
@@ -3,3 +3,4 @@ node_modules
|
||||
.sapper
|
||||
yarn.lock
|
||||
cypress/screenshots
|
||||
templates/.*
|
||||
@@ -6,14 +6,21 @@
|
||||
<h1>About this site</h1>
|
||||
|
||||
<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>
|
||||
|
||||
<script>
|
||||
import Layout from './_components/Layout.html';
|
||||
import { goto } from '../../../runtime/app.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Layout
|
||||
},
|
||||
|
||||
methods: {
|
||||
goto
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -82,6 +82,30 @@ const posts = [
|
||||
html: `
|
||||
<p>We're so glad you asked! Come on over to the <a href='https://github.com/sveltejs/svelte'>Svelte</a> and <a href='https://github.com/sveltejs/sapper'>Sapper</a> repos, and join us in the <a href='https://gitter.im/sveltejs/svelte'>Gitter chatroom</a>. Everyone is welcome, especially you!</p>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
title: 'A very long post with deep links',
|
||||
slug: 'a-very-long-post',
|
||||
html: `
|
||||
<h2 id='one'>One</h2>
|
||||
<p>I'll have a vodka rocks. (Mom, it's breakfast time.) And a piece of toast. Let me out that Queen. Fried cheese… with club sauce.</p>
|
||||
<p>Her lawyers are claiming the seal is worth $250,000. And that's not even including Buster's Swatch. This was a big get for God. What, so the guy we are meeting with can't even grow his own hair? COME ON! She's always got to wedge herself in the middle of us so that she can control everything. Yeah. Mom's awesome. It's, like, Hey, you want to go down to the whirlpool? Yeah, I don't have a husband. I call it Swing City. The CIA should've just Googled for his hideout, evidently. There are dozens of us! DOZENS! Yeah, like I'm going to take a whiz through this $5,000 suit. COME ON.</p>
|
||||
|
||||
<h2 id='two'>Two</h2>
|
||||
<p>Tobias Fünke costume. Heart attack never stopped old big bear.</p>
|
||||
<p>Nellie is blowing them all AWAY. I will be a bigger and hairier mole than the one on your inner left thigh! I'll sacrifice anything for my children.</p>
|
||||
<p>Up yours, granny! You couldn't handle it! Hey, Dad. Look at you. You're a year older…and a year closer to death. Buster: Oh yeah, I guess that's kind of funny. Bob Loblaw Law Blog. The guy runs a prison, he can have any piece of ass he wants.</p>
|
||||
|
||||
<h2 id='three'>Three</h2>
|
||||
<p>I prematurely shot my wad on what was supposed to be a dry run, so now I'm afraid I have something of a mess on my hands. Dead Dove DO NOT EAT. Never once touched my per diem. I'd go to Craft Service, get some raw veggies, bacon, Cup-A-Soup…baby, I got a stew goin'. You're losing blood, aren't you? Gob: Probably, my socks are wet. Sure, let the little fruit do it. HUZZAH! Although George Michael had only got to second base, he'd gone in head first, like Pete Rose. I will pack your sweet pink mouth with so much ice cream you'll be the envy of every Jerry and Jane on the block!</p>
|
||||
<p>Gosh Mom… after all these years, God's not going to take a call from you. Come on, this is a Bluth family celebration. It's no place for children.</p>
|
||||
<p>And I wouldn't just lie there, if that's what you're thinking. That's not what I WAS thinking. Who? i just dont want him to point out my cracker ass in front of ann. When a man needs to prove to a woman that he's actually… When a man loves a woman… Heyyyyyy Uncle Father Oscar. [Stabbing Gob] White power! Gob: I'm white! Let me take off my assistant's skirt and put on my Barbra-Streisand-in-The-Prince-of-Tides ass-masking therapist pantsuit. In the mid '90s, Tobias formed a folk music band with Lindsay and Maebe which he called Dr. Funke's 100 Percent Natural Good Time Family Band Solution. The group was underwritten by the Natural Food Life Company, a division of Chem-Grow, an Allen Crayne acqusition, which was part of the Squimm Group. Their motto was simple: We keep you alive.</p>
|
||||
|
||||
<h2 id='four'>Four</h2>
|
||||
<p>If you didn't have adult onset diabetes, I wouldn't mind giving you a little sugar. Everybody dance NOW. And the soup of the day is bread. Great, now I'm gonna smell to high heaven like a tuna melt!</p>
|
||||
<p>That's how Tony Wonder lost a nut. She calls it a Mayonegg. Go ahead, touch the Cornballer. There's a new daddy in town. A discipline daddy.</p>
|
||||
`
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
<link rel='icon' type='image/png' href='/favicon.png'>
|
||||
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/service-worker.js');
|
||||
}
|
||||
// if ('serviceWorker' in navigator) {
|
||||
// navigator.serviceWorker.register('/service-worker.js');
|
||||
// }
|
||||
</script>
|
||||
|
||||
<!-- Sapper generates a <style> tag containing critical CSS
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { init } from '__app__';
|
||||
import { init } from '../../../runtime/app.js';
|
||||
|
||||
// `routes` is an array of route objects injected by Sapper
|
||||
init(document.querySelector('#sapper'), __routes__);
|
||||
|
||||
// if (__dev__) {
|
||||
// // Enable hot-module reloading
|
||||
// import('sapper/webpack/hmr');
|
||||
// if (module.hot) module.hot.accept();
|
||||
// }
|
||||
window.READY = true;
|
||||
@@ -1,7 +1,5 @@
|
||||
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 = {
|
||||
entry: config.client.entry(),
|
||||
@@ -24,27 +22,18 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
},
|
||||
config.dev && {
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
{ loader: "style-loader" },
|
||||
{ loader: "css-loader" }
|
||||
]
|
||||
},
|
||||
!config.dev && {
|
||||
test: /\.css$/,
|
||||
use: ExtractTextPlugin.extract({
|
||||
fallback: 'style-loader',
|
||||
use: [{ loader: 'css-loader', options: { sourceMap: config.dev } }]
|
||||
})
|
||||
}
|
||||
].filter(Boolean)
|
||||
},
|
||||
plugins: [
|
||||
config.dev && new webpack.HotModuleReplacementPlugin(),
|
||||
!config.dev && new ExtractTextPlugin('main.css'),
|
||||
!config.dev && new webpack.optimize.ModuleConcatenationPlugin(),
|
||||
!config.dev && new UglifyJSPlugin()
|
||||
!config.dev && new webpack.optimize.ModuleConcatenationPlugin()
|
||||
].filter(Boolean),
|
||||
devtool: config.dev ? 'inline-source-map' : false
|
||||
};
|
||||
@@ -1,7 +1,4 @@
|
||||
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 = {
|
||||
entry: config.server.entry(),
|
||||
|
||||
244
test/common/test.js
Normal file
244
test/common/test.js
Normal file
@@ -0,0 +1,244 @@
|
||||
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).wait(() => window.READY).wait(100);
|
||||
|
||||
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?'
|
||||
);
|
||||
});
|
||||
|
||||
it('scrolls to active deeplink', async () => {
|
||||
const scrollY = await nightmare
|
||||
.goto(`${base}/blog/a-very-long-post#four`)
|
||||
.wait(() => window.READY)
|
||||
.wait(100)
|
||||
.evaluate(() => window.scrollY);
|
||||
|
||||
assert.ok(scrollY > 0, scrollY);
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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 |
@@ -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) => { ... })
|
||||
@@ -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')
|
||||
@@ -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();
|
||||
}
|
||||
@@ -5,15 +5,17 @@ const create_routes = require('../../lib/utils/create_routes.js');
|
||||
|
||||
describe('create_routes', () => {
|
||||
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(
|
||||
routes.map(r => r.file),
|
||||
[
|
||||
'about.html',
|
||||
'index.html',
|
||||
'post/foo.html',
|
||||
'post/bar.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/);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
const path = require('path');
|
||||
const route_manager = require('../lib/route_manager.js');
|
||||
const { src, dest, dev, server_routes } = require('../lib/config.js');
|
||||
const { src, dest, dev, entry } = require('../lib/config.js');
|
||||
|
||||
module.exports = {
|
||||
dev,
|
||||
@@ -8,7 +7,7 @@ module.exports = {
|
||||
client: {
|
||||
entry: () => {
|
||||
return {
|
||||
main: `${dest}/main.js`
|
||||
main: entry.client
|
||||
};
|
||||
},
|
||||
|
||||
@@ -25,7 +24,7 @@ module.exports = {
|
||||
server: {
|
||||
entry: () => {
|
||||
return {
|
||||
server_routes
|
||||
main: entry.server
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user