mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-13 19:45:26 +00:00
Compare commits
166 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
631afbbfe4 | ||
|
|
1cc9acb4f1 | ||
|
|
19005110f1 | ||
|
|
21ee8ad39d | ||
|
|
906b0c7ad5 | ||
|
|
896fd410d1 | ||
|
|
c0cc877456 | ||
|
|
3ed9ce27a1 | ||
|
|
edba45b809 | ||
|
|
43c1890235 | ||
|
|
605929053c | ||
|
|
2752c73ebb | ||
|
|
2547db39ac | ||
|
|
1285739cc5 | ||
|
|
14d64e854a | ||
|
|
c419c73550 | ||
|
|
835b94175d | ||
|
|
25bdcf9957 | ||
|
|
792ccf5c6a | ||
|
|
4ca8195037 | ||
|
|
cb12231053 | ||
|
|
d55401d45b | ||
|
|
99d4eafb0b | ||
|
|
bff6f550be | ||
|
|
f8ea9ebda1 | ||
|
|
181d7b4a61 | ||
|
|
beb415c65d | ||
|
|
5bbd7ead17 | ||
|
|
e11405d555 | ||
|
|
9fe0ca2c22 | ||
|
|
f2eb95d546 | ||
|
|
ab1ca60363 | ||
|
|
d95f52f8e9 | ||
|
|
b02183af53 | ||
|
|
f9828f9fd2 | ||
|
|
9a760c570f | ||
|
|
0f390920a8 | ||
|
|
9adb6ca7e6 | ||
|
|
24980651c0 | ||
|
|
7c6436a99c | ||
|
|
f6b26f1b07 | ||
|
|
55b60369f9 | ||
|
|
2be9dd1883 | ||
|
|
b29700f725 | ||
|
|
7188ce0d0d | ||
|
|
4f8ce19fe1 | ||
|
|
a85f2921e8 | ||
|
|
7a2ed16884 | ||
|
|
08e575fee0 | ||
|
|
7dbcab74d3 | ||
|
|
9b1b545194 | ||
|
|
7b01242f3e | ||
|
|
15b1fbf8a6 | ||
|
|
8f1d2e0a04 | ||
|
|
dfb8692d78 | ||
|
|
09d3c4d85e | ||
|
|
1e623dde29 | ||
|
|
5104abf329 | ||
|
|
6554fc8616 | ||
|
|
cd01b7e6db | ||
|
|
bfa3da6d3d | ||
|
|
6ee092f8d4 | ||
|
|
ac70004f77 | ||
|
|
3449f1eb37 | ||
|
|
16cb1fccc6 | ||
|
|
b20c1c029f | ||
|
|
7abfb1aab1 | ||
|
|
205c2defe4 | ||
|
|
09a6eec83e | ||
|
|
2cabf61ea7 | ||
|
|
71cfdd2907 | ||
|
|
297f4276de | ||
|
|
422e31e183 | ||
|
|
b53ee061c0 | ||
|
|
8bad37205d | ||
|
|
fd0dd4fe58 | ||
|
|
4940644ae3 | ||
|
|
fb8d952eeb | ||
|
|
fc631c4866 | ||
|
|
03ce2ea998 | ||
|
|
dd8deb2d8a | ||
|
|
7d721abb2a | ||
|
|
39b1fa89ce | ||
|
|
7a3506420f | ||
|
|
72ae4a1c64 | ||
|
|
a09c33d6a5 | ||
|
|
4590aa313c | ||
|
|
d11bd954e0 | ||
|
|
c15959710b | ||
|
|
bb8ff74f68 | ||
|
|
2cbbe91490 | ||
|
|
faeddd8add | ||
|
|
d77722c042 | ||
|
|
61daba7a64 | ||
|
|
54ff8cc2e6 | ||
|
|
e6fcafe09b | ||
|
|
a305d3cea1 | ||
|
|
75e70207b8 | ||
|
|
8a8526d9ed | ||
|
|
9a76229bb6 | ||
|
|
f4e46e6e6c | ||
|
|
90cd347112 | ||
|
|
5adfdd6fe0 | ||
|
|
a6dc61a182 | ||
|
|
96666d05ec | ||
|
|
6390ba692b | ||
|
|
0e131cc81e | ||
|
|
bd3d5713cb | ||
|
|
9ec23c47ad | ||
|
|
b7bb69925e | ||
|
|
25124f6ee7 | ||
|
|
73d491cd19 | ||
|
|
e25fceb4b8 | ||
|
|
3807147c57 | ||
|
|
a523ba58ff | ||
|
|
fe03fd3a52 | ||
|
|
89c430a0cb | ||
|
|
8ef312849c | ||
|
|
4200446684 | ||
|
|
681ed005b8 | ||
|
|
d457af8d51 | ||
|
|
0c158b9e1f | ||
|
|
50011e2077 | ||
|
|
f27b7973e3 | ||
|
|
2af2ab3cb9 | ||
|
|
6a4dc1901c | ||
|
|
fbbc0e9e19 | ||
|
|
1213c3da46 | ||
|
|
4cc2104088 | ||
|
|
d6dda371ca | ||
|
|
304c06085e | ||
|
|
33b6450e34 | ||
|
|
8faa98af6a | ||
|
|
14df138528 | ||
|
|
44285cdb2f | ||
|
|
bd656cfd5b | ||
|
|
c4b4bd587d | ||
|
|
2abfdb03d5 | ||
|
|
a80ac3a8b8 | ||
|
|
887cb09386 | ||
|
|
cfeeafded4 | ||
|
|
2cae674033 | ||
|
|
7c0f32662d | ||
|
|
b4fb1c3268 | ||
|
|
ecd0f673a9 | ||
|
|
40d16852f7 | ||
|
|
133be03791 | ||
|
|
727a76ebb5 | ||
|
|
e3c047831a | ||
|
|
81b5e0d764 | ||
|
|
98e904dcfc | ||
|
|
ca51372150 | ||
|
|
7cef1f1120 | ||
|
|
1b73baabce | ||
|
|
5aa01b922b | ||
|
|
f0bc68be88 | ||
|
|
be7c53becc | ||
|
|
9ea4137b87 | ||
|
|
7588911108 | ||
|
|
fc8280adea | ||
|
|
d08f9eb5a4 | ||
|
|
2b3472b1b1 | ||
|
|
30ddb3dd7e | ||
|
|
0c891ba79e | ||
|
|
ee94f355d5 | ||
|
|
bea9b7965a |
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"root": true,
|
|
||||||
"rules": {
|
|
||||||
"semi": [ 2, "always" ],
|
|
||||||
"space-before-blocks": [ 2, "always" ],
|
|
||||||
"no-mixed-spaces-and-tabs": [ 2, "smart-tabs" ],
|
|
||||||
"no-cond-assign": 0,
|
|
||||||
"no-unused-vars": 2,
|
|
||||||
"object-shorthand": [ 2, "always" ],
|
|
||||||
"no-const-assign": 2,
|
|
||||||
"no-class-assign": 2,
|
|
||||||
"no-this-before-super": 2,
|
|
||||||
"no-var": 2,
|
|
||||||
"no-unreachable": 2,
|
|
||||||
"valid-typeof": 2,
|
|
||||||
"quote-props": [ 2, "as-needed" ],
|
|
||||||
"one-var": [ 2, "never" ],
|
|
||||||
"prefer-arrow-callback": 2,
|
|
||||||
"prefer-const": [ 2, { "destructuring": "all" } ],
|
|
||||||
"arrow-spacing": 2,
|
|
||||||
"no-inner-declarations": 0
|
|
||||||
},
|
|
||||||
"env": {
|
|
||||||
"es6": true,
|
|
||||||
"browser": true,
|
|
||||||
"node": true,
|
|
||||||
"mocha": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:import/errors",
|
|
||||||
"plugin:import/warnings"
|
|
||||||
],
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 8,
|
|
||||||
"sourceType": "module"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -1,5 +1,16 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
yarn.lock
|
||||||
node_modules
|
node_modules
|
||||||
cypress/screenshots
|
cypress/screenshots
|
||||||
test/app/.sapper
|
test/app/.sapper
|
||||||
runtime.js
|
test/app/app/manifest
|
||||||
|
runtime.js
|
||||||
|
runtime.js.map
|
||||||
|
cli.js
|
||||||
|
cli.js.map
|
||||||
|
middleware.js
|
||||||
|
middleware.js.map
|
||||||
|
core.js
|
||||||
|
core.js.map
|
||||||
|
webpack/config.js
|
||||||
|
webpack/config.js.map
|
||||||
@@ -3,6 +3,7 @@ sudo: false
|
|||||||
language: node_js
|
language: node_js
|
||||||
|
|
||||||
node_js:
|
node_js:
|
||||||
|
- "6"
|
||||||
- "stable"
|
- "stable"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
|||||||
71
CHANGELOG.md
71
CHANGELOG.md
@@ -1,5 +1,76 @@
|
|||||||
# sapper changelog
|
# sapper changelog
|
||||||
|
|
||||||
|
## 0.7.4
|
||||||
|
|
||||||
|
* Force `NODE_ENV='production'` when running `build` or `export` ([#141](https://github.com/sveltejs/sapper/issues/141))
|
||||||
|
* Use source-map-support ([#134](https://github.com/sveltejs/sapper/pull/134))
|
||||||
|
|
||||||
|
## 0.7.3
|
||||||
|
|
||||||
|
* Handle webpack assets that are arrays instead of strings ([#131](https://github.com/sveltejs/sapper/pull/131))
|
||||||
|
* Wait for new server to start before broadcasting HMR update ([#129](https://github.com/sveltejs/sapper/pull/129))
|
||||||
|
|
||||||
|
## 0.7.2
|
||||||
|
|
||||||
|
* Add `hmr-client.js` to package
|
||||||
|
* Wait until first successful client build before creating service-worker.js
|
||||||
|
|
||||||
|
## 0.7.1
|
||||||
|
|
||||||
|
* Add missing `tslib` dependency
|
||||||
|
|
||||||
|
## 0.7.0
|
||||||
|
|
||||||
|
* Restructure app layout (see [migration guide](https://sapper.svelte.technology/guide#0-6-to-0-7)) ([#126](https://github.com/sveltejs/sapper/pull/126))
|
||||||
|
* Support `this.redirect(status, location)` and `this.error(status, error)` in `preload` functions ([#127](https://github.com/sveltejs/sapper/pull/127))
|
||||||
|
* Add `sapper dev` command
|
||||||
|
* Add `sapper --help` command
|
||||||
|
|
||||||
|
## 0.6.4
|
||||||
|
|
||||||
|
* Prevent phantom HMR requests in production mode ([#114](https://github.com/sveltejs/sapper/pull/114))
|
||||||
|
|
||||||
|
## 0.6.3
|
||||||
|
|
||||||
|
* Ignore non-HTML responses when crawling during `export`
|
||||||
|
* Build in prod mode for `export`
|
||||||
|
|
||||||
|
## 0.6.2
|
||||||
|
|
||||||
|
* Handle unspecified type in `sapper export`
|
||||||
|
|
||||||
|
## 0.6.1
|
||||||
|
|
||||||
|
* Fix `pkg.files` and `pkg.bin`
|
||||||
|
|
||||||
|
## 0.6.0
|
||||||
|
|
||||||
|
* Hydrate on first load, and only on first load ([#93](https://github.com/sveltejs/sapper/pull/93))
|
||||||
|
* Identify clashes between page and server routes ([#96](https://github.com/sveltejs/sapper/pull/96))
|
||||||
|
* Remove Express-specific utilities, for compatbility with Polka et al ([#94](https://github.com/sveltejs/sapper/issues/94))
|
||||||
|
* Return a promise from `init` when first page has rendered ([#99](https://github.com/sveltejs/sapper/issues/99))
|
||||||
|
* Handle invalid hash links ([#104](https://github.com/sveltejs/sapper/pull/104))
|
||||||
|
* Avoid `URLSearchParams` ([#107](https://github.com/sveltejs/sapper/pull/107))
|
||||||
|
* Don't automatically set `Content-Type` for server routes ([#111](https://github.com/sveltejs/sapper/pull/111))
|
||||||
|
* Handle empty query string routes, e.g. `/?` ([#105](https://github.com/sveltejs/sapper/pull/105))
|
||||||
|
|
||||||
|
## 0.5.1
|
||||||
|
|
||||||
|
* Only write service-worker.js to filesystem in dev mode ([#90](https://github.com/sveltejs/sapper/issues/90))
|
||||||
|
|
||||||
|
## 0.5.0
|
||||||
|
|
||||||
|
* Experimental support for `sapper export` ([#9](https://github.com/sveltejs/sapper/issues/9))
|
||||||
|
* Lazily load chokidar, for faster startup ([#64](https://github.com/sveltejs/sapper/pull/64))
|
||||||
|
|
||||||
|
## 0.4.0
|
||||||
|
|
||||||
|
* `%sapper.main%` has been replaced with `%sapper.scripts%` ([#86](https://github.com/sveltejs/sapper/issues/86))
|
||||||
|
* Node 6 support ([#67](https://github.com/sveltejs/sapper/pull/67))
|
||||||
|
* Explicitly load css-loader and style-loader ([#72](https://github.com/sveltejs/sapper/pull/72))
|
||||||
|
* DELETE requests are handled with `del` exports ([#77](https://github.com/sveltejs/sapper/issues/77))
|
||||||
|
* Send preloaded data for first route to client, where possible ([#3](https://github.com/sveltejs/sapper/issues/3))
|
||||||
|
|
||||||
## 0.3.2
|
## 0.3.2
|
||||||
|
|
||||||
* Expose `prefetch` function ([#61](https://github.com/sveltejs/sapper/pull/61))
|
* Expose `prefetch` function ([#61](https://github.com/sveltejs/sapper/pull/61))
|
||||||
|
|||||||
@@ -15,3 +15,7 @@ environment:
|
|||||||
install:
|
install:
|
||||||
- ps: Install-Product node $env:nodejs_version
|
- ps: Install-Product node $env:nodejs_version
|
||||||
- npm install
|
- npm install
|
||||||
|
|
||||||
|
test_script:
|
||||||
|
- node --version && npm --version
|
||||||
|
- npm test
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const cmd = process.argv[2];
|
|
||||||
|
|
||||||
if (cmd === 'build') {
|
|
||||||
process.env.NODE_ENV = 'production';
|
|
||||||
require('../lib/build.js')();
|
|
||||||
}
|
|
||||||
28
hmr-client.js
Normal file
28
hmr-client.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
let source;
|
||||||
|
|
||||||
|
function check() {
|
||||||
|
if (module.hot.status() === 'idle') {
|
||||||
|
module.hot.check(true).then(modules => {
|
||||||
|
console.log(`HMR updated`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function connect(port) {
|
||||||
|
if (source || !window.EventSource) return;
|
||||||
|
|
||||||
|
source = new EventSource(`http://localhost:${port}/hmr`);
|
||||||
|
|
||||||
|
source.onopen = function(event) {
|
||||||
|
console.log(`HMR connected`);
|
||||||
|
};
|
||||||
|
|
||||||
|
source.onmessage = function(event) {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (!data) return; // just a heartbeat
|
||||||
|
|
||||||
|
if (data.status === 'completed') {
|
||||||
|
check();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
42
lib/build.js
42
lib/build.js
@@ -1,42 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
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.sync(dest);
|
|
||||||
rimraf.sync(path.join(dest, '**/*'));
|
|
||||||
|
|
||||||
// create main.js and server-routes.js
|
|
||||||
create_app();
|
|
||||||
|
|
||||||
function handleErrors(err, stats) {
|
|
||||||
if (err) {
|
|
||||||
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stats.hasErrors()) {
|
|
||||||
console.log(stats.toString({ colors: true }));
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
client.run((err, clientStats) => {
|
|
||||||
handleErrors(err, clientStats);
|
|
||||||
const clientInfo = clientStats.toJson();
|
|
||||||
fs.writeFileSync(path.join(dest, 'stats.client.json'), JSON.stringify(clientInfo, null, ' '));
|
|
||||||
|
|
||||||
server.run((err, serverStats) => {
|
|
||||||
handleErrors(err, serverStats);
|
|
||||||
const serverInfo = serverStats.toJson();
|
|
||||||
fs.writeFileSync(path.join(dest, 'stats.server.json'), JSON.stringify(serverInfo, null, ' '));
|
|
||||||
|
|
||||||
generate_asset_cache(clientInfo, serverInfo);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const mkdirp = require('mkdirp');
|
|
||||||
const rimraf = require('rimraf');
|
|
||||||
|
|
||||||
exports.dev = process.env.NODE_ENV !== 'production';
|
|
||||||
|
|
||||||
exports.templates = path.resolve(process.env.SAPPER_TEMPLATES || 'templates');
|
|
||||||
|
|
||||||
exports.src = path.resolve(process.env.SAPPER_ROUTES || 'routes');
|
|
||||||
|
|
||||||
exports.dest = path.resolve(process.env.SAPPER_DEST || '.sapper');
|
|
||||||
|
|
||||||
if (exports.dev) {
|
|
||||||
mkdirp.sync(exports.dest);
|
|
||||||
rimraf.sync(path.join(exports.dest, '**/*'));
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.entry = {
|
|
||||||
client: path.resolve(exports.templates, '.main.rendered.js'),
|
|
||||||
server: path.resolve(exports.dest, 'server-entry.js')
|
|
||||||
};
|
|
||||||
238
lib/index.js
238
lib/index.js
@@ -1,238 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const route_manager = require('./route_manager.js');
|
|
||||||
const templates = require('./templates.js');
|
|
||||||
const create_app = require('./utils/create_app.js');
|
|
||||||
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 { dest, dev } = require('./config.js');
|
|
||||||
|
|
||||||
function connect_dev() {
|
|
||||||
create_app();
|
|
||||||
|
|
||||||
const watcher = create_watcher();
|
|
||||||
|
|
||||||
let asset_cache;
|
|
||||||
|
|
||||||
const middleware = compose_handlers([
|
|
||||||
require('webpack-hot-middleware')(compilers.client, {
|
|
||||||
reload: true,
|
|
||||||
path: '/__webpack_hmr',
|
|
||||||
heartbeat: 10 * 1000
|
|
||||||
}),
|
|
||||||
|
|
||||||
async (req, res, next) => {
|
|
||||||
asset_cache = await watcher.ready;
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
|
|
||||||
set_req_pathname,
|
|
||||||
|
|
||||||
get_asset_handler({
|
|
||||||
filter: pathname => pathname === '/index.html',
|
|
||||||
type: 'text/html',
|
|
||||||
cache: 'max-age=600',
|
|
||||||
fn: () => asset_cache.client.index
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_asset_handler({
|
|
||||||
filter: pathname => pathname === '/service-worker.js',
|
|
||||||
type: 'application/javascript',
|
|
||||||
cache: 'max-age=600',
|
|
||||||
fn: () => asset_cache.client.service_worker
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_asset_handler({
|
|
||||||
filter: pathname => pathname.startsWith('/client/'),
|
|
||||||
type: 'application/javascript',
|
|
||||||
cache: 'max-age=31536000',
|
|
||||||
fn: pathname => asset_cache.client.chunks[pathname]
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_route_handler(() => asset_cache),
|
|
||||||
|
|
||||||
get_not_found_handler(() => asset_cache)
|
|
||||||
]);
|
|
||||||
|
|
||||||
middleware.close = () => {
|
|
||||||
watcher.close();
|
|
||||||
// TODO shut down chokidar
|
|
||||||
};
|
|
||||||
|
|
||||||
return middleware;
|
|
||||||
}
|
|
||||||
|
|
||||||
function connect_prod() {
|
|
||||||
const asset_cache = generate_asset_cache(
|
|
||||||
read_json(path.join(dest, 'stats.client.json')),
|
|
||||||
read_json(path.join(dest, 'stats.server.json'))
|
|
||||||
);
|
|
||||||
|
|
||||||
const middleware = compose_handlers([
|
|
||||||
set_req_pathname,
|
|
||||||
|
|
||||||
get_asset_handler({
|
|
||||||
filter: pathname => pathname === '/index.html',
|
|
||||||
type: 'text/html',
|
|
||||||
cache: 'max-age=600',
|
|
||||||
fn: () => asset_cache.client.index
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_asset_handler({
|
|
||||||
filter: pathname => pathname === '/service-worker.js',
|
|
||||||
type: 'application/javascript',
|
|
||||||
cache: 'max-age=600',
|
|
||||||
fn: () => asset_cache.client.service_worker
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_asset_handler({
|
|
||||||
filter: pathname => pathname.startsWith('/client/'),
|
|
||||||
type: 'application/javascript',
|
|
||||||
cache: 'max-age=31536000',
|
|
||||||
fn: pathname => asset_cache.client.chunks[pathname]
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_route_handler(() => asset_cache),
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
function set_req_pathname(req, res, next) {
|
|
||||||
req.pathname = req.url.replace(/\?.+/, '');
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_asset_handler(opts) {
|
|
||||||
return (req, res, next) => {
|
|
||||||
if (!opts.filter(req.pathname)) return next();
|
|
||||||
|
|
||||||
res.set({
|
|
||||||
'Content-Type': opts.type,
|
|
||||||
'Cache-Control': opts.cache
|
|
||||||
});
|
|
||||||
res.end(opts.fn(req.pathname));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_route_handler(fn) {
|
|
||||||
return async function handle_route(req, res, next) {
|
|
||||||
const url = req.pathname;
|
|
||||||
|
|
||||||
const { client, server } = fn();
|
|
||||||
|
|
||||||
// whatever happens, we're going to serve some HTML
|
|
||||||
res.set({
|
|
||||||
'Content-Type': 'text/html'
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (const route of route_manager.routes) {
|
|
||||||
if (route.test(url)) {
|
|
||||||
req.params = route.exec(url);
|
|
||||||
|
|
||||||
const mod = require(server.entry)[route.id];
|
|
||||||
|
|
||||||
if (route.type === 'page') {
|
|
||||||
// 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 data = { params: req.params, query: req.query };
|
|
||||||
|
|
||||||
if (mod.preload) {
|
|
||||||
const promise = Promise.resolve(mod.preload(req)).then(preloaded => {
|
|
||||||
Object.assign(data, preloaded);
|
|
||||||
return mod.render(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
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 {
|
|
||||||
const handler = mod[req.method.toLowerCase()];
|
|
||||||
if (handler) handler(req, res, next);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
} catch(err) {
|
|
||||||
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 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) {
|
|
||||||
return (req, res, next) => {
|
|
||||||
let i = 0;
|
|
||||||
function go() {
|
|
||||||
const handler = handlers[i];
|
|
||||||
|
|
||||||
if (handler) {
|
|
||||||
handler(req, res, () => {
|
|
||||||
i += 1;
|
|
||||||
go();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
go();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function read_json(file) {
|
|
||||||
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
const glob = require('glob');
|
|
||||||
const chokidar = require('chokidar');
|
|
||||||
const create_routes = require('./utils/create_routes.js');
|
|
||||||
const { src, dev } = require('./config.js');
|
|
||||||
|
|
||||||
const callbacks = [];
|
|
||||||
|
|
||||||
exports.onchange = fn => {
|
|
||||||
callbacks.push(fn);
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const glob = require('glob');
|
|
||||||
const chokidar = require('chokidar');
|
|
||||||
const { dev } = require('./config.js');
|
|
||||||
|
|
||||||
let templates;
|
|
||||||
|
|
||||||
function create_templates() {
|
|
||||||
templates = glob.sync('*.html', { cwd: 'templates' })
|
|
||||||
.map(file => {
|
|
||||||
const template = fs.readFileSync(`templates/${file}`, 'utf-8');
|
|
||||||
const status = file.replace('.html', '').toLowerCase();
|
|
||||||
|
|
||||||
if (!/^[0-9x]{3}$/.test(status)) {
|
|
||||||
throw new Error(`Bad template — should be a valid status code like 404.html, or a wildcard like 2xx.html`);
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
if (template) return template.render(data);
|
|
||||||
|
|
||||||
return `Missing template for status code ${status}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
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,11 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const relative = require('require-relative');
|
|
||||||
const webpack = relative('webpack', process.cwd());
|
|
||||||
|
|
||||||
exports.client = webpack(
|
|
||||||
require(path.resolve('webpack.client.config.js'))
|
|
||||||
);
|
|
||||||
|
|
||||||
exports.server = webpack(
|
|
||||||
require(path.resolve('webpack.server.config.js'))
|
|
||||||
);
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const chokidar = require('chokidar');
|
|
||||||
const route_manager = require('../route_manager.js');
|
|
||||||
const { src, entry, dev } = require('../config.js');
|
|
||||||
|
|
||||||
function posixify(file) {
|
|
||||||
return file.replace(/[/\\]/g, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
function create_app() {
|
|
||||||
const { routes } = route_manager;
|
|
||||||
|
|
||||||
function create_client_main() {
|
|
||||||
const template = fs.readFileSync('templates/main.js', 'utf-8');
|
|
||||||
|
|
||||||
const code = `[${
|
|
||||||
routes
|
|
||||||
.filter(route => route.type === 'page')
|
|
||||||
.map(route => {
|
|
||||||
const params = route.dynamic.length === 0 ?
|
|
||||||
'{}' :
|
|
||||||
`{ ${route.dynamic.map((part, i) => `${part}: match[${i + 1}]`).join(', ') } }`;
|
|
||||||
|
|
||||||
const file = posixify(`${src}/${route.file}`);
|
|
||||||
return `{ pattern: ${route.pattern}, params: match => (${params}), load: () => import(/* webpackChunkName: "${route.id}" */ '${file}') }`
|
|
||||||
})
|
|
||||||
.join(', ')
|
|
||||||
}]`;
|
|
||||||
|
|
||||||
let main = template
|
|
||||||
.replace(/__app__/g, posixify(path.resolve(__dirname, '../../runtime/app.js')))
|
|
||||||
.replace(/__routes__/g, code)
|
|
||||||
.replace(/__dev__/g, String(dev));
|
|
||||||
|
|
||||||
if (dev) {
|
|
||||||
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();`
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(entry.client, main);
|
|
||||||
|
|
||||||
// need to fudge the mtime, because webpack is soft in the head
|
|
||||||
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() {
|
|
||||||
const imports = routes
|
|
||||||
.map(route => {
|
|
||||||
const file = posixify(`${src}/${route.file}`);
|
|
||||||
return route.type === 'page' ?
|
|
||||||
`import ${route.id} from '${file}';` :
|
|
||||||
`import * as ${route.id} from '${file}';`;
|
|
||||||
})
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
const exports = `export { ${routes.map(route => route.id)} };`;
|
|
||||||
|
|
||||||
fs.writeFileSync(entry.server, `${imports}\n\n${exports}`);
|
|
||||||
|
|
||||||
const { atime, mtime } = fs.statSync(entry.server);
|
|
||||||
fs.utimesSync(entry.server, new Date(atime.getTime() - 999999), new Date(mtime.getTime() - 999999));
|
|
||||||
}
|
|
||||||
|
|
||||||
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,75 +0,0 @@
|
|||||||
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 = {};
|
|
||||||
|
|
||||||
d.promise = new Promise((fulfil, reject) => {
|
|
||||||
d.fulfil = fulfil;
|
|
||||||
d.reject = reject;
|
|
||||||
});
|
|
||||||
|
|
||||||
return d;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = function create_watcher() {
|
|
||||||
const deferreds = {
|
|
||||||
client: deferred(),
|
|
||||||
server: deferred()
|
|
||||||
};
|
|
||||||
|
|
||||||
const invalidate = () => Promise.all([
|
|
||||||
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()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
function watch_compiler(type) {
|
|
||||||
const compiler = compilers[type];
|
|
||||||
|
|
||||||
compiler.plugin('invalid', filename => {
|
|
||||||
console.log(chalk.cyan(`${type} bundle invalidated, file changed: ${chalk.bold(filename)}`));
|
|
||||||
deferreds[type] = deferred();
|
|
||||||
watcher.ready = invalidate();
|
|
||||||
});
|
|
||||||
|
|
||||||
compiler.plugin('failed', err => {
|
|
||||||
deferreds[type].reject(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
return compiler.watch({}, (err, stats) => {
|
|
||||||
if (stats.hasErrors()) {
|
|
||||||
deferreds[type].reject(stats.toJson().errors[0]);
|
|
||||||
} else {
|
|
||||||
deferreds[type].fulfil(stats);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const watcher = {
|
|
||||||
ready: invalidate(),
|
|
||||||
client: watch_compiler('client'),
|
|
||||||
server: watch_compiler('server'),
|
|
||||||
|
|
||||||
close: () => {
|
|
||||||
watcher.client.close();
|
|
||||||
watcher.server.close();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return watcher;
|
|
||||||
};
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const glob = require('glob');
|
|
||||||
const templates = require('../templates.js');
|
|
||||||
const route_manager = require('../route_manager.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/${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);
|
|
||||||
|
|
||||||
if (dev) {
|
|
||||||
fs.writeFileSync(path.join(dest, 'service-worker.js'), service_worker);
|
|
||||||
fs.writeFileSync(path.join(dest, 'index.html'), index);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
client: {
|
|
||||||
main_file,
|
|
||||||
chunk_files,
|
|
||||||
|
|
||||||
main: read(`${dest}${main_file}`),
|
|
||||||
chunks: chunk_files.reduce((lookup, file) => {
|
|
||||||
lookup[file] = read(`${dest}${file}`);
|
|
||||||
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.main)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
function generate_service_worker(chunk_files) {
|
|
||||||
const assets = glob.sync('**', { cwd: 'assets', nodir: true });
|
|
||||||
|
|
||||||
const route_code = `[${
|
|
||||||
route_manager.routes
|
|
||||||
.filter(route => route.type === 'page')
|
|
||||||
.map(route => `{ pattern: ${route.pattern} }`)
|
|
||||||
.join(', ')
|
|
||||||
}]`;
|
|
||||||
|
|
||||||
return read('templates/service-worker.js')
|
|
||||||
.replace(/__timestamp__/g, Date.now())
|
|
||||||
.replace(/__assets__/g, JSON.stringify(assets))
|
|
||||||
.replace(/__shell__/g, JSON.stringify(chunk_files.concat('/index.html')))
|
|
||||||
.replace(/__routes__/g, route_code);
|
|
||||||
}
|
|
||||||
|
|
||||||
function generate_index(main_file) {
|
|
||||||
return templates.render(200, {
|
|
||||||
styles: '',
|
|
||||||
head: '',
|
|
||||||
html: '<noscript>Please enable JavaScript!</noscript>',
|
|
||||||
main: main_file
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function read(file) {
|
|
||||||
return fs.readFileSync(file, 'utf-8');
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
--require source-map-support/register
|
||||||
--recursive
|
--recursive
|
||||||
test/unit/**/*.js
|
test/unit/**/*.js
|
||||||
test/common/test.js
|
test/common/test.js
|
||||||
6739
package-lock.json
generated
6739
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
50
package.json
50
package.json
@@ -1,16 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "sapper",
|
"name": "sapper",
|
||||||
"version": "0.3.2",
|
"version": "0.7.4",
|
||||||
"description": "Military-grade apps, engineered by Svelte",
|
"description": "Military-grade apps, engineered by Svelte",
|
||||||
"main": "lib/index.js",
|
"main": "middleware.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"sapper": "cli/index.js"
|
"sapper": "cli.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"cli",
|
"cli.js",
|
||||||
"lib",
|
"core.js",
|
||||||
|
"middleware.js",
|
||||||
"runtime",
|
"runtime",
|
||||||
"runtime.js",
|
"runtime.js",
|
||||||
|
"hmr-client.js",
|
||||||
"webpack"
|
"webpack"
|
||||||
],
|
],
|
||||||
"directories": {
|
"directories": {
|
||||||
@@ -18,40 +20,60 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^2.3.0",
|
"chalk": "^2.3.0",
|
||||||
|
"cheerio": "^1.0.0-rc.2",
|
||||||
"chokidar": "^1.7.0",
|
"chokidar": "^1.7.0",
|
||||||
|
"code-frame": "^5.0.0",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
|
"express": "^4.16.2",
|
||||||
|
"get-port": "^3.2.0",
|
||||||
|
"glob": "^7.1.2",
|
||||||
|
"locate-character": "^2.0.5",
|
||||||
|
"mime": "^2.2.0",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
|
"mri": "^1.1.0",
|
||||||
|
"node-fetch": "^1.7.3",
|
||||||
"relative": "^3.0.2",
|
"relative": "^3.0.2",
|
||||||
"require-relative": "^0.8.7",
|
"require-relative": "^0.8.7",
|
||||||
"rimraf": "^2.6.2",
|
"rimraf": "^2.6.2",
|
||||||
"webpack": "^3.10.0",
|
"sander": "^0.6.0",
|
||||||
"webpack-hot-middleware": "^2.21.0"
|
"serialize-javascript": "^1.4.0",
|
||||||
|
"source-map-support": "^0.5.3",
|
||||||
|
"tslib": "^1.8.1",
|
||||||
|
"url-parse": "^1.2.0",
|
||||||
|
"wait-port": "^0.2.2",
|
||||||
|
"walk-sync": "^0.3.2",
|
||||||
|
"webpack": "^3.10.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@std/esm": "^0.19.7",
|
||||||
|
"@types/glob": "^5.0.34",
|
||||||
|
"@types/mkdirp": "^0.5.2",
|
||||||
|
"@types/rimraf": "^2.0.2",
|
||||||
|
"compression": "^1.7.1",
|
||||||
"css-loader": "^0.28.7",
|
"css-loader": "^0.28.7",
|
||||||
|
"electron": "^1.8.2",
|
||||||
"eslint": "^4.13.1",
|
"eslint": "^4.13.1",
|
||||||
"eslint-plugin-import": "^2.8.0",
|
"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",
|
"nightmare": "^2.10.0",
|
||||||
"node-fetch": "^1.7.3",
|
|
||||||
"npm-run-all": "^4.1.2",
|
"npm-run-all": "^4.1.2",
|
||||||
"rollup": "^0.53.0",
|
"rollup": "^0.53.0",
|
||||||
|
"rollup-plugin-json": "^2.3.0",
|
||||||
|
"rollup-plugin-string": "^2.0.2",
|
||||||
"rollup-plugin-typescript": "^0.8.1",
|
"rollup-plugin-typescript": "^0.8.1",
|
||||||
"style-loader": "^0.19.1",
|
"style-loader": "^0.19.1",
|
||||||
"svelte": "^1.49.1",
|
"svelte": "^1.49.1",
|
||||||
"svelte-loader": "^2.3.2",
|
"svelte-loader": "^2.3.2",
|
||||||
"tslib": "^1.8.1",
|
"ts-node": "^4.1.0",
|
||||||
"typescript": "^2.6.2",
|
"typescript": "^2.6.2"
|
||||||
"wait-on": "^2.0.2"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"cy:open": "cypress open",
|
"cy:open": "cypress open",
|
||||||
"test": "mocha --opts mocha.opts",
|
"test": "mocha --opts mocha.opts",
|
||||||
"pretest": "npm run build",
|
"pretest": "npm run build",
|
||||||
"build": "rollup -c",
|
"build": "rollup -c",
|
||||||
"dev": "rollup -cw"
|
"dev": "rollup -cw",
|
||||||
|
"prepublish": "npm test"
|
||||||
},
|
},
|
||||||
"repository": "https://github.com/sveltejs/sapper",
|
"repository": "https://github.com/sveltejs/sapper",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,17 +1,43 @@
|
|||||||
import typescript from 'rollup-plugin-typescript';
|
import typescript from 'rollup-plugin-typescript';
|
||||||
|
import string from 'rollup-plugin-string';
|
||||||
|
import json from 'rollup-plugin-json';
|
||||||
|
import pkg from './package.json';
|
||||||
|
|
||||||
|
const external = [].concat(
|
||||||
|
Object.keys(pkg.dependencies),
|
||||||
|
Object.keys(process.binding('natives')),
|
||||||
|
'sapper/core.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
const paths = {
|
||||||
|
'sapper/core.js': './core.js'
|
||||||
|
};
|
||||||
|
|
||||||
|
const plugins = [
|
||||||
|
string({
|
||||||
|
include: '**/*.md'
|
||||||
|
}),
|
||||||
|
json(),
|
||||||
|
typescript({
|
||||||
|
typescript: require('typescript')
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
// runtime.js
|
{ name: 'cli', banner: true },
|
||||||
{
|
{ name: 'core' },
|
||||||
input: 'src/runtime/index.ts',
|
{ name: 'middleware' },
|
||||||
output: {
|
{ name: 'runtime', format: 'es' },
|
||||||
file: 'runtime.js',
|
{ name: 'webpack', file: 'webpack/config' }
|
||||||
format: 'es'
|
].map(obj => ({
|
||||||
},
|
input: `src/${obj.name}/index.ts`,
|
||||||
plugins: [
|
output: {
|
||||||
typescript({
|
file: `${obj.file || obj.name}.js`,
|
||||||
typescript: require('typescript')
|
format: obj.format || 'cjs',
|
||||||
})
|
banner: obj.banner && '#!/usr/bin/env node',
|
||||||
]
|
paths,
|
||||||
}
|
sourcemap: true
|
||||||
];
|
},
|
||||||
|
external,
|
||||||
|
plugins
|
||||||
|
}));
|
||||||
|
|||||||
1
runtime/README.md
Normal file
1
runtime/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
This directory exists for legacy reasons and should be deleted before releasing version 1.
|
||||||
57
src/cli/build.ts
Normal file
57
src/cli/build.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import mkdirp from 'mkdirp';
|
||||||
|
import rimraf from 'rimraf';
|
||||||
|
import { create_compilers, create_app, create_routes, create_serviceworker } from 'sapper/core.js';
|
||||||
|
|
||||||
|
export default async function build({ src, dest, dev, entry }: {
|
||||||
|
src: string;
|
||||||
|
dest: string;
|
||||||
|
dev: boolean;
|
||||||
|
entry: { client: string, server: string }
|
||||||
|
}) {
|
||||||
|
mkdirp.sync(dest);
|
||||||
|
rimraf.sync(path.join(dest, '**/*'));
|
||||||
|
|
||||||
|
const routes = create_routes({ src });
|
||||||
|
|
||||||
|
// create app/manifest/client.js and app/manifest/server.js
|
||||||
|
create_app({ routes, src, dev });
|
||||||
|
|
||||||
|
const { client, server, serviceworker } = create_compilers();
|
||||||
|
|
||||||
|
const client_stats = await compile(client);
|
||||||
|
fs.writeFileSync(path.join(dest, 'client_info.json'), JSON.stringify(client_stats.toJson()));
|
||||||
|
|
||||||
|
await compile(server);
|
||||||
|
|
||||||
|
if (serviceworker) {
|
||||||
|
create_serviceworker({
|
||||||
|
routes,
|
||||||
|
client_files: client_stats.toJson().assets.map((chunk: { name: string }) => `/client/${chunk.name}`),
|
||||||
|
src
|
||||||
|
});
|
||||||
|
|
||||||
|
await compile(serviceworker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function compile(compiler: any) {
|
||||||
|
return new Promise((fulfil, reject) => {
|
||||||
|
compiler.run((err: Error, stats: any) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.hasErrors()) {
|
||||||
|
console.error(stats.toString({ colors: true }));
|
||||||
|
reject(new Error(`Encountered errors while building app`));
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
fulfil(stats);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
229
src/cli/dev.ts
Normal file
229
src/cli/dev.ts
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as net from 'net';
|
||||||
|
import * as chalk from 'chalk';
|
||||||
|
import * as child_process from 'child_process';
|
||||||
|
import * as http from 'http';
|
||||||
|
import mkdirp from 'mkdirp';
|
||||||
|
import rimraf from 'rimraf';
|
||||||
|
import { wait_for_port } from './utils';
|
||||||
|
import { create_compilers, create_app, create_routes, create_serviceworker, create_template } from 'sapper/core.js';
|
||||||
|
|
||||||
|
type Deferred = {
|
||||||
|
promise?: Promise<any>;
|
||||||
|
fulfil?: (value?: any) => void;
|
||||||
|
reject?: (err: Error) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deferred() {
|
||||||
|
const d: Deferred = {};
|
||||||
|
|
||||||
|
d.promise = new Promise((fulfil, reject) => {
|
||||||
|
d.fulfil = fulfil;
|
||||||
|
d.reject = reject;
|
||||||
|
});
|
||||||
|
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
function create_hot_update_server(port: number, interval = 10000) {
|
||||||
|
const clients = new Set();
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
if (req.url !== '/hmr') return;
|
||||||
|
|
||||||
|
req.socket.setKeepAlive(true);
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Content-Type': 'text/event-stream;charset=utf-8',
|
||||||
|
'Cache-Control': 'no-cache, no-transform',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
// While behind nginx, event stream should not be buffered:
|
||||||
|
// http://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffering
|
||||||
|
'X-Accel-Buffering': 'no'
|
||||||
|
});
|
||||||
|
|
||||||
|
res.write('\n');
|
||||||
|
|
||||||
|
clients.add(res);
|
||||||
|
req.on('close', () => {
|
||||||
|
clients.delete(res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(port);
|
||||||
|
|
||||||
|
function send(data: any) {
|
||||||
|
clients.forEach(client => {
|
||||||
|
client.write(`data: ${JSON.stringify(data)}\n\n`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
send(null)
|
||||||
|
}, interval);
|
||||||
|
|
||||||
|
return { send };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function dev(src: string, dir: string) {
|
||||||
|
rimraf.sync(dir);
|
||||||
|
mkdirp.sync(dir);
|
||||||
|
|
||||||
|
const chokidar = require('chokidar');
|
||||||
|
|
||||||
|
// initial build
|
||||||
|
const dev_port = await require('get-port')(10000);
|
||||||
|
|
||||||
|
const routes = create_routes({ src });
|
||||||
|
create_app({ routes, src, dev: true, dev_port });
|
||||||
|
|
||||||
|
const hot_update_server = create_hot_update_server(dev_port);
|
||||||
|
|
||||||
|
// TODO watch the configs themselves?
|
||||||
|
const compilers = create_compilers();
|
||||||
|
|
||||||
|
function watch_files(pattern: string, callback: () => void) {
|
||||||
|
const watcher = chokidar.watch(pattern, {
|
||||||
|
persistent: false
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher.on('add', callback);
|
||||||
|
watcher.on('change', callback);
|
||||||
|
watcher.on('unlink', callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch_files('routes/**/*.+(html|js|mjs)', () => {
|
||||||
|
const routes = create_routes({ src });
|
||||||
|
create_app({ routes, src, dev: true, dev_port });
|
||||||
|
});
|
||||||
|
|
||||||
|
watch_files('app/template.html', () => {
|
||||||
|
const template = create_template();
|
||||||
|
// TODO reload current page?
|
||||||
|
});
|
||||||
|
|
||||||
|
let proc: child_process.ChildProcess;
|
||||||
|
|
||||||
|
const deferreds = {
|
||||||
|
server: deferred(),
|
||||||
|
client: deferred()
|
||||||
|
};
|
||||||
|
|
||||||
|
const times = {
|
||||||
|
client_start: Date.now(),
|
||||||
|
server_start: Date.now(),
|
||||||
|
serviceworker_start: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
compilers.server.plugin('invalid', () => {
|
||||||
|
times.server_start = Date.now();
|
||||||
|
// TODO print message
|
||||||
|
deferreds.server = deferred();
|
||||||
|
});
|
||||||
|
|
||||||
|
compilers.server.watch({}, (err: Error, stats: any) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(chalk.red(err.message));
|
||||||
|
} else if (stats.hasErrors()) {
|
||||||
|
// print errors. TODO notify client
|
||||||
|
stats.toJson().errors.forEach((error: Error) => {
|
||||||
|
console.error(error); // TODO make this look nice
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(`built server in ${Date.now() - times.server_start}ms`); // TODO prettify
|
||||||
|
|
||||||
|
const server_info = stats.toJson();
|
||||||
|
fs.writeFileSync(path.join(dir, 'server_info.json'), JSON.stringify(server_info, null, ' '));
|
||||||
|
|
||||||
|
deferreds.client.promise.then(() => {
|
||||||
|
function restart() {
|
||||||
|
wait_for_port(3000, deferreds.server.fulfil); // TODO control port
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proc) {
|
||||||
|
proc.kill();
|
||||||
|
proc.on('exit', restart);
|
||||||
|
} else {
|
||||||
|
restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
proc = child_process.fork(`${dir}/server.js`, [], {
|
||||||
|
cwd: process.cwd(),
|
||||||
|
env: Object.assign({}, process.env)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
compilers.client.plugin('invalid', (filename: string) => {
|
||||||
|
times.client_start = Date.now();
|
||||||
|
|
||||||
|
deferreds.client = deferred();
|
||||||
|
|
||||||
|
// TODO we should delete old assets. due to a webpack bug
|
||||||
|
// i don't even begin to comprehend, this is apparently
|
||||||
|
// quite difficult
|
||||||
|
});
|
||||||
|
|
||||||
|
compilers.client.watch({}, (err: Error, stats: any) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(chalk.red(err.message));
|
||||||
|
} else if (stats.hasErrors()) {
|
||||||
|
// print errors. TODO notify client
|
||||||
|
stats.toJson().errors.forEach((error: Error) => {
|
||||||
|
console.error(error); // TODO make this look nice
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(`built client in ${Date.now() - times.client_start}ms`); // TODO prettify
|
||||||
|
|
||||||
|
const client_info = stats.toJson();
|
||||||
|
fs.writeFileSync(path.join(dir, 'client_info.json'), JSON.stringify(client_info, null, ' '));
|
||||||
|
deferreds.client.fulfil();
|
||||||
|
|
||||||
|
const client_files = client_info.assets.map((chunk: { name: string }) => `/client/${chunk.name}`);
|
||||||
|
|
||||||
|
deferreds.server.promise.then(() => {
|
||||||
|
hot_update_server.send({
|
||||||
|
status: 'completed'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
create_serviceworker({
|
||||||
|
routes: create_routes({ src }),
|
||||||
|
client_files,
|
||||||
|
src
|
||||||
|
});
|
||||||
|
|
||||||
|
watch_serviceworker();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let watch_serviceworker = compilers.serviceworker
|
||||||
|
? function() {
|
||||||
|
watch_serviceworker = noop;
|
||||||
|
|
||||||
|
compilers.serviceworker.plugin('invalid', (filename: string) => {
|
||||||
|
times.serviceworker_start = Date.now();
|
||||||
|
});
|
||||||
|
|
||||||
|
compilers.serviceworker.watch({}, (err: Error, stats: any) => {
|
||||||
|
if (err) {
|
||||||
|
// TODO notify client
|
||||||
|
} else if (stats.hasErrors()) {
|
||||||
|
// print errors. TODO notify client
|
||||||
|
stats.toJson().errors.forEach((error: Error) => {
|
||||||
|
console.error(error); // TODO make this look nice
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(`built service worker in ${Date.now() - times.serviceworker_start}ms`); // TODO prettify
|
||||||
|
|
||||||
|
const serviceworker_info = stats.toJson();
|
||||||
|
fs.writeFileSync(path.join(dir, 'serviceworker_info.json'), JSON.stringify(serviceworker_info, null, ' '));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
: noop;
|
||||||
|
}
|
||||||
|
|
||||||
|
function noop() {}
|
||||||
91
src/cli/export.ts
Normal file
91
src/cli/export.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import * as child_process from 'child_process';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as sander from 'sander';
|
||||||
|
import express from 'express';
|
||||||
|
import cheerio from 'cheerio';
|
||||||
|
import URL from 'url-parse';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import { wait_for_port } from './utils';
|
||||||
|
|
||||||
|
const { OUTPUT_DIR = 'dist' } = process.env;
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
function read_json(file: string) {
|
||||||
|
return JSON.parse(sander.readFileSync(file, { encoding: 'utf-8' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function exporter(dir: string) { // dir === '.sapper'
|
||||||
|
// Prep output directory
|
||||||
|
sander.rimrafSync(OUTPUT_DIR);
|
||||||
|
|
||||||
|
sander.copydirSync('assets').to(OUTPUT_DIR);
|
||||||
|
sander.copydirSync(dir, 'client').to(OUTPUT_DIR, 'client');
|
||||||
|
sander.copyFileSync(dir, 'service-worker.js').to(OUTPUT_DIR, 'service-worker.js');
|
||||||
|
|
||||||
|
const port = await require('get-port')(3000);
|
||||||
|
|
||||||
|
const origin = `http://localhost:${port}`;
|
||||||
|
|
||||||
|
const proc = child_process.fork(path.resolve(`${dir}/server.js`), [], {
|
||||||
|
cwd: process.cwd(),
|
||||||
|
env: {
|
||||||
|
PORT: port,
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
SAPPER_EXPORT: 'true'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const seen = new Set();
|
||||||
|
const saved = new Set();
|
||||||
|
|
||||||
|
proc.on('message', message => {
|
||||||
|
if (!message.__sapper__) return;
|
||||||
|
|
||||||
|
const url = new URL(message.url, origin);
|
||||||
|
|
||||||
|
if (saved.has(url.pathname)) return;
|
||||||
|
saved.add(url.pathname);
|
||||||
|
|
||||||
|
if (message.type === 'text/html') {
|
||||||
|
const dest = `${OUTPUT_DIR}/${url.pathname}/index.html`;
|
||||||
|
sander.writeFileSync(dest, message.body);
|
||||||
|
} else {
|
||||||
|
const dest = `${OUTPUT_DIR}/${url.pathname}`;
|
||||||
|
sander.writeFileSync(dest, message.body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handle(url: URL) {
|
||||||
|
if (url.origin !== origin) return;
|
||||||
|
|
||||||
|
if (seen.has(url.pathname)) return;
|
||||||
|
seen.add(url.pathname);
|
||||||
|
|
||||||
|
return fetch(url.href)
|
||||||
|
.then(r => {
|
||||||
|
if (r.headers.get('Content-Type') === 'text/html') {
|
||||||
|
return r.text().then((body: string) => {
|
||||||
|
const $ = cheerio.load(body);
|
||||||
|
const hrefs: string[] = [];
|
||||||
|
|
||||||
|
$('a[href]').each((i: number, $a) => {
|
||||||
|
hrefs.push($a.attribs.href);
|
||||||
|
});
|
||||||
|
|
||||||
|
return hrefs.reduce((promise, href) => {
|
||||||
|
return promise.then(() => handle(new URL(href, url.href)));
|
||||||
|
}, Promise.resolve());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
console.error(`Error rendering ${url.pathname}: ${err.message}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_port(port, () => {
|
||||||
|
handle(new URL(origin)) // TODO all static routes
|
||||||
|
.then(() => proc.kill())
|
||||||
|
});
|
||||||
|
}
|
||||||
20
src/cli/help.md
Normal file
20
src/cli/help.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# sapper v<@version@>
|
||||||
|
|
||||||
|
https://sapper.svelte.technology
|
||||||
|
|
||||||
|
> sapper dev
|
||||||
|
|
||||||
|
Start a development server
|
||||||
|
|
||||||
|
> sapper build
|
||||||
|
|
||||||
|
Creates a production-ready version of your app
|
||||||
|
|
||||||
|
> sapper export
|
||||||
|
|
||||||
|
If possible, exports your app as static files, suitable for hosting on
|
||||||
|
services like Netlify or Surge
|
||||||
|
|
||||||
|
> sapper --help
|
||||||
|
|
||||||
|
Shows this message
|
||||||
61
src/cli/index.ts
Executable file
61
src/cli/index.ts
Executable file
@@ -0,0 +1,61 @@
|
|||||||
|
import mri from 'mri';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import help from './help.md';
|
||||||
|
import build from './build';
|
||||||
|
import exporter from './export';
|
||||||
|
import dev from './dev';
|
||||||
|
import upgrade from './upgrade';
|
||||||
|
import { dest, entry, src } from '../config';
|
||||||
|
import * as pkg from '../../package.json';
|
||||||
|
|
||||||
|
const opts = mri(process.argv.slice(2), {
|
||||||
|
alias: {
|
||||||
|
h: 'help'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (opts.help) {
|
||||||
|
const rendered = help
|
||||||
|
.replace('<@version@>', pkg.version)
|
||||||
|
.replace(/^(.+)/gm, (m: string, $1: string) => /[#>]/.test(m) ? $1 : ` ${$1}`)
|
||||||
|
.replace(/^# (.+)/gm, (m: string, $1: string) => chalk.bold.underline($1))
|
||||||
|
.replace(/^> (.+)/gm, (m: string, $1: string) => chalk.cyan($1));
|
||||||
|
|
||||||
|
console.log(`\n${rendered}\n`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [cmd] = opts._;
|
||||||
|
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
if (cmd === 'build') {
|
||||||
|
process.env.NODE_ENV = 'production';
|
||||||
|
|
||||||
|
build({ dest, dev: false, entry, src })
|
||||||
|
.then(() => {
|
||||||
|
const elapsed = Date.now() - start;
|
||||||
|
console.error(`built in ${elapsed}ms`); // TODO beautify this, e.g. 'built in 4.7 seconds'
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
|
||||||
|
});
|
||||||
|
} else if (cmd === 'export') {
|
||||||
|
process.env.NODE_ENV = 'production';
|
||||||
|
|
||||||
|
build({ dest, dev: false, entry, src })
|
||||||
|
.then(() => exporter(dest))
|
||||||
|
.then(() => {
|
||||||
|
const elapsed = Date.now() - start;
|
||||||
|
console.error(`extracted in ${elapsed}ms`); // TODO beautify this, e.g. 'built in 4.7 seconds'
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
|
||||||
|
});
|
||||||
|
} else if (cmd === 'dev') {
|
||||||
|
dev(src, dest);
|
||||||
|
} else if (cmd === 'upgrade') {
|
||||||
|
upgrade();
|
||||||
|
} else {
|
||||||
|
console.log(`unrecognized command ${cmd} — try \`sapper --help\` for more information`);
|
||||||
|
}
|
||||||
53
src/cli/upgrade.ts
Normal file
53
src/cli/upgrade.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
|
||||||
|
export default async function upgrade() {
|
||||||
|
const upgraded = [
|
||||||
|
await upgrade_sapper_main()
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
if (upgraded.length === 0) {
|
||||||
|
console.log(`No changes!`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upgrade_sapper_main() {
|
||||||
|
const _2xx = read('templates/2xx.html');
|
||||||
|
const _4xx = read('templates/4xx.html');
|
||||||
|
const _5xx = read('templates/5xx.html');
|
||||||
|
|
||||||
|
const pattern = /<script src='\%sapper\.main\%'><\/script>/;
|
||||||
|
|
||||||
|
let replaced = false;
|
||||||
|
|
||||||
|
['2xx', '4xx', '5xx'].forEach(code => {
|
||||||
|
const file = `templates/${code}.html`
|
||||||
|
const template = read(file);
|
||||||
|
if (!template) return;
|
||||||
|
|
||||||
|
if (/\%sapper\.main\%/.test(template)) {
|
||||||
|
if (!pattern.test(template)) {
|
||||||
|
console.log(chalk.red(`Could not replace %sapper.main% in ${file}`));
|
||||||
|
} else {
|
||||||
|
write(file, template.replace(pattern, `%sapper.scripts%`));
|
||||||
|
console.log(chalk.green(`Replaced %sapper.main% in ${file}`));
|
||||||
|
replaced = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return replaced;
|
||||||
|
}
|
||||||
|
|
||||||
|
function read(file: string) {
|
||||||
|
try {
|
||||||
|
return fs.readFileSync(file, 'utf-8');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function write(file: string, data: string) {
|
||||||
|
fs.writeFileSync(file, data);
|
||||||
|
}
|
||||||
24
src/cli/utils.ts
Normal file
24
src/cli/utils.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import waitPort from 'wait-port';
|
||||||
|
|
||||||
|
export function wait_for_port(port: number, cb: () => void) {
|
||||||
|
waitPort({ port }).then(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// import * as net from 'net';
|
||||||
|
|
||||||
|
// export function wait_for_port(port: number, cb: () => void) {
|
||||||
|
// const socket = net.createConnection(port, 'localhost', () => {
|
||||||
|
// cb();
|
||||||
|
// socket.destroy();
|
||||||
|
// });
|
||||||
|
|
||||||
|
// socket.on('error', err => {
|
||||||
|
// setTimeout(() => {
|
||||||
|
// wait_for_port(port, cb);
|
||||||
|
// }, 100);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// setTimeout(() => {
|
||||||
|
// socket.destroy();
|
||||||
|
// }, 100);
|
||||||
|
// }
|
||||||
12
src/config.ts
Normal file
12
src/config.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
export const isDev = () => process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
|
export const templates = path.resolve(process.env.SAPPER_TEMPLATES || 'templates');
|
||||||
|
export const src = path.resolve(process.env.SAPPER_ROUTES || 'routes');
|
||||||
|
export const dest = path.resolve(process.env.SAPPER_DEST || '.sapper');
|
||||||
|
|
||||||
|
export const entry = {
|
||||||
|
client: path.resolve(templates, '.main.rendered.js'),
|
||||||
|
server: path.resolve(dest, 'server-entry.js')
|
||||||
|
};
|
||||||
91
src/core/create_app.ts
Normal file
91
src/core/create_app.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import mkdirp from 'mkdirp';
|
||||||
|
import create_routes from './create_routes';
|
||||||
|
import { fudge_mtime, posixify, write } from './utils';
|
||||||
|
import { Route } from '../interfaces';
|
||||||
|
|
||||||
|
export default function create_app({ routes, src, dev, dev_port }: {
|
||||||
|
routes: Route[];
|
||||||
|
src: string;
|
||||||
|
dev: boolean;
|
||||||
|
dev_port: number;
|
||||||
|
}) {
|
||||||
|
mkdirp.sync('app/manifest');
|
||||||
|
|
||||||
|
write('app/manifest/client.js', generate_client(routes, src, dev, dev_port));
|
||||||
|
write('app/manifest/server.js', generate_server(routes, src));
|
||||||
|
}
|
||||||
|
|
||||||
|
function generate_client(routes: Route[], src: string, dev: boolean, dev_port?: number) {
|
||||||
|
let code = `
|
||||||
|
// This file is generated by Sapper — do not edit it!
|
||||||
|
export const routes = [
|
||||||
|
${routes
|
||||||
|
.filter(route => route.type === 'page')
|
||||||
|
.map(route => {
|
||||||
|
const file = posixify(`../../routes/${route.file}`);
|
||||||
|
|
||||||
|
if (route.id === '_4xx' || route.id === '_5xx') {
|
||||||
|
return `{ error: '${route.id.slice(1)}', load: () => import(/* webpackChunkName: "${route.id}" */ '${file}') }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = route.dynamic.length === 0
|
||||||
|
? '{}'
|
||||||
|
: `{ ${route.dynamic.map((part, i) => `${part}: match[${i + 1}]`).join(', ')} }`;
|
||||||
|
|
||||||
|
return `{ pattern: ${route.pattern}, params: ${route.dynamic.length > 0 ? `match` : `()`} => (${params}), load: () => import(/* webpackChunkName: "${route.id}" */ '${file}') }`;
|
||||||
|
})
|
||||||
|
.join(',\n\t')}
|
||||||
|
];`.replace(/^\t\t/gm, '').trim();
|
||||||
|
|
||||||
|
if (dev) {
|
||||||
|
const hmr_client = posixify(
|
||||||
|
path.resolve(__dirname, 'hmr-client.js')
|
||||||
|
);
|
||||||
|
|
||||||
|
code += `
|
||||||
|
|
||||||
|
if (module.hot) {
|
||||||
|
import('${hmr_client}').then(client => {
|
||||||
|
client.connect(${dev_port});
|
||||||
|
});
|
||||||
|
}`.replace(/^\t{3}/gm, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generate_server(routes: Route[], src: string) {
|
||||||
|
let code = `
|
||||||
|
// This file is generated by Sapper — do not edit it!
|
||||||
|
${routes
|
||||||
|
.map(route => {
|
||||||
|
const file = posixify(`../../routes/${route.file}`);
|
||||||
|
return route.type === 'page'
|
||||||
|
? `import ${route.id} from '${file}';`
|
||||||
|
: `import * as ${route.id} from '${file}';`;
|
||||||
|
})
|
||||||
|
.join('\n')}
|
||||||
|
|
||||||
|
export const routes = [
|
||||||
|
${routes
|
||||||
|
.map(route => {
|
||||||
|
const file = posixify(`${src}/${route.file}`);
|
||||||
|
|
||||||
|
if (route.id === '_4xx' || route.id === '_5xx') {
|
||||||
|
return `{ error: '${route.id.slice(1)}', module: ${route.id} }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = route.dynamic.length === 0
|
||||||
|
? '{}'
|
||||||
|
: `{ ${route.dynamic.map((part, i) => `${part}: match[${i + 1}]`).join(', ')} }`;
|
||||||
|
|
||||||
|
return `{ id: '${route.id}', type: '${route.type}', pattern: ${route.pattern}, params: ${route.dynamic.length > 0 ? `match` : `()`} => (${params}), module: ${route.id} }`;
|
||||||
|
})
|
||||||
|
.join(',\n\t')
|
||||||
|
}
|
||||||
|
];`.replace(/^\t\t/gm, '').trim();
|
||||||
|
|
||||||
|
return code;
|
||||||
|
}
|
||||||
29
src/core/create_compilers.ts
Normal file
29
src/core/create_compilers.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
import relative from 'require-relative';
|
||||||
|
|
||||||
|
export default function create_compilers() {
|
||||||
|
const webpack = relative('webpack', process.cwd());
|
||||||
|
|
||||||
|
const serviceworker_config = try_require(path.resolve('webpack/service-worker.config.js'));
|
||||||
|
|
||||||
|
return {
|
||||||
|
client: webpack(
|
||||||
|
require(path.resolve('webpack/client.config.js'))
|
||||||
|
),
|
||||||
|
|
||||||
|
server: webpack(
|
||||||
|
require(path.resolve('webpack/server.config.js'))
|
||||||
|
),
|
||||||
|
|
||||||
|
serviceworker: serviceworker_config && webpack(serviceworker_config)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function try_require(specifier: string) {
|
||||||
|
try {
|
||||||
|
return require(specifier);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === 'MODULE_NOT_FOUND') return null;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
const path = require('path');
|
import * as path from 'path';
|
||||||
|
import glob from 'glob';
|
||||||
|
import { Route } from '../interfaces';
|
||||||
|
|
||||||
module.exports = function create_matchers(files) {
|
export default function create_routes({ src, files = glob.sync('**/*.+(html|js|mjs)', { cwd: src }) }: {
|
||||||
const routes = files
|
src: string;
|
||||||
.map(file => {
|
files?: string[];
|
||||||
|
}) {
|
||||||
|
const routes: Route[] = files
|
||||||
|
.map((file: string) => {
|
||||||
if (/(^|\/|\\)_/.test(file)) return;
|
if (/(^|\/|\\)_/.test(file)) return;
|
||||||
|
|
||||||
const parts = file.replace(/\.(html|js|mjs)$/, '').split('/'); // glob output is always posix-style
|
const parts = file.replace(/\.(html|js|mjs)$/, '').split('/'); // glob output is always posix-style
|
||||||
@@ -31,15 +36,15 @@ module.exports = function create_matchers(files) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pattern = new RegExp(`^${pattern_string || '\\/'}$`);
|
const pattern = new RegExp(`^${pattern_string}\\/?$`);
|
||||||
|
|
||||||
const test = url => pattern.test(url);
|
const test = (url: string) => pattern.test(url);
|
||||||
|
|
||||||
const exec = url => {
|
const exec = (url: string) => {
|
||||||
const match = pattern.exec(url);
|
const match = pattern.exec(url);
|
||||||
if (!match) return;
|
if (!match) return;
|
||||||
|
|
||||||
const params = {};
|
const params: Record<string, string> = {};
|
||||||
dynamic.forEach((param, i) => {
|
dynamic.forEach((param, i) => {
|
||||||
params[param] = match[i + 1];
|
params[param] = match[i + 1];
|
||||||
});
|
});
|
||||||
@@ -59,7 +64,7 @@ module.exports = function create_matchers(files) {
|
|||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.sort((a, b) => {
|
.sort((a: Route, b: Route) => {
|
||||||
let same = true;
|
let same = true;
|
||||||
|
|
||||||
for (let i = 0; true; i += 1) {
|
for (let i = 0; true; i += 1) {
|
||||||
@@ -78,7 +83,7 @@ module.exports = function create_matchers(files) {
|
|||||||
const b_is_dynamic = b_part[0] === '[';
|
const b_is_dynamic = b_part[0] === '[';
|
||||||
|
|
||||||
if (a_is_dynamic === b_is_dynamic) {
|
if (a_is_dynamic === b_is_dynamic) {
|
||||||
if (!a_is_dynamic) same = false;
|
if (!a_is_dynamic && a_part !== b_part) same = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,4 +92,4 @@ module.exports = function create_matchers(files) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return routes;
|
return routes;
|
||||||
};
|
}
|
||||||
27
src/core/create_serviceworker.ts
Normal file
27
src/core/create_serviceworker.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import glob from 'glob';
|
||||||
|
import create_routes from './create_routes';
|
||||||
|
import { fudge_mtime, posixify, write } from './utils';
|
||||||
|
import { Route } from '../interfaces';
|
||||||
|
|
||||||
|
export default function create_serviceworker({ routes, client_files, src }: {
|
||||||
|
routes: Route[];
|
||||||
|
client_files: string[];
|
||||||
|
src: string;
|
||||||
|
}) {
|
||||||
|
const assets = glob.sync('**', { cwd: 'assets', nodir: true });
|
||||||
|
|
||||||
|
let code = `
|
||||||
|
// This file is generated by Sapper — do not edit it!
|
||||||
|
export const timestamp = ${Date.now()};
|
||||||
|
|
||||||
|
export const assets = [\n\t${assets.map((x: string) => `"${x}"`).join(',\n\t')}\n];
|
||||||
|
|
||||||
|
export const shell = [\n\t${client_files.map((x: string) => `"${x}"`).join(',\n\t')}\n];
|
||||||
|
|
||||||
|
export const routes = [\n\t${routes.filter((r: Route) => r.type === 'page' && !/^_[45]xx$/.test(r.id)).map((r: Route) => `{ pattern: ${r.pattern} }`).join(',\n\t')}\n];
|
||||||
|
`.replace(/^\t\t/gm, '').trim();
|
||||||
|
|
||||||
|
write('app/manifest/service-worker.js', code);
|
||||||
|
}
|
||||||
39
src/core/create_template.ts
Normal file
39
src/core/create_template.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import framer from 'code-frame';
|
||||||
|
import { locate } from 'locate-character';
|
||||||
|
|
||||||
|
function error(e: any) {
|
||||||
|
if (e.title) console.error(chalk.bold.red(e.title));
|
||||||
|
if (e.body) console.error(chalk.red(e.body));
|
||||||
|
if (e.url) console.error(chalk.cyan(e.url));
|
||||||
|
if (e.frame) console.error(chalk.grey(e.frame));
|
||||||
|
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function create_templates() {
|
||||||
|
const template = fs.readFileSync(`app/template.html`, 'utf-8');
|
||||||
|
|
||||||
|
const index = template.indexOf('%sapper.main%');
|
||||||
|
if (index !== -1) {
|
||||||
|
// TODO remove this in a future version
|
||||||
|
const { line, column } = locate(template, index, { offsetLine: 1 });
|
||||||
|
const frame = framer(template, line, column);
|
||||||
|
|
||||||
|
error({
|
||||||
|
title: `app/template.html`,
|
||||||
|
body: `<script src='%sapper.main%'> is unsupported — use %sapper.scripts% (without the <script> tag) instead`,
|
||||||
|
url: 'https://github.com/sveltejs/sapper/issues/86',
|
||||||
|
frame
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
render: (data: Record<string, string>) => {
|
||||||
|
return template.replace(/%sapper\.(\w+)%/g, (match, key) => {
|
||||||
|
return key in data ? data[key] : '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
5
src/core/index.ts
Normal file
5
src/core/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export { default as create_app } from './create_app';
|
||||||
|
export { default as create_serviceworker } from './create_serviceworker';
|
||||||
|
export { default as create_compilers } from './create_compilers';
|
||||||
|
export { default as create_routes } from './create_routes';
|
||||||
|
export { default as create_template } from './create_template';
|
||||||
20
src/core/utils.ts
Normal file
20
src/core/utils.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
export function write(file: string, code: string) {
|
||||||
|
fs.writeFileSync(file, code);
|
||||||
|
fudge_mtime(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function posixify(file: string) {
|
||||||
|
return file.replace(/[/\\]/g, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fudge_mtime(file: string) {
|
||||||
|
// need to fudge the mtime so that webpack doesn't go doolally
|
||||||
|
const { atime, mtime } = fs.statSync(file);
|
||||||
|
fs.utimesSync(
|
||||||
|
file,
|
||||||
|
new Date(atime.getTime() - 999999),
|
||||||
|
new Date(mtime.getTime() - 999999)
|
||||||
|
);
|
||||||
|
}
|
||||||
15
src/interfaces.ts
Normal file
15
src/interfaces.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export type Route = {
|
||||||
|
id: string;
|
||||||
|
type: 'page' | 'route';
|
||||||
|
file: string;
|
||||||
|
pattern: RegExp;
|
||||||
|
test: (url: string) => boolean;
|
||||||
|
exec: (url: string) => Record<string, string>;
|
||||||
|
parts: string[];
|
||||||
|
dynamic: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Template = {
|
||||||
|
render: (data: Record<string, string>) => string;
|
||||||
|
stream: (req, res, data: Record<string, string | Promise<string>>) => void;
|
||||||
|
};
|
||||||
350
src/middleware/index.ts
Normal file
350
src/middleware/index.ts
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { ClientRequest, ServerResponse } from 'http';
|
||||||
|
// import * as mime from 'mime';
|
||||||
|
import mkdirp from 'mkdirp';
|
||||||
|
import rimraf from 'rimraf';
|
||||||
|
import serialize from 'serialize-javascript';
|
||||||
|
import escape_html from 'escape-html';
|
||||||
|
import { create_routes, templates, create_compilers, create_template } from 'sapper/core.js';
|
||||||
|
import { dest, entry, isDev, src } from '../config';
|
||||||
|
import { Route, Template } from '../interfaces';
|
||||||
|
import sourceMapSupport from 'source-map-support';
|
||||||
|
|
||||||
|
sourceMapSupport.install();
|
||||||
|
|
||||||
|
const dev = isDev();
|
||||||
|
|
||||||
|
type RouteObject = {
|
||||||
|
id: string;
|
||||||
|
type: 'page' | 'route';
|
||||||
|
pattern: RegExp;
|
||||||
|
params: (match: RegExpMatchArray) => Record<string, string>;
|
||||||
|
module: {
|
||||||
|
render: (data: any) => {
|
||||||
|
head: string;
|
||||||
|
css: { code: string, map: any };
|
||||||
|
html: string
|
||||||
|
},
|
||||||
|
preload: (data: any) => any | Promise<any>
|
||||||
|
};
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Handler = (req: Req, res: ServerResponse, next: () => void) => void;
|
||||||
|
|
||||||
|
interface Req extends ClientRequest {
|
||||||
|
url: string;
|
||||||
|
method: string;
|
||||||
|
pathname: string;
|
||||||
|
params: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function middleware({ routes }: {
|
||||||
|
routes: RouteObject[]
|
||||||
|
}) {
|
||||||
|
const client_info = JSON.parse(fs.readFileSync(path.join(dest, 'client_info.json'), 'utf-8'));
|
||||||
|
|
||||||
|
const template = create_template();
|
||||||
|
|
||||||
|
const shell = try_read(path.join(dest, 'index.html'));
|
||||||
|
const serviceworker = try_read(path.join(dest, 'service-worker.js'));
|
||||||
|
|
||||||
|
const middleware = compose_handlers([
|
||||||
|
(req: Req, res: ServerResponse, next: () => void) => {
|
||||||
|
req.pathname = req.url.replace(/\?.*/, '');
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
|
||||||
|
shell && get_asset_handler({
|
||||||
|
pathname: '/index.html',
|
||||||
|
type: 'text/html',
|
||||||
|
cache: 'max-age=600',
|
||||||
|
body: shell
|
||||||
|
}),
|
||||||
|
|
||||||
|
serviceworker && get_asset_handler({
|
||||||
|
pathname: '/service-worker.js',
|
||||||
|
type: 'application/javascript',
|
||||||
|
cache: 'max-age=600',
|
||||||
|
body: serviceworker
|
||||||
|
}),
|
||||||
|
|
||||||
|
(req: Req, res: ServerResponse, next: () => void) => {
|
||||||
|
if (req.pathname.startsWith('/client/')) {
|
||||||
|
// const type = mime.getType(req.pathname);
|
||||||
|
const type = 'application/javascript'; // TODO might not be, if using e.g. CSS plugin
|
||||||
|
|
||||||
|
// TODO cache?
|
||||||
|
const rs = fs.createReadStream(path.join(dest, req.pathname.slice(1)));
|
||||||
|
|
||||||
|
rs.on('error', error => {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end('not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', type);
|
||||||
|
res.setHeader('Cache-Control', 'max-age=31536000');
|
||||||
|
rs.pipe(res);
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
get_route_handler(client_info.assetsByChunkName, routes, template)
|
||||||
|
].filter(Boolean));
|
||||||
|
|
||||||
|
return middleware;
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_asset_handler({ pathname, type, cache, body }: {
|
||||||
|
pathname: string;
|
||||||
|
type: string;
|
||||||
|
cache: string;
|
||||||
|
body: string;
|
||||||
|
}) {
|
||||||
|
return (req: Req, res: ServerResponse, next: () => void) => {
|
||||||
|
if (req.pathname !== pathname) return next();
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', type);
|
||||||
|
res.setHeader('Cache-Control', cache);
|
||||||
|
res.end(body);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved = Promise.resolve();
|
||||||
|
|
||||||
|
function get_route_handler(chunks: Record<string, string>, routes: RouteObject[], template: Template) {
|
||||||
|
function handle_route(route: RouteObject, req: Req, res: ServerResponse) {
|
||||||
|
req.params = route.params(route.pattern.exec(req.pathname));
|
||||||
|
|
||||||
|
const mod = route.module;
|
||||||
|
|
||||||
|
if (route.type === 'page') {
|
||||||
|
res.setHeader('Content-Type', 'text/html');
|
||||||
|
|
||||||
|
// preload main.js and current route
|
||||||
|
// TODO detect other stuff we can preload? images, CSS, fonts?
|
||||||
|
const link = []
|
||||||
|
.concat(chunks.main, chunks[route.id])
|
||||||
|
.map(file => `</client/${file}>;rel="preload";as="script"`)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
res.setHeader('Link', link);
|
||||||
|
|
||||||
|
const data = { params: req.params, query: req.query };
|
||||||
|
|
||||||
|
let redirect: { statusCode: number, location: string };
|
||||||
|
let error: { statusCode: number, message: Error | string };
|
||||||
|
|
||||||
|
Promise.resolve(
|
||||||
|
mod.preload ? mod.preload.call({
|
||||||
|
redirect: (statusCode: number, location: string) => {
|
||||||
|
redirect = { statusCode, location };
|
||||||
|
},
|
||||||
|
error: (statusCode: number, message: Error | string) => {
|
||||||
|
error = { statusCode, message };
|
||||||
|
}
|
||||||
|
}, req) : {}
|
||||||
|
).catch(err => {
|
||||||
|
error = { statusCode: 500, message: err };
|
||||||
|
}).then(preloaded => {
|
||||||
|
if (redirect) {
|
||||||
|
res.statusCode = redirect.statusCode;
|
||||||
|
res.setHeader('Location', redirect.location);
|
||||||
|
res.end();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
handle_error(req, res, error.statusCode, error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serialized = try_serialize(preloaded); // TODO bail on non-POJOs
|
||||||
|
Object.assign(data, preloaded);
|
||||||
|
|
||||||
|
const { html, head, css } = mod.render(data);
|
||||||
|
|
||||||
|
let scripts = []
|
||||||
|
.concat(chunks.main) // chunks main might be an array. it might not! thanks, webpack
|
||||||
|
.map(file => `<script src='/client/${file}'></script>`)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
scripts = `<script>__SAPPER__ = { preloaded: ${serialized} };</script>${scripts}`;
|
||||||
|
|
||||||
|
const page = template.render({
|
||||||
|
scripts,
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (process.send) {
|
||||||
|
process.send({
|
||||||
|
__sapper__: true,
|
||||||
|
url: req.url,
|
||||||
|
method: req.method,
|
||||||
|
status: 200,
|
||||||
|
type: 'text/html',
|
||||||
|
body: page
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
const method = req.method.toLowerCase();
|
||||||
|
// 'delete' cannot be exported from a module because it is a keyword,
|
||||||
|
// so check for 'del' instead
|
||||||
|
const method_export = method === 'delete' ? 'del' : method;
|
||||||
|
const handler = mod[method_export];
|
||||||
|
if (handler) {
|
||||||
|
if (process.env.SAPPER_EXPORT) {
|
||||||
|
const { write, end, setHeader } = res;
|
||||||
|
const chunks: any[] = [];
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
|
||||||
|
// intercept data so that it can be exported
|
||||||
|
res.write = function(chunk: any) {
|
||||||
|
chunks.push(new Buffer(chunk));
|
||||||
|
write.apply(res, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
res.setHeader = function(name: string, value: string) {
|
||||||
|
headers[name.toLowerCase()] = value;
|
||||||
|
setHeader.apply(res, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
res.end = function(chunk?: any) {
|
||||||
|
if (chunk) chunks.push(new Buffer(chunk));
|
||||||
|
end.apply(res, arguments);
|
||||||
|
|
||||||
|
process.send({
|
||||||
|
__sapper__: true,
|
||||||
|
url: req.url,
|
||||||
|
method: req.method,
|
||||||
|
status: res.statusCode,
|
||||||
|
type: headers['content-type'],
|
||||||
|
body: Buffer.concat(chunks).toString()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handler(req, res, () => {
|
||||||
|
handle_not_found(req, res, 404, 'Not found');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// no matching handler for method — 404
|
||||||
|
handle_not_found(req, res, 404, 'Not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const not_found_route = routes.find((route: RouteObject) => route.error === '4xx');
|
||||||
|
|
||||||
|
function handle_not_found(req: Req, res: ServerResponse, statusCode: number, message: Error | string) {
|
||||||
|
res.statusCode = statusCode;
|
||||||
|
res.setHeader('Content-Type', 'text/html');
|
||||||
|
|
||||||
|
const error = message instanceof Error ? message : new Error(message);
|
||||||
|
|
||||||
|
const rendered = not_found_route ? not_found_route.module.render({
|
||||||
|
status: 404,
|
||||||
|
error
|
||||||
|
}) : { head: '', css: null, html: error.message };
|
||||||
|
|
||||||
|
const { head, css, html } = rendered;
|
||||||
|
|
||||||
|
res.end(template.render({
|
||||||
|
scripts: `<script src='/client/${chunks.main}'></script>`,
|
||||||
|
html,
|
||||||
|
head: `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`,
|
||||||
|
styles: (css && css.code ? `<style>${css.code}</style>` : '')
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const error_route = routes.find((route: RouteObject) => route.error === '5xx');
|
||||||
|
|
||||||
|
function handle_error(req: Req, res: ServerResponse, statusCode: number, message: Error | string) {
|
||||||
|
if (statusCode >= 400 && statusCode < 500) {
|
||||||
|
return handle_not_found(req, res, statusCode, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.statusCode = statusCode;
|
||||||
|
res.setHeader('Content-Type', 'text/html');
|
||||||
|
|
||||||
|
const error = message instanceof Error ? message : new Error(message);
|
||||||
|
|
||||||
|
const rendered = error_route ? error_route.module.render({
|
||||||
|
status: 500,
|
||||||
|
error
|
||||||
|
}) : { head: '', css: null, html: `Internal server error: ${error.message}` };
|
||||||
|
|
||||||
|
const { head, css, html } = rendered;
|
||||||
|
|
||||||
|
res.end(template.render({
|
||||||
|
scripts: `<script src='/client/${chunks.main}'></script>`,
|
||||||
|
html,
|
||||||
|
head: `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`,
|
||||||
|
styles: (css && css.code ? `<style>${css.code}</style>` : '')
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return function find_route(req: Req, res: ServerResponse, next: () => void) {
|
||||||
|
const url = req.pathname;
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const route of routes) {
|
||||||
|
if (!route.error && route.pattern.test(url)) return handle_route(route, req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_not_found(req, res, 404, 'Not found');
|
||||||
|
} catch (error) {
|
||||||
|
handle_error(req, res, 500, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function compose_handlers(handlers: Handler[]) {
|
||||||
|
return (req: Req, res: ServerResponse, next: () => void) => {
|
||||||
|
let i = 0;
|
||||||
|
function go() {
|
||||||
|
const handler = handlers[i];
|
||||||
|
|
||||||
|
if (handler) {
|
||||||
|
handler(req, res, () => {
|
||||||
|
i += 1;
|
||||||
|
go();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function read_json(file: string) {
|
||||||
|
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function try_serialize(data: any) {
|
||||||
|
try {
|
||||||
|
return serialize(data);
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function try_read(file: string) {
|
||||||
|
try {
|
||||||
|
return fs.readFileSync(file, 'utf-8');
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { detach, findAnchor, scroll_state, which } from './utils';
|
import { detach, findAnchor, scroll_state, which } from './utils';
|
||||||
import { Component, ComponentConstructor, Params, Query, Route, RouteData, ScrollPosition } from './interfaces';
|
import { Component, ComponentConstructor, Params, Query, Route, RouteData, ScrollPosition, Target } from './interfaces';
|
||||||
|
|
||||||
export let component: Component;
|
export let component: Component;
|
||||||
let target: Node;
|
let target: Node;
|
||||||
let routes: Route[];
|
let routes: Route[];
|
||||||
|
let errors: { '4xx': Route, '5xx': Route };
|
||||||
|
|
||||||
const history = typeof window !== 'undefined' ? window.history : {
|
const history = typeof window !== 'undefined' ? window.history : {
|
||||||
pushState: (state: any, title: string, href: string) => {},
|
pushState: (state: any, title: string, href: string) => {},
|
||||||
@@ -19,7 +20,7 @@ if ('scrollRestoration' in history) {
|
|||||||
history.scrollRestoration = 'manual';
|
history.scrollRestoration = 'manual';
|
||||||
}
|
}
|
||||||
|
|
||||||
function select_route(url: URL): { route: Route, data: RouteData } {
|
function select_route(url: URL): Target {
|
||||||
if (url.origin !== window.location.origin) return null;
|
if (url.origin !== window.location.origin) return null;
|
||||||
|
|
||||||
for (const route of routes) {
|
for (const route of routes) {
|
||||||
@@ -28,9 +29,13 @@ function select_route(url: URL): { route: Route, data: RouteData } {
|
|||||||
const params = route.params(match);
|
const params = route.params(match);
|
||||||
|
|
||||||
const query: Record<string, string | true> = {};
|
const query: Record<string, string | true> = {};
|
||||||
for (const [key, value] of url.searchParams) query[key] = value || true;
|
if (url.search.length > 0) {
|
||||||
|
url.search.slice(1).split('&').forEach(searchParam => {
|
||||||
return { route, data: { params, query } };
|
const [, key, value] = /([^=]+)=(.*)/.exec(searchParam);
|
||||||
|
query[key] = value || true;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return { url, route, data: { params, query } };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,7 +65,7 @@ function render(Component: ComponentConstructor, data: any, scroll: ScrollPositi
|
|||||||
component = new Component({
|
component = new Component({
|
||||||
target,
|
target,
|
||||||
data,
|
data,
|
||||||
hydrate: !!component
|
hydrate: !component
|
||||||
});
|
});
|
||||||
|
|
||||||
if (scroll) {
|
if (scroll) {
|
||||||
@@ -68,44 +73,74 @@ function render(Component: ComponentConstructor, data: any, scroll: ScrollPositi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepare_route(Component, data) {
|
function prepare_route(Component: ComponentConstructor, data: RouteData) {
|
||||||
return Promise.resolve(
|
let redirect: { statusCode: number, location: string } = null;
|
||||||
Component.preload ? Component.preload(data) : {}
|
let error: { statusCode: number, message: Error | string } = null;
|
||||||
).then(preloaded => {
|
|
||||||
|
if (!Component.preload) {
|
||||||
|
return { Component, data, redirect, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!component && window.__SAPPER__ && window.__SAPPER__.preloaded) {
|
||||||
|
return { Component, data: Object.assign(data, window.__SAPPER__.preloaded), redirect, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(Component.preload.call({
|
||||||
|
redirect: (statusCode: number, location: string) => {
|
||||||
|
redirect = { statusCode, location };
|
||||||
|
},
|
||||||
|
error: (statusCode: number, message: Error | string) => {
|
||||||
|
error = { statusCode, message };
|
||||||
|
}
|
||||||
|
}, data)).catch(err => {
|
||||||
|
error = { statusCode: 500, message: err };
|
||||||
|
}).then(preloaded => {
|
||||||
|
if (error) {
|
||||||
|
const route = error.statusCode >= 400 && error.statusCode < 500
|
||||||
|
? errors['4xx']
|
||||||
|
: errors['5xx'];
|
||||||
|
|
||||||
|
return route.load().then(({ default: Component }: { default: ComponentConstructor }) => {
|
||||||
|
const err = error.message instanceof Error ? error.message : new Error(error.message);
|
||||||
|
Object.assign(data, { status: error.statusCode, error: err });
|
||||||
|
return { Component, data, redirect: null };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Object.assign(data, preloaded)
|
Object.assign(data, preloaded)
|
||||||
return { Component, data };
|
return { Component, data, redirect };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigate(url: URL, id: number) {
|
function navigate(target: Target, id: number) {
|
||||||
const selected = select_route(url);
|
if (id) {
|
||||||
if (selected) {
|
// popstate or initial navigation
|
||||||
if (id) {
|
cid = id;
|
||||||
// popstate or initial navigation
|
} else {
|
||||||
cid = id;
|
// clicked on a link. preserve scroll state
|
||||||
} else {
|
scroll_history[cid] = scroll_state();
|
||||||
// clicked on a link. preserve scroll state
|
|
||||||
scroll_history[cid] = scroll_state();
|
|
||||||
|
|
||||||
id = cid = ++uid;
|
id = cid = ++uid;
|
||||||
scroll_history[cid] = { x: 0, y: 0 };
|
scroll_history[cid] = { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
cid = id;
|
||||||
|
|
||||||
|
const loaded = prefetching && prefetching.href === target.url.href ?
|
||||||
|
prefetching.promise :
|
||||||
|
target.route.load().then(mod => prepare_route(mod.default, target.data));
|
||||||
|
|
||||||
|
prefetching = null;
|
||||||
|
|
||||||
|
const token = current_token = {};
|
||||||
|
|
||||||
|
return loaded.then(({ Component, data, redirect }) => {
|
||||||
|
if (redirect) {
|
||||||
|
return goto(redirect.location, { replaceState: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const loaded = prefetching && prefetching.href === url.href ?
|
render(Component, data, scroll_history[id], token);
|
||||||
prefetching.promise :
|
});
|
||||||
selected.route.load().then(mod => prepare_route(mod.default, selected.data));
|
|
||||||
|
|
||||||
prefetching = null;
|
|
||||||
|
|
||||||
const token = current_token = {};
|
|
||||||
|
|
||||||
loaded.then(({ Component, data }) => {
|
|
||||||
render(Component, data, scroll_history[id], token);
|
|
||||||
});
|
|
||||||
|
|
||||||
cid = id;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handle_click(event: MouseEvent) {
|
function handle_click(event: MouseEvent) {
|
||||||
@@ -141,7 +176,9 @@ function handle_click(event: MouseEvent) {
|
|||||||
// Don't handle hash changes
|
// Don't handle hash changes
|
||||||
if (url.pathname === window.location.pathname && url.search === window.location.search) return;
|
if (url.pathname === window.location.pathname && url.search === window.location.search) return;
|
||||||
|
|
||||||
if (navigate(url, null)) {
|
const target = select_route(url);
|
||||||
|
if (target) {
|
||||||
|
navigate(target, null);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
history.pushState({ id: cid }, '', url.href);
|
history.pushState({ id: cid }, '', url.href);
|
||||||
}
|
}
|
||||||
@@ -151,7 +188,9 @@ function handle_popstate(event: PopStateEvent) {
|
|||||||
scroll_history[cid] = scroll_state();
|
scroll_history[cid] = scroll_state();
|
||||||
|
|
||||||
if (event.state) {
|
if (event.state) {
|
||||||
navigate(new URL(window.location.href), event.state.id);
|
const url = new URL(window.location.href);
|
||||||
|
const target = select_route(url);
|
||||||
|
navigate(target, event.state.id);
|
||||||
} else {
|
} else {
|
||||||
// hashchange
|
// hashchange
|
||||||
cid = ++uid;
|
cid = ++uid;
|
||||||
@@ -176,17 +215,21 @@ export function prefetch(href: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handle_touchstart_mouseover(event: MouseEvent | TouchEvent) {
|
function handle_touchstart_mouseover(event: MouseEvent | TouchEvent) {
|
||||||
const a: HTMLAnchorElement = <HTMLAnchorElement>findAnchor(<Node>event.target);
|
const a: HTMLAnchorElement = <HTMLAnchorElement>findAnchor(<Node>event.target);
|
||||||
if (!a || a.rel !== 'prefetch') return;
|
if (!a || a.rel !== 'prefetch') return;
|
||||||
|
|
||||||
prefetch(a.href);
|
prefetch(a.href);
|
||||||
}
|
}
|
||||||
|
|
||||||
let inited: boolean;
|
let inited: boolean;
|
||||||
|
|
||||||
export function init(_target: Node, _routes: Route[]) {
|
export function init(_target: Node, _routes: Route[]) {
|
||||||
target = _target;
|
target = _target;
|
||||||
routes = _routes;
|
routes = _routes.filter(r => !r.error);
|
||||||
|
errors = {
|
||||||
|
'4xx': _routes.find(r => r.error === '4xx'),
|
||||||
|
'5xx': _routes.find(r => r.error === '5xx')
|
||||||
|
};
|
||||||
|
|
||||||
if (!inited) { // this check makes HMR possible
|
if (!inited) { // this check makes HMR possible
|
||||||
window.addEventListener('click', handle_click);
|
window.addEventListener('click', handle_click);
|
||||||
@@ -199,23 +242,27 @@ export function init(_target: Node, _routes: Route[]) {
|
|||||||
inited = true;
|
inited = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
return Promise.resolve().then(() => {
|
||||||
const { hash, href } = window.location;
|
const { hash, href } = window.location;
|
||||||
|
|
||||||
const deep_linked = hash && document.querySelector(hash);
|
const deep_linked = hash && document.getElementById(hash.slice(1));
|
||||||
scroll_history[uid] = deep_linked ?
|
scroll_history[uid] = deep_linked ?
|
||||||
{ x: 0, y: deep_linked.getBoundingClientRect().top } :
|
{ x: 0, y: deep_linked.getBoundingClientRect().top } :
|
||||||
scroll_state();
|
scroll_state();
|
||||||
|
|
||||||
history.replaceState({ id: uid }, '', href);
|
history.replaceState({ id: uid }, '', href);
|
||||||
navigate(new URL(window.location.href), uid);
|
|
||||||
|
const target = select_route(new URL(window.location.href));
|
||||||
|
return navigate(target, uid);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function goto(href: string, opts = { replaceState: false }) {
|
export function goto(href: string, opts = { replaceState: false }) {
|
||||||
if (navigate(new URL(href, window.location.href), null)) {
|
const target = select_route(new URL(href, window.location.href));
|
||||||
|
if (target) {
|
||||||
|
navigate(target, null);
|
||||||
if (history) history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
|
if (history) history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
|
||||||
} else {
|
} else {
|
||||||
window.location.href = href;
|
window.location.href = href;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,4 +20,10 @@ export type Route = {
|
|||||||
export type ScrollPosition = {
|
export type ScrollPosition = {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Target = {
|
||||||
|
url: URL;
|
||||||
|
route: Route;
|
||||||
|
data: RouteData;
|
||||||
};
|
};
|
||||||
44
src/webpack/index.ts
Normal file
44
src/webpack/index.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { dest, isDev, entry } from '../config';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
dev: isDev(),
|
||||||
|
|
||||||
|
client: {
|
||||||
|
entry: () => {
|
||||||
|
return {
|
||||||
|
main: [
|
||||||
|
'./app/client.js',
|
||||||
|
// workaround for https://github.com/webpack-contrib/extract-text-webpack-plugin/issues/456
|
||||||
|
'style-loader/lib/addStyles',
|
||||||
|
'css-loader/lib/css-base'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
output: () => {
|
||||||
|
return {
|
||||||
|
path: `${dest}/client`,
|
||||||
|
filename: '[hash]/[name].js',
|
||||||
|
chunkFilename: '[hash]/[name].[id].js',
|
||||||
|
publicPath: '/client/'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
server: {
|
||||||
|
entry: () => {
|
||||||
|
return {
|
||||||
|
server: './app/server.js'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
output: () => {
|
||||||
|
return {
|
||||||
|
path: `${dest}`,
|
||||||
|
filename: '[name].js',
|
||||||
|
chunkFilename: '[hash]/[name].[id].js',
|
||||||
|
libraryTarget: 'commonjs2'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
3
test/app/.gitignore
vendored
3
test/app/.gitignore
vendored
@@ -3,4 +3,5 @@ node_modules
|
|||||||
.sapper
|
.sapper
|
||||||
yarn.lock
|
yarn.lock
|
||||||
cypress/screenshots
|
cypress/screenshots
|
||||||
templates/.*
|
templates/.*
|
||||||
|
dist
|
||||||
|
|||||||
6
test/app/app/client.js
Normal file
6
test/app/app/client.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { init } from '../../../runtime.js';
|
||||||
|
import { routes } from './manifest/client.js';
|
||||||
|
|
||||||
|
window.init = () => {
|
||||||
|
return init(document.querySelector('#sapper'), routes);
|
||||||
|
};
|
||||||
78
test/app/app/server.js
Normal file
78
test/app/app/server.js
Normal 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}`);
|
||||||
|
});
|
||||||
@@ -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,
|
// `shell` is an array of all the files generated by webpack,
|
||||||
// `assets` is an array of everything in the `assets` directory
|
// `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);
|
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 => {
|
self.addEventListener('install', event => {
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
caches
|
caches
|
||||||
@@ -32,6 +32,6 @@
|
|||||||
<!-- Sapper creates a <script> tag containing `templates/main.js`
|
<!-- Sapper creates a <script> tag containing `templates/main.js`
|
||||||
and anything else it needs to hydrate the app and
|
and anything else it needs to hydrate the app and
|
||||||
initialise the router -->
|
initialise the router -->
|
||||||
<script src='%sapper.main%'></script>
|
%sapper.scripts%
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
6
test/app/package-lock.json
generated
6
test/app/package-lock.json
generated
@@ -2722,9 +2722,9 @@
|
|||||||
"integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ="
|
"integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ="
|
||||||
},
|
},
|
||||||
"marked": {
|
"marked": {
|
||||||
"version": "0.3.7",
|
"version": "0.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/marked/-/marked-0.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/marked/-/marked-0.3.9.tgz",
|
||||||
"integrity": "sha512-zBEP4qO1YQp5aXHt8S5wTiOv9i2X74V/LQL0zhUNvVaklt6Ywa6lChxIvS+ibYlCGgADwKwZFhjC3+XfpsvQvQ=="
|
"integrity": "sha512-nW5u0dxpXxHfkHzzrveY45gCbi+R4PaO4WRZYqZNl+vB0hVGeqlFn0aOg1c8AKL63TrNFn9Bm2UP4AdiZ9TPLw=="
|
||||||
},
|
},
|
||||||
"math-expression-evaluator": {
|
"math-expression-evaluator": {
|
||||||
"version": "1.2.17",
|
"version": "1.2.17",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"express": "^4.16.2",
|
"express": "^4.16.2",
|
||||||
"extract-text-webpack-plugin": "^3.0.2",
|
"extract-text-webpack-plugin": "^3.0.2",
|
||||||
"glob": "^7.1.2",
|
"glob": "^7.1.2",
|
||||||
"marked": "^0.3.7",
|
"marked": "^0.3.9",
|
||||||
"node-fetch": "^1.7.3",
|
"node-fetch": "^1.7.3",
|
||||||
"npm-run-all": "^4.1.2",
|
"npm-run-all": "^4.1.2",
|
||||||
"serve-static": "^1.13.1",
|
"serve-static": "^1.13.1",
|
||||||
|
|||||||
18
test/app/routes/4xx.html
Normal file
18
test/app/routes/4xx.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<:Head>
|
||||||
|
<title>{{status}}</title>
|
||||||
|
</:Head>
|
||||||
|
|
||||||
|
<Layout page='home'>
|
||||||
|
<h1>Not found</h1>
|
||||||
|
<p>{{error.message}}</p>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Layout from './_components/Layout.html';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Layout
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
18
test/app/routes/5xx.html
Normal file
18
test/app/routes/5xx.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<:Head>
|
||||||
|
<title>Internal server error</title>
|
||||||
|
</:Head>
|
||||||
|
|
||||||
|
<Layout page='home'>
|
||||||
|
<h1>Internal server error</h1>
|
||||||
|
<p>{{error.message}}</p>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Layout from './_components/Layout.html';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Layout
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -3,6 +3,9 @@
|
|||||||
<li><a href='/'>home</a></li>
|
<li><a href='/'>home</a></li>
|
||||||
<li><a href='/about'>about</a></li>
|
<li><a href='/about'>about</a></li>
|
||||||
<li><a href='/slow-preload'>slow preload</a></li>
|
<li><a href='/slow-preload'>slow preload</a></li>
|
||||||
|
<li><a href='/redirect-from'>redirect</a></li>
|
||||||
|
<li><a href='/blog/nope'>broken link</a></li>
|
||||||
|
<li><a href='/blog/throw-an-error'>error link</a></li>
|
||||||
<li><a rel=prefetch class='{{page === "blog" ? "selected" : ""}}' href='/blog'>blog</a></li>
|
<li><a rel=prefetch class='{{page === "blog" ? "selected" : ""}}' href='/blog'>blog</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
9
test/app/routes/api/delete/[id].js
Normal file
9
test/app/routes/api/delete/[id].js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export function del(req, res) {
|
||||||
|
res.set({
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
});
|
||||||
|
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
id: req.params.id
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -59,8 +59,21 @@
|
|||||||
// is called [slug].html
|
// is called [slug].html
|
||||||
const { slug } = params;
|
const { slug } = params;
|
||||||
|
|
||||||
return fetch(`/api/blog/${slug}`).then(r => r.json()).then(post => {
|
if (slug === 'throw-an-error') {
|
||||||
return { post };
|
return this.error(500, 'something went wrong');
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(`/api/blog/${slug}`).then(r => {
|
||||||
|
if (r.status === 200) {
|
||||||
|
return r.json().then(post => ({ post }));
|
||||||
|
this.error(r.status, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (r.status === 404) {
|
||||||
|
this.error(404, 'Not found');
|
||||||
|
} else {
|
||||||
|
throw new Error('Something went wrong');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
preload({ params, query }) {
|
preload({ params, query }) {
|
||||||
return fetch(`/api/blog`).then(r => r.json()).then(posts => {
|
return fetch(`/api/blog/contents`).then(r => r.json()).then(posts => {
|
||||||
return { posts };
|
return { posts };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
15
test/app/routes/delete-test.html
Normal file
15
test/app/routes/delete-test.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<button class='del' on:click='del()'>delete</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
del() {
|
||||||
|
fetch(`/api/delete/42`, { method: 'DELETE' })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
window.deleted = data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -6,13 +6,15 @@
|
|||||||
<h1>Great success!</h1>
|
<h1>Great success!</h1>
|
||||||
|
|
||||||
<figure>
|
<figure>
|
||||||
<img src='/great-success.png'>
|
<img alt='borat' src='/great-success.png'>
|
||||||
<figcaption>HIGH FIVE!</figcaption>
|
<figcaption>HIGH FIVE!</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<p><strong>Try editing this file (routes/index.html) to test hot module reloading.</strong></p>
|
<p><strong>Try editing this file (routes/index.html) to test hot module reloading.</strong></p>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
<div class='hydrate-test'></div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
h1, figure, p {
|
h1, figure, p {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
7
test/app/routes/redirect-from.html
Normal file
7
test/app/routes/redirect-from.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
preload() {
|
||||||
|
this.redirect(301, '/redirect-to');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
1
test/app/routes/redirect-to.html
Normal file
1
test/app/routes/redirect-to.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<h1>redirected</h1>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
preload({ url }) {
|
preload({ url }) {
|
||||||
return { url };
|
if (url) return { url };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,7 +4,11 @@
|
|||||||
export default {
|
export default {
|
||||||
preload() {
|
preload() {
|
||||||
return new Promise(fulfil => {
|
return new Promise(fulfil => {
|
||||||
window.fulfil = fulfil;
|
if (typeof window !== 'undefined') {
|
||||||
|
window.fulfil = fulfil;
|
||||||
|
} else {
|
||||||
|
fulfil({});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}`);
|
|
||||||
});
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { init } from '../../../runtime.js';
|
|
||||||
|
|
||||||
// `routes` is an array of route objects injected by Sapper
|
|
||||||
init(document.querySelector('#sapper'), __routes__);
|
|
||||||
|
|
||||||
window.READY = true;
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
const config = require('../../webpack/config.js');
|
const config = require('../../../webpack/config.js');
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: config.client.entry(),
|
entry: './app/client.js',
|
||||||
output: config.client.output(),
|
output: config.client.output(),
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.js', '.html']
|
extensions: ['.js', '.html']
|
||||||
@@ -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 = {
|
module.exports = {
|
||||||
entry: config.server.entry(),
|
entry: {
|
||||||
|
'server': './app/server.js'
|
||||||
|
},
|
||||||
output: config.server.output(),
|
output: config.server.output(),
|
||||||
target: 'node',
|
target: 'node',
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.js', '.html']
|
extensions: ['.js', '.html']
|
||||||
},
|
},
|
||||||
|
externals: Object.keys(pkg.dependencies).concat(Object.keys(sapper_pkg.dependencies)),
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
17
test/app/webpack/service-worker.config.js
Normal file
17
test/app/webpack/service-worker.config.js
Normal 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)
|
||||||
|
};
|
||||||
@@ -1,325 +1,485 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
|
const Nightmare = require('nightmare');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const serve = require('serve-static');
|
const serve = require('serve-static');
|
||||||
const Nightmare = require('nightmare');
|
const walkSync = require('walk-sync');
|
||||||
const getPort = require('get-port');
|
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
run('production');
|
run('production');
|
||||||
run('development');
|
run('development');
|
||||||
|
|
||||||
|
Nightmare.action('page', {
|
||||||
|
title(done) {
|
||||||
|
this.evaluate_now(() => document.querySelector('h1').textContent, done);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Nightmare.action('init', function(done) {
|
||||||
|
this.evaluate_now(() => window.init(), done);
|
||||||
|
});
|
||||||
|
|
||||||
function run(env) {
|
function run(env) {
|
||||||
describe(`env=${env}`, function () {
|
describe(`env=${env}`, function () {
|
||||||
this.timeout(20000);
|
this.timeout(20000);
|
||||||
|
|
||||||
let PORT;
|
let PORT;
|
||||||
let server;
|
let proc;
|
||||||
let nightmare;
|
let nightmare;
|
||||||
let middleware;
|
|
||||||
let capture;
|
let capture;
|
||||||
|
|
||||||
let base;
|
let base;
|
||||||
|
|
||||||
function get(url) {
|
before(() => {
|
||||||
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.chdir(path.resolve(__dirname, '../app'));
|
||||||
|
|
||||||
process.env.NODE_ENV = env;
|
let exec_promise = Promise.resolve();
|
||||||
|
|
||||||
if (env === 'production') {
|
if (env === 'production') {
|
||||||
const cli = path.resolve(__dirname, '../../cli/index.js');
|
const cli = path.resolve(__dirname, '../../cli.js');
|
||||||
await exec(`${cli} build`);
|
exec_promise = exec(`node ${cli} export`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolved = require.resolve('../..');
|
return exec_promise.then(() => {
|
||||||
delete require.cache[resolved];
|
const resolved = require.resolve('../../middleware.js');
|
||||||
const sapper = require(resolved);
|
delete require.cache[resolved];
|
||||||
|
delete require.cache[require.resolve('../../core.js')]; // TODO remove this
|
||||||
|
|
||||||
PORT = await getPort();
|
return require('get-port')();
|
||||||
base = `http://localhost:${PORT}`;
|
}).then(port => {
|
||||||
|
base = `http://localhost:${port}`;
|
||||||
|
|
||||||
global.fetch = (url, opts) => {
|
proc = require('child_process').fork('.sapper/server.js', {
|
||||||
if (url[0] === '/') url = `${base}${url}`;
|
cwd: process.cwd(),
|
||||||
return fetch(url, opts);
|
env: {
|
||||||
};
|
NODE_ENV: env,
|
||||||
|
PORT: port
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let handler;
|
||||||
|
|
||||||
|
proc.on('message', message => {
|
||||||
|
if (message.__sapper__) return;
|
||||||
|
if (handler) handler(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
capture = fn => {
|
||||||
|
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'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
after(() => {
|
after(() => {
|
||||||
server.close();
|
proc.kill();
|
||||||
middleware.close();
|
|
||||||
|
|
||||||
// give a chance to clean up
|
// give a chance to clean up
|
||||||
return new Promise(fulfil => setTimeout(fulfil, 500));
|
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', () => {
|
describe('basic functionality', () => {
|
||||||
beforeEach(() => {
|
it('serves /', () => {
|
||||||
nightmare = new Nightmare();
|
return nightmare.goto(base).page.title().then(title => {
|
||||||
|
assert.equal(title, 'Great success!');
|
||||||
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 () => {
|
it('serves /?', () => {
|
||||||
await nightmare.end();
|
return nightmare.goto(`${base}?`).page.title().then(title => {
|
||||||
|
assert.equal(title, 'Great success!');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('serves /', async () => {
|
it('serves static route', () => {
|
||||||
const title = await nightmare
|
return nightmare.goto(`${base}/about`).page.title().then(title => {
|
||||||
.goto(base)
|
assert.equal(title, 'About this site');
|
||||||
.evaluate(() => document.querySelector('h1').textContent);
|
});
|
||||||
|
|
||||||
assert.equal(title, 'Great success!');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('serves static route', async () => {
|
it('serves dynamic route', () => {
|
||||||
const title = await nightmare
|
return nightmare.goto(`${base}/blog/what-is-sapper`).page.title().then(title => {
|
||||||
|
assert.equal(title, 'What is Sapper?');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('navigates to a new page without reloading', () => {
|
||||||
|
return capture(() => nightmare.goto(base).init().wait(400))
|
||||||
|
.then(() => {
|
||||||
|
return capture(() => nightmare.click('a[href="/about"]'));
|
||||||
|
})
|
||||||
|
.then(requests => {
|
||||||
|
assert.deepEqual(requests.map(r => r.url), []);
|
||||||
|
return nightmare.path();
|
||||||
|
})
|
||||||
|
.then(path => {
|
||||||
|
assert.equal(path, '/about');
|
||||||
|
return nightmare.title();
|
||||||
|
})
|
||||||
|
.then(title => {
|
||||||
|
assert.equal(title, 'About');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('navigates programmatically', () => {
|
||||||
|
return nightmare
|
||||||
.goto(`${base}/about`)
|
.goto(`${base}/about`)
|
||||||
.evaluate(() => document.querySelector('h1').textContent);
|
.init()
|
||||||
|
|
||||||
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(200);
|
|
||||||
|
|
||||||
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('.goto')
|
.click('.goto')
|
||||||
.wait(() => window.location.pathname === '/blog/what-is-sapper')
|
.wait(() => window.location.pathname === '/blog/what-is-sapper')
|
||||||
.wait(100);
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
await nightmare.evaluate(() => document.title),
|
|
||||||
'What is Sapper?'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('prefetches programmatically', async () => {
|
|
||||||
await nightmare
|
|
||||||
.goto(`${base}/about`)
|
|
||||||
.wait(() => window.READY);
|
|
||||||
|
|
||||||
const requests = await capture(async () => {
|
|
||||||
return await nightmare
|
|
||||||
.click('.prefetch')
|
|
||||||
.wait(100);
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.ok(!!requests.find(r => r.url === '/api/blog/why-the-name'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('scrolls to active deeplink', async () => {
|
|
||||||
const scrollY = await nightmare
|
|
||||||
.goto(`${base}/blog/a-very-long-post#four`)
|
|
||||||
.wait(() => window.READY)
|
|
||||||
.wait(100)
|
.wait(100)
|
||||||
.evaluate(() => window.scrollY);
|
.title()
|
||||||
|
.then(title => {
|
||||||
assert.ok(scrollY > 0, scrollY);
|
assert.equal(title, 'What is Sapper?');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reuses prefetch promise', async () => {
|
it('prefetches programmatically', () => {
|
||||||
await nightmare
|
return nightmare
|
||||||
|
.goto(`${base}/about`)
|
||||||
|
.init()
|
||||||
|
.wait(200)
|
||||||
|
.then(() => {
|
||||||
|
return capture(() => {
|
||||||
|
return nightmare
|
||||||
|
.click('.prefetch')
|
||||||
|
.wait(200);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(requests => {
|
||||||
|
assert.ok(!!requests.find(r => r.url === '/api/blog/why-the-name'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scrolls to active deeplink', () => {
|
||||||
|
return nightmare
|
||||||
|
.goto(`${base}/blog/a-very-long-post#four`)
|
||||||
|
.init()
|
||||||
|
.evaluate(() => window.scrollY)
|
||||||
|
.then(scrollY => {
|
||||||
|
assert.ok(scrollY > 0, scrollY);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reuses prefetch promise', () => {
|
||||||
|
return nightmare
|
||||||
.goto(`${base}/blog`)
|
.goto(`${base}/blog`)
|
||||||
.wait(() => window.READY)
|
.init().wait(300)
|
||||||
.wait(200);
|
.then(() => {
|
||||||
|
return capture(() => {
|
||||||
|
return nightmare
|
||||||
|
.mouseover('[href="/blog/what-is-sapper"]')
|
||||||
|
.wait(200);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(mouseover_requests => {
|
||||||
|
assert.ok(mouseover_requests.findIndex(r => r.url === '/api/blog/what-is-sapper') !== -1);
|
||||||
|
|
||||||
const mouseover_requests = (await capture(async () => {
|
return capture(() => {
|
||||||
await nightmare
|
return nightmare
|
||||||
.mouseover('[href="/blog/what-is-sapper"]')
|
.click('[href="/blog/what-is-sapper"]')
|
||||||
.wait(200);
|
.wait(200);
|
||||||
})).map(r => r.url);
|
});
|
||||||
|
})
|
||||||
assert.deepEqual(mouseover_requests, [
|
.then(click_requests => {
|
||||||
'/api/blog/what-is-sapper'
|
assert.ok(click_requests.findIndex(r => r.url === '/api/blog/what-is-sapper') === -1);
|
||||||
]);
|
});
|
||||||
|
|
||||||
const click_requests = (await capture(async () => {
|
|
||||||
await nightmare
|
|
||||||
.click('[href="/blog/what-is-sapper"]')
|
|
||||||
.wait(200);
|
|
||||||
})).map(r => r.url);
|
|
||||||
|
|
||||||
assert.deepEqual(click_requests, []);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cancels navigation if subsequent navigation occurs during preload', async () => {
|
it('cancels navigation if subsequent navigation occurs during preload', () => {
|
||||||
await nightmare
|
return nightmare
|
||||||
.goto(base)
|
.goto(base)
|
||||||
.wait(() => window.READY)
|
.init()
|
||||||
.click('a[href="/slow-preload"]')
|
.click('a[href="/slow-preload"]')
|
||||||
.wait(100)
|
.wait(100)
|
||||||
.click('a[href="/about"]')
|
.click('a[href="/about"]')
|
||||||
.wait(100);
|
.wait(100)
|
||||||
|
.then(() => nightmare.path())
|
||||||
assert.equal(
|
.then(path => {
|
||||||
await nightmare.path(),
|
assert.equal(path, '/about');
|
||||||
'/about'
|
return nightmare.title();
|
||||||
);
|
})
|
||||||
|
.then(title => {
|
||||||
assert.equal(
|
assert.equal(title, 'About');
|
||||||
await nightmare.evaluate(() => document.querySelector('h1').textContent),
|
return nightmare.evaluate(() => window.fulfil({})).wait(100);
|
||||||
'About this site'
|
})
|
||||||
);
|
.then(() => nightmare.path())
|
||||||
|
.then(path => {
|
||||||
await nightmare
|
assert.equal(path, '/about');
|
||||||
.evaluate(() => window.fulfil({}))
|
return nightmare.title();
|
||||||
.wait(100);
|
})
|
||||||
|
.then(title => {
|
||||||
assert.equal(
|
assert.equal(title, 'About');
|
||||||
await nightmare.path(),
|
});
|
||||||
'/about'
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
await nightmare.evaluate(() => document.querySelector('h1').textContent),
|
|
||||||
'About this site'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes entire request object to preload', async () => {
|
it('passes entire request object to preload', () => {
|
||||||
const html = await nightmare
|
return nightmare
|
||||||
.goto(`${base}/show-url`)
|
.goto(`${base}/show-url`)
|
||||||
.evaluate(() => document.querySelector('p').innerHTML);
|
.init()
|
||||||
|
.evaluate(() => document.querySelector('p').innerHTML)
|
||||||
|
.end().then(html => {
|
||||||
|
assert.equal(html, `URL is /show-url`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
assert.equal(html, `URL is /show-url`);
|
it('calls a delete handler', () => {
|
||||||
|
return nightmare
|
||||||
|
.goto(`${base}/delete-test`)
|
||||||
|
.init()
|
||||||
|
.click('.del')
|
||||||
|
.wait(() => window.deleted)
|
||||||
|
.evaluate(() => window.deleted.id)
|
||||||
|
.then(id => {
|
||||||
|
assert.equal(id, 42);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hydrates initial route', () => {
|
||||||
|
return nightmare.goto(base)
|
||||||
|
.wait('.hydrate-test')
|
||||||
|
.evaluate(() => {
|
||||||
|
window.el = document.querySelector('.hydrate-test');
|
||||||
|
})
|
||||||
|
.init()
|
||||||
|
.evaluate(() => {
|
||||||
|
return document.querySelector('.hydrate-test') === window.el;
|
||||||
|
})
|
||||||
|
.then(matches => {
|
||||||
|
assert.ok(matches);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects on server', () => {
|
||||||
|
return nightmare.goto(`${base}/redirect-from`)
|
||||||
|
.path()
|
||||||
|
.then(path => {
|
||||||
|
assert.equal(path, '/redirect-to');
|
||||||
|
})
|
||||||
|
.then(() => nightmare.page.title())
|
||||||
|
.then(title => {
|
||||||
|
assert.equal(title, 'redirected');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects in client', () => {
|
||||||
|
return nightmare.goto(base)
|
||||||
|
.wait('[href="/redirect-from"]')
|
||||||
|
.click('[href="/redirect-from"]')
|
||||||
|
.wait(200)
|
||||||
|
.path()
|
||||||
|
.then(path => {
|
||||||
|
assert.equal(path, '/redirect-to');
|
||||||
|
})
|
||||||
|
.then(() => nightmare.page.title())
|
||||||
|
.then(title => {
|
||||||
|
assert.equal(title, 'redirected');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles 4xx error on server', () => {
|
||||||
|
return nightmare.goto(`${base}/blog/nope`)
|
||||||
|
.path()
|
||||||
|
.then(path => {
|
||||||
|
assert.equal(path, '/blog/nope');
|
||||||
|
})
|
||||||
|
.then(() => nightmare.page.title())
|
||||||
|
.then(title => {
|
||||||
|
assert.equal(title, 'Not found')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles 4xx error in client', () => {
|
||||||
|
return nightmare.goto(base)
|
||||||
|
.init()
|
||||||
|
.click('[href="/blog/nope"]')
|
||||||
|
.wait(200)
|
||||||
|
.path()
|
||||||
|
.then(path => {
|
||||||
|
assert.equal(path, '/blog/nope');
|
||||||
|
})
|
||||||
|
.then(() => nightmare.page.title())
|
||||||
|
.then(title => {
|
||||||
|
assert.equal(title, 'Not found');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles non-4xx error on server', () => {
|
||||||
|
return nightmare.goto(`${base}/blog/throw-an-error`)
|
||||||
|
.path()
|
||||||
|
.then(path => {
|
||||||
|
assert.equal(path, '/blog/throw-an-error');
|
||||||
|
})
|
||||||
|
.then(() => nightmare.page.title())
|
||||||
|
.then(title => {
|
||||||
|
assert.equal(title, 'Internal server error')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles non-4xx error in client', () => {
|
||||||
|
return nightmare.goto(base)
|
||||||
|
.init()
|
||||||
|
.click('[href="/blog/throw-an-error"]')
|
||||||
|
.wait(200)
|
||||||
|
.path()
|
||||||
|
.then(path => {
|
||||||
|
assert.equal(path, '/blog/throw-an-error');
|
||||||
|
})
|
||||||
|
.then(() => nightmare.page.title())
|
||||||
|
.then(title => {
|
||||||
|
assert.equal(title, 'Internal server error');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('headers', () => {
|
describe('headers', () => {
|
||||||
it('sets Content-Type and Link...preload headers', async () => {
|
it('sets Content-Type and Link...preload headers', () => {
|
||||||
const { headers } = await get('/');
|
return capture(() => nightmare.goto(base).end()).then(requests => {
|
||||||
|
const { headers } = requests[0];
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
headers['Content-Type'],
|
headers['content-type'],
|
||||||
'text/html'
|
'text/html'
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
/<\/client\/main.\w+\.js>;rel="preload";as="script", <\/client\/_.\d+.\w+.js>;rel="preload";as="script"/.test(headers['Link']),
|
/<\/client\/[^/]+\/main\.js>;rel="preload";as="script", <\/client\/[^/]+\/_\.\d+\.js>;rel="preload";as="script"/.test(headers['link']),
|
||||||
headers['Link']
|
headers['link']
|
||||||
);
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (env === 'production') {
|
||||||
|
describe('export', () => {
|
||||||
|
it('export all pages', () => {
|
||||||
|
const dest = path.resolve(__dirname, '../app/dist');
|
||||||
|
|
||||||
|
// Pages that should show up in the extraction directory.
|
||||||
|
const expectedPages = [
|
||||||
|
'index.html',
|
||||||
|
'about/index.html',
|
||||||
|
'slow-preload/index.html',
|
||||||
|
|
||||||
|
'blog/index.html',
|
||||||
|
'blog/a-very-long-post/index.html',
|
||||||
|
'blog/how-can-i-get-involved/index.html',
|
||||||
|
'blog/how-is-sapper-different-from-next/index.html',
|
||||||
|
'blog/how-to-use-sapper/index.html',
|
||||||
|
'blog/what-is-sapper/index.html',
|
||||||
|
'blog/why-the-name/index.html',
|
||||||
|
|
||||||
|
'api/blog/contents',
|
||||||
|
'api/blog/a-very-long-post',
|
||||||
|
'api/blog/how-can-i-get-involved',
|
||||||
|
'api/blog/how-is-sapper-different-from-next',
|
||||||
|
'api/blog/how-to-use-sapper',
|
||||||
|
'api/blog/what-is-sapper',
|
||||||
|
'api/blog/why-the-name',
|
||||||
|
|
||||||
|
'favicon.png',
|
||||||
|
'global.css',
|
||||||
|
'great-success.png',
|
||||||
|
'manifest.json',
|
||||||
|
'service-worker.js',
|
||||||
|
'svelte-logo-192.png',
|
||||||
|
'svelte-logo-512.png',
|
||||||
|
];
|
||||||
|
// Client scripts that should show up in the extraction directory.
|
||||||
|
const expectedClientRegexes = [
|
||||||
|
/client\/[^/]+\/_(\.\d+)?\.js/,
|
||||||
|
/client\/[^/]+\/about(\.\d+)?\.js/,
|
||||||
|
/client\/[^/]+\/blog_\$slug\$(\.\d+)?\.js/,
|
||||||
|
/client\/[^/]+\/blog(\.\d+)?\.js/,
|
||||||
|
/client\/[^/]+\/main(\.\d+)?\.js/,
|
||||||
|
/client\/[^/]+\/show_url(\.\d+)?\.js/,
|
||||||
|
/client\/[^/]+\/slow_preload(\.\d+)?\.js/,
|
||||||
|
];
|
||||||
|
const allPages = walkSync(dest);
|
||||||
|
|
||||||
|
expectedPages.forEach((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.
|
||||||
|
let matched = false;
|
||||||
|
for (const page of allPages) {
|
||||||
|
if (expectedRegex.test(page)) {
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.ok(matched, `Could not find client page matching ${expectedRegex}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function exec(cmd) {
|
function exec(cmd) {
|
||||||
return new Promise((fulfil, reject) => {
|
return new Promise((fulfil, reject) => {
|
||||||
require('child_process').exec(cmd, (err, stdout, stderr) => {
|
const parts = cmd.split(' ');
|
||||||
if (err) {
|
const proc = require('child_process').spawn(parts.shift(), parts);
|
||||||
process.stdout.write(stdout);
|
|
||||||
process.stderr.write(stderr);
|
|
||||||
|
|
||||||
return reject(err);
|
proc.stdout.on('data', data => {
|
||||||
}
|
process.stdout.write(data);
|
||||||
|
|
||||||
fulfil();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
proc.stderr.on('data', data => {
|
||||||
|
process.stderr.write(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on('error', reject);
|
||||||
|
|
||||||
|
proc.on('close', () => fulfil());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
const path = require('path');
|
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
|
const { create_routes } = require('../../core.js');
|
||||||
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/foo.html', 'post/[id].html', 'post/bar.html']);
|
const routes = create_routes({
|
||||||
|
files: ['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),
|
||||||
@@ -21,7 +21,9 @@ describe('create_routes', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('generates params', () => {
|
it('generates params', () => {
|
||||||
const routes = create_routes(['index.html', 'about.html', '[wildcard].html', 'post/[id].html']);
|
const routes = create_routes({
|
||||||
|
files: ['index.html', 'about.html', '[wildcard].html', 'post/[id].html']
|
||||||
|
});
|
||||||
|
|
||||||
let file;
|
let file;
|
||||||
let params;
|
let params;
|
||||||
@@ -40,7 +42,9 @@ describe('create_routes', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('ignores files and directories with leading underscores', () => {
|
it('ignores files and directories with leading underscores', () => {
|
||||||
const routes = create_routes(['index.html', '_foo.html', 'a/_b/c/d.html', 'e/f/g/h.html', 'i/_j.html']);
|
const routes = create_routes({
|
||||||
|
files: ['index.html', '_foo.html', 'a/_b/c/d.html', 'e/f/g/h.html', 'i/_j.html']
|
||||||
|
});
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
routes.map(r => r.file),
|
routes.map(r => r.file),
|
||||||
@@ -52,8 +56,12 @@ describe('create_routes', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('matches /foo/:bar before /:baz/qux', () => {
|
it('matches /foo/:bar before /:baz/qux', () => {
|
||||||
const a = create_routes(['foo/[bar].html', '[baz]/qux.html']);
|
const a = create_routes({
|
||||||
const b = create_routes(['[baz]/qux.html', 'foo/[bar].html']);
|
files: ['foo/[bar].html', '[baz]/qux.html']
|
||||||
|
});
|
||||||
|
const b = create_routes({
|
||||||
|
files: ['[baz]/qux.html', 'foo/[bar].html']
|
||||||
|
});
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
a.map(r => r.file),
|
a.map(r => r.file),
|
||||||
@@ -68,12 +76,22 @@ describe('create_routes', () => {
|
|||||||
|
|
||||||
it('fails if routes are indistinguishable', () => {
|
it('fails if routes are indistinguishable', () => {
|
||||||
assert.throws(() => {
|
assert.throws(() => {
|
||||||
create_routes(['[foo].html', '[bar]/index.html']);
|
create_routes({
|
||||||
|
files: ['[foo].html', '[bar]/index.html']
|
||||||
|
});
|
||||||
}, /The \[foo\].html and \[bar\]\/index.html routes clash/);
|
}, /The \[foo\].html and \[bar\]\/index.html routes clash/);
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
create_routes({
|
||||||
|
files: ['foo.html', 'foo.js']
|
||||||
|
});
|
||||||
|
}, /The foo.html and foo.js routes clash/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('matches nested routes', () => {
|
it('matches nested routes', () => {
|
||||||
const route = create_routes(['settings/[submenu].html'])[0];
|
const route = create_routes({
|
||||||
|
files: ['settings/[submenu].html']
|
||||||
|
})[0];
|
||||||
|
|
||||||
assert.deepEqual(route.exec('/settings/foo'), {
|
assert.deepEqual(route.exec('/settings/foo'), {
|
||||||
submenu: 'foo'
|
submenu: 'foo'
|
||||||
@@ -85,7 +103,9 @@ describe('create_routes', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('prefers index routes to nested routes', () => {
|
it('prefers index routes to nested routes', () => {
|
||||||
const routes = create_routes(['settings/[submenu].html', 'settings.html']);
|
const routes = create_routes({
|
||||||
|
files: ['settings/[submenu].html', 'settings.html']
|
||||||
|
});
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
routes.map(r => r.file),
|
routes.map(r => r.file),
|
||||||
@@ -94,7 +114,9 @@ describe('create_routes', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('matches deeply nested routes', () => {
|
it('matches deeply nested routes', () => {
|
||||||
const route = create_routes(['settings/[a]/[b]/index.html'])[0];
|
const route = create_routes({
|
||||||
|
files: ['settings/[a]/[b]/index.html']
|
||||||
|
})[0];
|
||||||
|
|
||||||
assert.deepEqual(route.exec('/settings/foo/bar'), {
|
assert.deepEqual(route.exec('/settings/foo/bar'), {
|
||||||
a: 'foo',
|
a: 'foo',
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
const { dest, dev, entry } = require('../lib/config.js');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
dev,
|
|
||||||
|
|
||||||
client: {
|
|
||||||
entry: () => {
|
|
||||||
return {
|
|
||||||
main: entry.client
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
output: () => {
|
|
||||||
return {
|
|
||||||
path: `${dest}/client`,
|
|
||||||
filename: '[name].[hash].js',
|
|
||||||
chunkFilename: '[name].[id].[hash].js',
|
|
||||||
publicPath: '/client/'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
server: {
|
|
||||||
entry: () => {
|
|
||||||
return {
|
|
||||||
main: entry.server
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
output: () => {
|
|
||||||
return {
|
|
||||||
path: `${dest}/server`,
|
|
||||||
filename: '[name].[hash].js',
|
|
||||||
chunkFilename: '[name].[id].[hash].js',
|
|
||||||
libraryTarget: 'commonjs2'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
import 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000';
|
|
||||||
Reference in New Issue
Block a user