work in progress

This commit is contained in:
Rich Harris
2018-02-16 12:01:55 -05:00
parent 9a760c570f
commit f9828f9fd2
36 changed files with 667 additions and 7791 deletions

6
test/app/app/client.js Normal file
View File

@@ -0,0 +1,6 @@
import { init } from '../../../runtime.js';
import { routes } from './manifest/client.js';
window.init = () => {
return init(document.querySelector('#sapper'), routes);
};

View File

@@ -0,0 +1,12 @@
// This file is generated by Sapper — do not edit it!
export const routes = [
{ pattern: /^\/?$/, params: () => ({}), load: () => import(/* webpackChunkName: "_" */ '../../routes/index.html') },
{ pattern: /^\/4xx\/?$/, params: () => ({}), load: () => import(/* webpackChunkName: "_4xx" */ '../../routes/4xx.html') },
{ pattern: /^\/about\/?$/, params: () => ({}), load: () => import(/* webpackChunkName: "about" */ '../../routes/about.html') },
{ pattern: /^\/show-url\/?$/, params: () => ({}), load: () => import(/* webpackChunkName: "show_url" */ '../../routes/show-url.html') },
{ pattern: /^\/slow-preload\/?$/, params: () => ({}), load: () => import(/* webpackChunkName: "slow_preload" */ '../../routes/slow-preload.html') },
{ pattern: /^\/delete-test\/?$/, params: () => ({}), load: () => import(/* webpackChunkName: "delete_test" */ '../../routes/delete-test.html') },
{ pattern: /^\/5xx\/?$/, params: () => ({}), load: () => import(/* webpackChunkName: "_5xx" */ '../../routes/5xx.html') },
{ pattern: /^\/blog\/?$/, params: () => ({}), load: () => import(/* webpackChunkName: "blog" */ '../../routes/blog/index.html') },
{ pattern: /^\/blog(?:\/([^\/]+))?\/?$/, params: match => ({ slug: match[1] }), load: () => import(/* webpackChunkName: "blog_$slug$" */ '../../routes/blog/[slug].html') }
];

View File

@@ -0,0 +1,28 @@
// This file is generated by Sapper — do not edit it!
import _ from '../../routes/index.html';
import _4xx from '../../routes/4xx.html';
import about from '../../routes/about.html';
import show_url from '../../routes/show-url.html';
import slow_preload from '../../routes/slow-preload.html';
import delete_test from '../../routes/delete-test.html';
import _5xx from '../../routes/5xx.html';
import blog from '../../routes/blog/index.html';
import * as api_blog_contents from '../../routes/api/blog/contents.js';
import * as api_delete_$id$ from '../../routes/api/delete/[id].js';
import * as api_blog_$slug$ from '../../routes/api/blog/[slug].js';
import blog_$slug$ from '../../routes/blog/[slug].html';
export const routes = [
{ id: '_', type: 'page', pattern: /^\/?$/, params: () => ({}), module: _ },
{ id: '_4xx', type: 'page', pattern: /^\/4xx\/?$/, params: () => ({}), module: _4xx },
{ id: 'about', type: 'page', pattern: /^\/about\/?$/, params: () => ({}), module: about },
{ id: 'show_url', type: 'page', pattern: /^\/show-url\/?$/, params: () => ({}), module: show_url },
{ id: 'slow_preload', type: 'page', pattern: /^\/slow-preload\/?$/, params: () => ({}), module: slow_preload },
{ id: 'delete_test', type: 'page', pattern: /^\/delete-test\/?$/, params: () => ({}), module: delete_test },
{ id: '_5xx', type: 'page', pattern: /^\/5xx\/?$/, params: () => ({}), module: _5xx },
{ id: 'blog', type: 'page', pattern: /^\/blog\/?$/, params: () => ({}), module: blog },
{ id: 'api_blog_contents', type: 'route', pattern: /^\/api\/blog\/contents\/?$/, params: () => ({}), module: api_blog_contents },
{ id: 'api_delete_$id$', type: 'route', pattern: /^\/api\/delete(?:\/([^\/]+))?\/?$/, params: match => ({ id: match[1] }), module: api_delete_$id$ },
{ id: 'api_blog_$slug$', type: 'route', pattern: /^\/api\/blog(?:\/([^\/]+))?\/?$/, params: match => ({ slug: match[1] }), module: api_blog_$slug$ },
{ id: 'blog_$slug$', type: 'page', pattern: /^\/blog(?:\/([^\/]+))?\/?$/, params: match => ({ slug: match[1] }), module: blog_$slug$ }
];

View File

@@ -0,0 +1,37 @@
export const timestamp = 1518800295364;
export const assets = [
"favicon.png",
"global.css",
"great-success.png",
"manifest.json",
"svelte-logo-192.png",
"svelte-logo-512.png"
];
export const shell = [
"/client/_.0.3a37f8afa58c59f4bdf9.js",
"/client/blog.1.3a37f8afa58c59f4bdf9.js",
"/client/blog_$slug$.2.3a37f8afa58c59f4bdf9.js",
"/client/about.3.3a37f8afa58c59f4bdf9.js",
"/client/_5xx.4.3a37f8afa58c59f4bdf9.js",
"/client/_4xx.5.3a37f8afa58c59f4bdf9.js",
"/client/slow_preload.6.3a37f8afa58c59f4bdf9.js",
"/client/show_url.7.3a37f8afa58c59f4bdf9.js",
"/client/delete_test.8.3a37f8afa58c59f4bdf9.js",
"/client/main.3a37f8afa58c59f4bdf9.js"
];
export const routes = [
{ pattern: /^\/?$/ },
{ pattern: /^\/4xx\/?$/ },
{ pattern: /^\/about\/?$/ },
{ pattern: /^\/show-url\/?$/ },
{ pattern: /^\/slow-preload\/?$/ },
{ pattern: /^\/delete-test\/?$/ },
{ pattern: /^\/5xx\/?$/ },
{ pattern: /^\/blog\/?$/ },
{ pattern: /^\/blog(?:\/([^\/]+))?\/?$/ }
];

78
test/app/app/server.js Normal file
View File

@@ -0,0 +1,78 @@
import fs from 'fs';
import express from 'express';
import compression from 'compression';
import serve from 'serve-static';
import sapper from '../../../middleware';
import { routes } from './manifest/server.js';
let count;
let ended;
process.on('message', message => {
if (message.action === 'start') {
count = 0;
ended = false;
process.send({ type: 'ready' });
}
if (message.action === 'end') {
ended = true;
if (count === 0) process.send({ type: 'done' });
}
});
const app = express();
app.use((req, res, next) => {
count += 1;
const { write, end } = res;
const chunks = [];
res.write = function(chunk) {
chunks.push(new Buffer(chunk));
write.apply(res, arguments);
};
res.end = function(chunk) {
if (chunk) chunks.push(new Buffer(chunk));
end.apply(res, arguments);
count -= 1;
process.send({
method: req.method,
url: req.url,
status: res.statusCode,
headers: res._headers,
body: Buffer.concat(chunks).toString()
});
if (count === 0 && ended) {
process.send({ type: 'done' });
}
};
next();
});
const { PORT = 3000 } = process.env;
// this allows us to do e.g. `fetch('/api/blog')` on the server
const fetch = require('node-fetch');
global.fetch = (url, opts) => {
if (url[0] === '/') url = `http://localhost:${PORT}${url}`;
return fetch(url, opts);
};
app.use(compression({ threshold: 0 }));
app.use(serve('assets'));
app.use(sapper({
routes
}));
app.listen(PORT, () => {
console.log(`listening on port ${PORT}`);
});

View File

@@ -1,14 +1,12 @@
const ASSETS = `cache__timestamp__`;
import { assets, shell, timestamp, routes } from './manifest/service-worker.js';
const ASSETS = `cachetimestamp`;
// `shell` is an array of all the files generated by webpack,
// `assets` is an array of everything in the `assets` directory
const to_cache = __shell__.concat(__assets__);
const to_cache = shell.concat(assets);
const cached = new Set(to_cache);
// `routes` is an array of `{ pattern: RegExp }` objects that
// match the pages in your app
const routes = __routes__;
self.addEventListener('install', event => {
event.waitUntil(
caches

18
test/app/routes/4xx.html Normal file
View File

@@ -0,0 +1,18 @@
<:Head>
<title>{{status}}</title>
</:Head>
<Layout page='home'>
<h1>{{status}}</h1>
<p>{{message}}</p>
</Layout>
<script>
import Layout from './_components/Layout.html';
export default {
components: {
Layout
}
};
</script>

18
test/app/routes/5xx.html Normal file
View File

@@ -0,0 +1,18 @@
<:Head>
<title>{{status}}</title>
</:Head>
<Layout page='home'>
<h1>{{status}}</h1>
<p>{{error.message}}</p>
</Layout>
<script>
import Layout from './_components/Layout.html';
export default {
components: {
Layout
}
};
</script>

View File

@@ -1,24 +0,0 @@
const fs = require('fs');
const app = require('express')();
const compression = require('compression');
const sapper = require('sapper');
const static = require('serve-static');
const { PORT = 3000 } = process.env;
// this allows us to do e.g. `fetch('/api/blog')` on the server
const fetch = require('node-fetch');
global.fetch = (url, opts) => {
if (url[0] === '/') url = `http://localhost:${PORT}${url}`;
return fetch(url, opts);
};
app.use(compression({ threshold: 0 }));
app.use(static('assets'));
app.use(sapper());
app.listen(PORT, () => {
console.log(`listening on port ${PORT}`);
});

View File

@@ -1,44 +0,0 @@
<!doctype>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width'>
<meta name='theme-color' content='#aa1e1e'>
<link rel='manifest' href='/manifest.json'>
<link rel='icon' type='image/png' href='/favicon.png'>
<!-- %sapper.status% is the HTTP status code, e.g. 404 -->
<title>%sapper.status%</title>
<style>
body {
max-width: 800px;
padding: 1em;
margin: 0 auto;
font-family: Roboto, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
background-color: #f4f4f4;
box-sizing: border-box;
}
h1 {
color: rgb(170,30,30);
border-bottom: 1px solid #aaa;
padding: 0 0 0.5em 0;
margin: 1em 0;
}
pre {
font-family: Menlo, monospace;
font-size: 14px;
line-height: 1.2;
overflow-x: auto;
white-space: pre-wrap;
}
</style>
</head>
<body>
<h1>%sapper.title%</h1>
<p>Could not %sapper.method% %sapper.url%</p>
</body>
</html>

View File

@@ -1,47 +0,0 @@
<!doctype>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width'>
<meta name='theme-color' content='#aa1e1e'>
<link rel='manifest' href='/manifest.json'>
<link rel='icon' type='image/png' href='/favicon.png'>
<title>%sapper.status%</title>
<style>
body {
max-width: 800px;
padding: 1em;
margin: 0 auto;
box-sizing: border-box;
}
h1 {
color: rgb(170,30,30);
border-bottom: 1px solid #aaa;
padding: 0 0 0.5em 0;
margin: 1em 0;
}
pre {
font-family: Menlo, monospace;
font-size: 14px;
line-height: 1.2;
overflow-x: auto;
white-space: pre-wrap;
}
.stack {
font-size: 12px;
color: #999;
}
</style>
</head>
<body>
<h1>%sapper.title%</h1>
<pre>%sapper.error%</pre>
<pre class='stack'>%sapper.stack%</pre>
</body>
</html>

View File

@@ -1,5 +0,0 @@
import { init } from '../../../runtime.js';
window.init = () => {
return init(document.querySelector('#sapper'), __routes__);
};

View File

@@ -1,8 +1,8 @@
const config = require('../../webpack/config.js');
const config = require('../../../webpack/config.js');
const webpack = require('webpack');
module.exports = {
entry: config.client.entry(),
entry: './app/client.js',
output: config.client.output(),
resolve: {
extensions: ['.js', '.html']

View File

@@ -1,12 +1,17 @@
const config = require('../../webpack/config.js');
const config = require('../../../webpack/config.js');
const pkg = require('../package.json');
const sapper_pkg = require('../../../package.json');
module.exports = {
entry: config.server.entry(),
entry: {
'server': './app/server.js'
},
output: config.server.output(),
target: 'node',
resolve: {
extensions: ['.js', '.html']
},
externals: Object.keys(pkg.dependencies).concat(Object.keys(sapper_pkg.dependencies)),
module: {
rules: [
{

View File

@@ -0,0 +1,17 @@
const path = require('path');
const config = require('../../../webpack/config.js');
const webpack = require('webpack');
module.exports = {
entry: {
'service-worker': './app/service-worker.js'
},
output: {
path: path.resolve(`.sapper`),
filename: '[name].js',
chunkFilename: '[name].[id].[hash].js'
},
plugins: [
!config.dev && new webpack.optimize.ModuleConcatenationPlugin()
].filter(Boolean)
};

View File

@@ -15,70 +15,25 @@ Nightmare.action('page', {
}
});
Nightmare.action('init', function(done) {
this.evaluate_now(() => window.init(), done);
});
function run(env) {
describe(`env=${env}`, function () {
this.timeout(20000);
let PORT;
let server;
let proc;
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 = {
setHeader(header, value) {
result.headers[header] = value;
},
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(() => {
process.chdir(path.resolve(__dirname, '../app'));
process.env.NODE_ENV = env;
let exec_promise = Promise.resolve();
let sapper;
if (env === 'production') {
const cli = path.resolve(__dirname, '../../cli.js');
@@ -90,81 +45,85 @@ function run(env) {
delete require.cache[resolved];
delete require.cache[require.resolve('../../core.js')]; // TODO remove this
sapper = require(resolved);
return require('get-port')();
}).then(port => {
PORT = port;
base = `http://localhost:${PORT}`;
base = `http://localhost:${port}`;
Nightmare.action('init', function(done) {
this.evaluate_now(() => window.init(), done);
proc = require('child_process').fork('.sapper/server.js', {
cwd: process.cwd(),
env: {
NODE_ENV: env,
PORT: port
}
});
global.fetch = (url, opts) => {
if (url[0] === '/') url = `${base}${url}`;
return fetch(url, opts);
};
let handler;
proc.on('message', message => {
if (handler) handler(message);
});
let captured;
capture = fn => {
const result = captured = [];
return fn().then(() => {
captured = null;
return result;
return new Promise((fulfil, reject) => {
const captured = [];
let start = Date.now();
handler = message => {
if (message.type === 'ready') {
fn().then(() => {
proc.send({
action: 'end'
});
}, reject);
}
else if (message.type === 'done') {
fulfil(captured);
handler = null;
}
else {
captured.push(message);
}
};
proc.send({
action: 'start'
});
});
};
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();
proc.kill();
// give a chance to clean up
return new Promise(fulfil => setTimeout(fulfil, 500));
});
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(() => {
return nightmare.end();
});
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(() => {
return nightmare.end();
});
it('serves /', () => {
return nightmare.goto(base).page.title().then(title => {
assert.equal(title, 'Great success!');
@@ -190,7 +149,7 @@ function run(env) {
});
it('navigates to a new page without reloading', () => {
return nightmare.goto(base).init().wait(100)
return capture(() => nightmare.goto(base).init().wait(200))
.then(() => {
return capture(() => nightmare.click('a[href="/about"]'));
})
@@ -224,6 +183,7 @@ function run(env) {
return nightmare
.goto(`${base}/about`)
.init()
.wait(100)
.then(() => {
return capture(() => {
return nightmare
@@ -340,15 +300,17 @@ function run(env) {
describe('headers', () => {
it('sets Content-Type and Link...preload headers', () => {
return get('/').then(({ headers }) => {
return capture(() => nightmare.goto(base).end()).then(requests => {
const { headers } = requests[0];
assert.equal(
headers['Content-Type'],
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']
/<\/client\/main.\w+\.js>;rel="preload";as="script", <\/client\/_.\d+.\w+.js>;rel="preload";as="script"/.test(headers['link']),
headers['link']
);
});
});
@@ -402,9 +364,9 @@ function run(env) {
const allPages = walkSync(dest);
expectedPages.forEach((expectedPage) => {
assert.ok(allPages.includes(expectedPage),
`Could not find page matching ${expectedPage}`);
assert.ok(allPages.includes(expectedPage),`Could not find page matching ${expectedPage}`);
});
expectedClientRegexes.forEach((expectedRegex) => {
// Ensure each client page regular expression matches at least one
// generated page.
@@ -415,8 +377,7 @@ function run(env) {
break;
}
}
assert.ok(matched,
`Could not find client page matching ${expectedRegex}`);
assert.ok(matched, `Could not find client page matching ${expectedRegex}`);
});
});
});