mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-13 11:35:28 +00:00
Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff1e632057 | ||
|
|
aeeb231477 | ||
|
|
d1940db8c0 | ||
|
|
98f9a64b64 | ||
|
|
b9bef802d3 | ||
|
|
a7024b3806 | ||
|
|
423e02aeae | ||
|
|
12b73ecebf | ||
|
|
e1bc38b5a7 | ||
|
|
b66f624f01 | ||
|
|
502dd547d1 | ||
|
|
4c343490d2 | ||
|
|
b3027c5816 | ||
|
|
c29e8022cc | ||
|
|
e4cd4c9cb0 | ||
|
|
feddad42b2 | ||
|
|
3c4ebcda30 | ||
|
|
75aedf4663 | ||
|
|
c8366dec74 | ||
|
|
9a936669c6 | ||
|
|
0226bd90c6 | ||
|
|
e1926e1bcb | ||
|
|
db1c1f332a | ||
|
|
e8d510b261 | ||
|
|
f8e237b265 | ||
|
|
68c2f2e388 | ||
|
|
0bcb61650b | ||
|
|
43a12a8331 | ||
|
|
f0feab5738 | ||
|
|
e9203b4d71 | ||
|
|
8e79e706e6 | ||
|
|
4b495f44fd | ||
|
|
222a750b7b | ||
|
|
5b214c964c | ||
|
|
95f99fd378 | ||
|
|
1bed4b0670 | ||
|
|
9d4890913a | ||
|
|
f50d3c4262 | ||
|
|
8925e541d5 | ||
|
|
a48afb77d3 | ||
|
|
45e845ee92 | ||
|
|
492f024d2a | ||
|
|
8d40992cf1 | ||
|
|
4232f75b19 | ||
|
|
fefb0d96d7 | ||
|
|
cd91bf2ca4 | ||
|
|
7466e8da82 | ||
|
|
463307db86 | ||
|
|
2a68394dce | ||
|
|
c8fe0679ae | ||
|
|
51d45cf38f | ||
|
|
92fbc28e11 | ||
|
|
c1b1b3ed63 | ||
|
|
cd8b9ddb14 | ||
|
|
924855d248 | ||
|
|
8b516ef9bd | ||
|
|
8604088f3d | ||
|
|
8da3ca16ab | ||
|
|
d0dd1d6cc9 | ||
|
|
8270463281 | ||
|
|
514331b5e3 | ||
|
|
63d39575be | ||
|
|
3af5503009 | ||
|
|
c1de442dd1 | ||
|
|
0c6b7e3836 | ||
|
|
dc5e2543cb | ||
|
|
ecc7b80d91 | ||
|
|
40024e7d86 | ||
|
|
6f71f7ad4d | ||
|
|
6eb99b195e | ||
|
|
9e08fee9a1 | ||
|
|
442ce366e2 | ||
|
|
dc929fcd83 | ||
|
|
2dc246398b | ||
|
|
b7ac067459 | ||
|
|
8b50ff34b8 | ||
|
|
62abdb2a87 | ||
|
|
34d0bae4a1 | ||
|
|
4f0b336627 | ||
|
|
e71bf298fb | ||
|
|
e4936375db | ||
|
|
08ff7ad234 | ||
|
|
5995b7ae6a | ||
|
|
71ed3864b7 | ||
|
|
bd7f6e2b1a | ||
|
|
dd1f2d79ff | ||
|
|
dccd3cdeb0 | ||
|
|
b3b5d9f352 | ||
|
|
10ddaeb7a3 | ||
|
|
060f9b2f5e | ||
|
|
32dfa94247 | ||
|
|
797cc3cde1 | ||
|
|
9eca90067c | ||
|
|
57f293e872 | ||
|
|
7e65c481d8 | ||
|
|
0fe93cd177 | ||
|
|
67fe570f6d | ||
|
|
a3d44aba31 | ||
|
|
80ae909b73 | ||
|
|
892b18cf80 | ||
|
|
0eb96bf01f | ||
|
|
419f5c5235 | ||
|
|
4c61ed5fdd |
38
.eslintrc.json
Normal file
38
.eslintrc.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,2 +1,5 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
node_modules
|
||||
cypress/screenshots
|
||||
test/app/.sapper
|
||||
runtime.js
|
||||
21
.travis.yml
Normal file
21
.travis.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
sudo: false
|
||||
|
||||
language: node_js
|
||||
|
||||
node_js:
|
||||
- "stable"
|
||||
|
||||
env:
|
||||
global:
|
||||
- BUILD_TIMEOUT=10000
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- xvfb
|
||||
|
||||
install:
|
||||
- export DISPLAY=':99.0'
|
||||
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||
- npm install
|
||||
- (cd test/app && npm install)
|
||||
77
CHANGELOG.md
Normal file
77
CHANGELOG.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# sapper changelog
|
||||
|
||||
## 0.3.0
|
||||
|
||||
* Move `sapper/runtime/app.js` to `sapper/runtime.js`
|
||||
* Cancel navigation if overtaken by second navigation ([#48](https://github.com/sveltejs/sapper/issues/48))
|
||||
* Store preloaded data, to avoiding double prefetching ([#49](https://github.com/sveltejs/sapper/issues/49))
|
||||
* Pass server request object to `preload` ([#54](https://github.com/sveltejs/sapper/pull/54))
|
||||
* Nested routes ([#55](https://github.com/sveltejs/sapper/issues/55))
|
||||
|
||||
## 0.2.10
|
||||
|
||||
* Handle deep links correctly ([#44](https://github.com/sveltejs/sapper/issues/44))
|
||||
|
||||
## 0.2.9
|
||||
|
||||
* Don't write files to disk in prod mode
|
||||
|
||||
## 0.2.8
|
||||
|
||||
* Add `goto` function ([#29](https://github.com/sveltejs/sapper/issues/29))
|
||||
* Don't use `/tmp` as destination in Now environments
|
||||
|
||||
## 0.2.7
|
||||
|
||||
* Fix streaming bug
|
||||
|
||||
## 0.2.6
|
||||
|
||||
* Render main.js back to templates, to allow relative imports ([#40](https://github.com/sveltejs/sapper/issues/40))
|
||||
|
||||
## 0.2.5
|
||||
|
||||
* Fix nested routes on Windows ([#39](https://github.com/sveltejs/sapper/pull/39))
|
||||
* Rebundle when routes and main.js change ([#34](https://github.com/sveltejs/sapper/pull/34))
|
||||
* Add `Link...preload` headers for JavaScript assets ([#2](https://github.com/sveltejs/sapper/issues/2))
|
||||
* Stream document up to first dynamic content ([#19](https://github.com/sveltejs/sapper/issues/19))
|
||||
* Error if routes clash ([#33](https://github.com/sveltejs/sapper/issues/33))
|
||||
|
||||
## 0.2.4
|
||||
|
||||
* Posixify path to HMR client
|
||||
|
||||
## 0.2.3
|
||||
|
||||
* Posixify import paths, even on Windows ([#31](https://github.com/sveltejs/sapper/pull/31))
|
||||
* Pass `url` to 404 handler
|
||||
|
||||
## 0.2.2
|
||||
|
||||
* Create destination directory when building, don't assume it's already there from dev mode
|
||||
* We have tests now!
|
||||
|
||||
## 0.2.1
|
||||
|
||||
* Inject HMR logic in dev mode
|
||||
|
||||
## 0.2.0
|
||||
|
||||
* Separate `sapper build` from prod server ([#21](https://github.com/sveltejs/sapper/issues/21))
|
||||
|
||||
## 0.1.3-5
|
||||
|
||||
* Fix typo
|
||||
|
||||
## 0.1.2
|
||||
|
||||
* Use `atime.getTime()` and `mtime.getTime()` instead of `atimeMs` and `mtimeMs` ([#11](https://github.com/sveltejs/sapper/issues/11))
|
||||
* Make dest dir before anyone tries to write to it ([#18](https://github.com/sveltejs/sapper/pull/18))
|
||||
|
||||
## 0.1.1
|
||||
|
||||
* Expose resolved pathname to `sapper/runtime/app.js` as `__app__` inside main.js
|
||||
|
||||
## 0.1.0
|
||||
|
||||
* First public preview
|
||||
17
appveyor.yml
Normal file
17
appveyor.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
version: "{build}"
|
||||
|
||||
shallow_clone: true
|
||||
|
||||
init:
|
||||
- git config --global core.autocrlf false
|
||||
|
||||
build: off
|
||||
|
||||
environment:
|
||||
matrix:
|
||||
# node.js
|
||||
- nodejs_version: stable
|
||||
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version
|
||||
- npm install
|
||||
8
cli/index.js
Executable file
8
cli/index.js
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const cmd = process.argv[2];
|
||||
|
||||
if (cmd === 'build') {
|
||||
process.env.NODE_ENV = 'production';
|
||||
require('../lib/build.js')();
|
||||
}
|
||||
115
connect.js
115
connect.js
@@ -1,115 +0,0 @@
|
||||
require('svelte/ssr/register');
|
||||
const esm = require('@std/esm');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const glob = require('glob');
|
||||
const rimraf = require('rimraf');
|
||||
const mkdirp = require('mkdirp');
|
||||
const create_routes = require('./lib/utils/create_routes.js');
|
||||
const templates = require('./lib/templates.js');
|
||||
const create_app = require('./lib/utils/create_app.js');
|
||||
const create_webpack_compiler = require('./lib/utils/create_webpack_compiler.js');
|
||||
const escape_html = require('escape-html');
|
||||
const { src, dest, dev } = require('./lib/config.js');
|
||||
|
||||
const esmRequire = esm(module, {
|
||||
esm: 'js'
|
||||
});
|
||||
|
||||
module.exports = function connect(opts) {
|
||||
mkdirp(dest);
|
||||
rimraf.sync(path.join(dest, '**/*'));
|
||||
|
||||
let routes = create_routes(
|
||||
glob.sync('**/*.+(html|js|mjs)', { cwd: src })
|
||||
);
|
||||
|
||||
create_app(src, dest, routes, opts);
|
||||
|
||||
const webpack_compiler = create_webpack_compiler(
|
||||
dest,
|
||||
routes,
|
||||
dev
|
||||
);
|
||||
|
||||
return async function(req, res, next) {
|
||||
const url = req.url.replace(/\?.+/, '');
|
||||
|
||||
if (url === '/service-worker.js' || url === '/index.html' || url.startsWith('/client/')) {
|
||||
await webpack_compiler.ready;
|
||||
res.set({
|
||||
'Content-Type': url === '/index.html' ? 'text/html' : 'application/javascript'
|
||||
});
|
||||
fs.createReadStream(`${dest}${url}`).pipe(res);
|
||||
return;
|
||||
}
|
||||
|
||||
// whatever happens, we're going to serve some HTML
|
||||
res.set({
|
||||
'Content-Type': 'text/html'
|
||||
});
|
||||
|
||||
try {
|
||||
for (const route of routes) {
|
||||
if (route.test(url)) {
|
||||
await webpack_compiler.ready;
|
||||
|
||||
req.params = route.exec(url);
|
||||
|
||||
const chunk = webpack_compiler.chunks[route.id];
|
||||
const mod = require(path.resolve(dest, 'server', chunk));
|
||||
|
||||
if (route.type === 'page') {
|
||||
let data = { params: req.params, query: req.query };
|
||||
if (mod.default.preload) data = Object.assign(data, await mod.default.preload(data));
|
||||
|
||||
const { html, head, css } = mod.default.render(data);
|
||||
|
||||
const page = templates.render(200, {
|
||||
main: webpack_compiler.client_main,
|
||||
html,
|
||||
head: `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`,
|
||||
styles: (css && css.code ? `<style>${css.code}</style>` : '')
|
||||
});
|
||||
|
||||
res.status(200);
|
||||
res.end(page);
|
||||
}
|
||||
|
||||
else {
|
||||
const handler = mod[req.method.toLowerCase()];
|
||||
if (handler) {
|
||||
if (handler.length === 2) {
|
||||
handler(req, res);
|
||||
} else {
|
||||
const data = await handler(req);
|
||||
|
||||
// TODO headers, error handling
|
||||
if (typeof data === 'string') {
|
||||
res.end(data);
|
||||
} else {
|
||||
res.end(JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.status(404).end(templates.render(404, {
|
||||
title: 'Not found',
|
||||
status: 404,
|
||||
method: req.method,
|
||||
url
|
||||
}));
|
||||
} catch(err) {
|
||||
res.status(500).end(templates.render(500, {
|
||||
title: err.name || 'Internal server error',
|
||||
url,
|
||||
error: escape_html(err.details || err.message || err || 'Unknown error')
|
||||
}));
|
||||
}
|
||||
};
|
||||
};
|
||||
9
cypress.json
Normal file
9
cypress.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:3000",
|
||||
"videoRecording": false,
|
||||
"fixturesFolder": "test/cypress/fixtures",
|
||||
"integrationFolder": "test/cypress/integration",
|
||||
"pluginsFile": false,
|
||||
"screenshotsFolder": "test/cypress/screenshots",
|
||||
"supportFile": "test/cypress/support/index.js"
|
||||
}
|
||||
42
lib/build.js
Normal file
42
lib/build.js
Normal file
@@ -0,0 +1,42 @@
|
||||
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,4 +1,6 @@
|
||||
const path = require('path');
|
||||
const mkdirp = require('mkdirp');
|
||||
const rimraf = require('rimraf');
|
||||
|
||||
exports.dev = process.env.NODE_ENV !== 'production';
|
||||
|
||||
@@ -6,7 +8,14 @@ exports.templates = path.resolve(process.env.SAPPER_TEMPLATES || 'templates');
|
||||
|
||||
exports.src = path.resolve(process.env.SAPPER_ROUTES || 'routes');
|
||||
|
||||
exports.dest = path.resolve(
|
||||
process.env.NOW ? '/tmp' :
|
||||
process.env.SAPPER_DEST || '.sapper'
|
||||
);
|
||||
exports.dest = path.resolve(process.env.SAPPER_DEST || '.sapper');
|
||||
|
||||
if (exports.dev) {
|
||||
mkdirp.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
Normal file
238
lib/index.js
Normal file
@@ -0,0 +1,238 @@
|
||||
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,16 +1,31 @@
|
||||
const glob = require('glob');
|
||||
const chokidar = require('chokidar');
|
||||
const create_routes = require('./utils/create_routes.js');
|
||||
const { src } = require('./config.js');
|
||||
const { src, dev } = require('./config.js');
|
||||
|
||||
const route_manager = {
|
||||
routes: create_routes(
|
||||
glob.sync('**/*.+(html|js|mjs)', { cwd: src })
|
||||
),
|
||||
const callbacks = [];
|
||||
|
||||
onchange(fn) {
|
||||
// TODO in dev mode, keep this updated, and allow
|
||||
// webpack compiler etc to hook into it
|
||||
}
|
||||
exports.onchange = fn => {
|
||||
callbacks.push(fn);
|
||||
};
|
||||
|
||||
module.exports = route_manager;
|
||||
function update() {
|
||||
exports.routes = create_routes(
|
||||
glob.sync('**/*.+(html|js|mjs)', { cwd: src })
|
||||
);
|
||||
|
||||
callbacks.forEach(fn => fn());
|
||||
}
|
||||
|
||||
update();
|
||||
|
||||
if (dev) {
|
||||
const watcher = chokidar.watch(`${src}/**/*.+(html|js|mjs)`, {
|
||||
ignoreInitial: true,
|
||||
persistent: false
|
||||
});
|
||||
|
||||
watcher.on('add', update);
|
||||
watcher.on('change', update);
|
||||
watcher.on('unlink', update);
|
||||
}
|
||||
103
lib/templates.js
103
lib/templates.js
@@ -1,34 +1,79 @@
|
||||
const fs = require('fs');
|
||||
const glob = require('glob');
|
||||
const chokidar = require('chokidar');
|
||||
const { dev } = require('./config.js');
|
||||
|
||||
const templates = glob.sync('*.html', { cwd: 'templates' })
|
||||
.map(file => {
|
||||
const template = fs.readFileSync(`templates/${file}`, 'utf-8');
|
||||
const status = file.replace('.html', '').toLowerCase();
|
||||
let templates;
|
||||
|
||||
if (!/^[0-9x]{3}$/.test(status)) {
|
||||
throw new Error(`Bad template — should be a valid status code like 404.html, or a wildcard like 2xx.html`);
|
||||
}
|
||||
function create_templates() {
|
||||
templates = glob.sync('*.html', { cwd: 'templates' })
|
||||
.map(file => {
|
||||
const template = fs.readFileSync(`templates/${file}`, 'utf-8');
|
||||
const status = file.replace('.html', '').toLowerCase();
|
||||
|
||||
const specificity = (
|
||||
(status[0] === 'x' ? 0 : 4) +
|
||||
(status[1] === 'x' ? 0 : 2) +
|
||||
(status[2] === 'x' ? 0 : 1)
|
||||
);
|
||||
|
||||
const pattern = new RegExp(`^${status.split('').map(d => d === 'x' ? '\\d' : d).join('')}$`);
|
||||
|
||||
return {
|
||||
test: status => pattern.test(status),
|
||||
specificity,
|
||||
render(data) {
|
||||
return template.replace(/%sapper\.(\w+)%/g, (match, key) => {
|
||||
return key in data ? data[key] : '';
|
||||
});
|
||||
if (!/^[0-9x]{3}$/.test(status)) {
|
||||
throw new Error(`Bad template — should be a valid status code like 404.html, or a wildcard like 2xx.html`);
|
||||
}
|
||||
}
|
||||
})
|
||||
.sort((a, b) => b.specificity - a.specificity);
|
||||
|
||||
const specificity = (
|
||||
(status[0] === 'x' ? 0 : 4) +
|
||||
(status[1] === 'x' ? 0 : 2) +
|
||||
(status[2] === 'x' ? 0 : 1)
|
||||
);
|
||||
|
||||
const pattern = new RegExp(`^${status.split('').map(d => d === 'x' ? '\\d' : d).join('')}$`);
|
||||
|
||||
return {
|
||||
test: status => pattern.test(status),
|
||||
specificity,
|
||||
render: data => {
|
||||
return template.replace(/%sapper\.(\w+)%/g, (match, key) => {
|
||||
return key in data ? data[key] : '';
|
||||
});
|
||||
},
|
||||
stream: async (res, data) => {
|
||||
let i = 0;
|
||||
|
||||
do {
|
||||
const start = template.indexOf('%sapper', i);
|
||||
|
||||
if (start === -1) {
|
||||
res.end(template.slice(i));
|
||||
return;
|
||||
}
|
||||
|
||||
res.write(template.slice(i, start));
|
||||
|
||||
const end = template.indexOf('%', start + 1);
|
||||
if (end === -1) {
|
||||
throw new Error(`Bad template`); // TODO validate ahead of time
|
||||
}
|
||||
|
||||
const tag = template.slice(start + 1, end);
|
||||
const match = /sapper\.(\w+)/.exec(tag);
|
||||
if (!match || !(match[1] in data)) throw new Error(`Bad template`); // TODO ditto
|
||||
|
||||
res.write(await data[match[1]]);
|
||||
i = end + 1;
|
||||
} while (i < template.length);
|
||||
}
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.specificity - a.specificity);
|
||||
}
|
||||
|
||||
create_templates();
|
||||
|
||||
if (dev) {
|
||||
const watcher = chokidar.watch('templates/**.html', {
|
||||
ignoreInitial: true,
|
||||
persistent: false
|
||||
});
|
||||
|
||||
watcher.on('add', create_templates);
|
||||
watcher.on('change', create_templates);
|
||||
watcher.on('unlink', create_templates);
|
||||
}
|
||||
|
||||
exports.render = (status, data) => {
|
||||
const template = templates.find(template => template.test(status));
|
||||
@@ -37,7 +82,9 @@ exports.render = (status, data) => {
|
||||
return `Missing template for status code ${status}`;
|
||||
};
|
||||
|
||||
exports.onchange = fn => {
|
||||
// TODO in dev mode, keep this updated, and allow
|
||||
// webpack compiler etc to hook into it
|
||||
exports.stream = (res, status, data) => {
|
||||
const template = templates.find(template => template.test(status));
|
||||
if (template) return template.stream(res, data);
|
||||
|
||||
return `Missing template for status code ${status}`;
|
||||
};
|
||||
11
lib/utils/compilers.js
Normal file
11
lib/utils/compilers.js
Normal file
@@ -0,0 +1,11 @@
|
||||
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,26 +1,83 @@
|
||||
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');
|
||||
|
||||
const template = fs.readFileSync(path.resolve(__dirname, '../../templates/main.js'), 'utf-8');
|
||||
function posixify(file) {
|
||||
return file.replace(/[/\\]/g, '/');
|
||||
}
|
||||
|
||||
module.exports = function create_app(src, dest, routes, options) {
|
||||
// TODO in dev mode, watch files
|
||||
function create_app() {
|
||||
const { routes } = route_manager;
|
||||
|
||||
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(', ') } }`;
|
||||
function create_client_main() {
|
||||
const template = fs.readFileSync('templates/main.js', 'utf-8');
|
||||
|
||||
return `{ pattern: ${route.pattern}, params: match => (${params}), load: () => import('${src}/${route.file}') }`
|
||||
})
|
||||
.join(',\n\t');
|
||||
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 main = template
|
||||
.replace('__app__', path.resolve(__dirname, '../../runtime/app.js'))
|
||||
.replace('__selector__', options.selector || 'main')
|
||||
.replace('__routes__', code);
|
||||
const file = posixify(`${src}/${route.file}`);
|
||||
return `{ pattern: ${route.pattern}, params: match => (${params}), load: () => import(/* webpackChunkName: "${route.id}" */ '${file}') }`
|
||||
})
|
||||
.join(', ')
|
||||
}]`;
|
||||
|
||||
fs.writeFileSync(path.join(dest, 'main.js'), main);
|
||||
};
|
||||
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,22 +1,37 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = function create_matchers(files) {
|
||||
return files
|
||||
const routes = files
|
||||
.map(file => {
|
||||
if (/(^|\/|\\)_/.test(file)) return;
|
||||
|
||||
const parts = file.replace(/\.(html|js|mjs)$/, '').split(path.sep);
|
||||
const parts = file.replace(/\.(html|js|mjs)$/, '').split('/'); // glob output is always posix-style
|
||||
if (parts[parts.length - 1] === 'index') parts.pop();
|
||||
|
||||
const id = parts.join('_').replace(/[[\]]/g, '$') || '_';
|
||||
const id = (
|
||||
parts.join('_').replace(/[[\]]/g, '$').replace(/^\d/, '_$&').replace(/[^a-zA-Z0-9_$]/g, '_')
|
||||
) || '_';
|
||||
|
||||
const dynamic = parts
|
||||
.filter(part => part[0] === '[')
|
||||
.map(part => part.slice(1, -1));
|
||||
|
||||
const pattern = new RegExp(
|
||||
`^\\/${parts.map(p => p[0] === '[' ? '([^/]+)' : p).join('\\/')}$`
|
||||
);
|
||||
let pattern_string = '';
|
||||
let i = parts.length;
|
||||
let nested = true;
|
||||
while (i--) {
|
||||
const part = parts[i];
|
||||
const dynamic = part[0] === '[';
|
||||
|
||||
if (dynamic) {
|
||||
pattern_string = nested ? `(?:\\/([^/]+)${pattern_string})?` : `\\/([^/]+)${pattern_string}`;
|
||||
} else {
|
||||
nested = false;
|
||||
pattern_string = `\\/${part}${pattern_string}`;
|
||||
}
|
||||
}
|
||||
|
||||
const pattern = new RegExp(`^${pattern_string || '\\/'}$`);
|
||||
|
||||
const test = url => pattern.test(url);
|
||||
|
||||
@@ -45,9 +60,31 @@ module.exports = function create_matchers(files) {
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => {
|
||||
return (
|
||||
(a.dynamic.length - b.dynamic.length) || // match static paths first
|
||||
(b.parts.length - a.parts.length) // match longer paths first
|
||||
);
|
||||
let same = true;
|
||||
|
||||
for (let i = 0; true; i += 1) {
|
||||
const a_part = a.parts[i];
|
||||
const b_part = b.parts[i];
|
||||
|
||||
if (!a_part && !b_part) {
|
||||
if (same) throw new Error(`The ${a.file} and ${b.file} routes clash`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!a_part) return -1;
|
||||
if (!b_part) return 1;
|
||||
|
||||
const a_is_dynamic = a_part[0] === '[';
|
||||
const b_is_dynamic = b_part[0] === '[';
|
||||
|
||||
if (a_is_dynamic === b_is_dynamic) {
|
||||
if (!a_is_dynamic) same = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
return a_is_dynamic ? 1 : -1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return routes;
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
const path = require('path');
|
||||
const assert = require('assert');
|
||||
|
||||
const create_matchers = require('./create_matchers.js');
|
||||
|
||||
describe('create_matchers', () => {
|
||||
it('sorts routes correctly', () => {
|
||||
const matchers = create_matchers(['index.html', 'about.html', '[wildcard].html', 'post/[id].html']);
|
||||
|
||||
assert.deepEqual(
|
||||
matchers.map(m => m.file),
|
||||
[
|
||||
'about.html',
|
||||
'index.html',
|
||||
'post/[id].html',
|
||||
'[wildcard].html'
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it('generates params', () => {
|
||||
const matchers = create_matchers(['index.html', 'about.html', '[wildcard].html', 'post/[id].html']);
|
||||
|
||||
let file;
|
||||
let params;
|
||||
for (let i = 0; i < matchers.length; i += 1) {
|
||||
const matcher = matchers[i];
|
||||
if (params = matcher.exec('/post/123')) {
|
||||
file = matcher.file;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assert.equal(file, 'post/[id].html');
|
||||
assert.deepEqual(params, {
|
||||
id: '123'
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores files and directories with leading underscores', () => {
|
||||
const matches = create_matchers(['index.html', '_foo.html', 'a/_b/c/d.html', 'e/f/g/h.html', 'i/_j.html']);
|
||||
|
||||
assert.deepEqual(
|
||||
matches.map(m => m.file),
|
||||
[
|
||||
'e/f/g/h.html',
|
||||
'index.html'
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
75
lib/utils/create_watcher.js
Normal file
75
lib/utils/create_watcher.js
Normal file
@@ -0,0 +1,75 @@
|
||||
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,100 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const glob = require('glob');
|
||||
const webpack = require('webpack');
|
||||
const templates = require('../templates.js');
|
||||
|
||||
module.exports = function create_webpack_compiler(dest, routes, dev) {
|
||||
const compiler = {};
|
||||
|
||||
const client = webpack(
|
||||
require(path.resolve('webpack.client.config.js'))
|
||||
);
|
||||
|
||||
const server = webpack(
|
||||
require(path.resolve('webpack.server.config.js'))
|
||||
);
|
||||
|
||||
if (false) { // TODO watch in dev
|
||||
// TODO how can we invalidate compiler.client_main when watcher restarts?
|
||||
compiler.client_main = new Promise((fulfil, reject) => {
|
||||
client.watch({}, (err, stats) => {
|
||||
if (err || stats.hasErrors()) {
|
||||
// TODO handle errors
|
||||
}
|
||||
|
||||
const filename = stats.toJson().assetsByChunkName.main;
|
||||
fulfil(`/client/${filename}`);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO server
|
||||
} else {
|
||||
compiler.ready = Promise.all([
|
||||
new Promise((fulfil, reject) => {
|
||||
client.run((err, stats) => {
|
||||
console.log(stats.toString({ colors: true }));
|
||||
|
||||
const info = stats.toJson();
|
||||
|
||||
if (err || stats.hasErrors()) {
|
||||
reject(err || info.errors[0]);
|
||||
}
|
||||
|
||||
compiler.client_main = `/client/${info.assetsByChunkName.main}`;
|
||||
compiler.assets = info.assets.map(asset => `/client/${asset.name}`);
|
||||
|
||||
fulfil();
|
||||
});
|
||||
}),
|
||||
|
||||
new Promise((fulfil, reject) => {
|
||||
server.run((err, stats) => {
|
||||
console.log(stats.toString({ colors: true }));
|
||||
|
||||
const info = stats.toJson();
|
||||
|
||||
if (err || stats.hasErrors()) {
|
||||
reject(err || info.errors[0]);
|
||||
}
|
||||
|
||||
compiler.chunks = info.assetsByChunkName;
|
||||
|
||||
fulfil();
|
||||
});
|
||||
})
|
||||
]).then(() => {
|
||||
const assets = glob.sync('**', { cwd: 'assets' });
|
||||
|
||||
const route_code = `[${
|
||||
routes
|
||||
.filter(route => route.type === 'page')
|
||||
.map(route => `{ pattern: ${route.pattern} }`)
|
||||
.join(', ')
|
||||
}]`;
|
||||
|
||||
const service_worker = fs.readFileSync('templates/service-worker.js', 'utf-8')
|
||||
.replace('__timestamp__', Date.now())
|
||||
.replace('__assets__', JSON.stringify(assets))
|
||||
.replace('__shell__', JSON.stringify(compiler.assets.concat('/index.html')))
|
||||
.replace('__routes__', route_code);
|
||||
|
||||
fs.writeFileSync(path.resolve(dest, 'service-worker.js'), service_worker);
|
||||
|
||||
const shell = templates.render(200, {
|
||||
styles: '',
|
||||
head: '',
|
||||
html: '<noscript>Please enable JavaScript!</noscript>',
|
||||
main: compiler.client_main
|
||||
});
|
||||
|
||||
fs.writeFileSync(path.resolve(dest, 'index.html'), shell);
|
||||
});
|
||||
|
||||
compiler.get_chunk = async id => {
|
||||
return path.resolve(dest, 'server', compiler.chunks[id]);
|
||||
};
|
||||
}
|
||||
|
||||
return compiler;
|
||||
};
|
||||
79
lib/utils/generate_asset_cache.js
Normal file
79
lib/utils/generate_asset_cache.js
Normal file
@@ -0,0 +1,79 @@
|
||||
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,2 +1,3 @@
|
||||
--recursive
|
||||
utils/**/*.test.js
|
||||
test/unit/**/*.js
|
||||
test/common/test.js
|
||||
3908
package-lock.json
generated
3908
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
54
package.json
54
package.json
@@ -1,27 +1,56 @@
|
||||
{
|
||||
"name": "sapper",
|
||||
"version": "0.0.15",
|
||||
"description": "Combat-ready apps, engineered by Svelte",
|
||||
"main": "connect.js",
|
||||
"version": "0.3.0",
|
||||
"description": "Military-grade apps, engineered by Svelte",
|
||||
"main": "lib/index.js",
|
||||
"bin": {
|
||||
"sapper": "cli/index.js"
|
||||
},
|
||||
"files": [
|
||||
"cli",
|
||||
"lib",
|
||||
"runtime",
|
||||
"webpack"
|
||||
],
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@std/esm": "^0.18.0",
|
||||
"chalk": "^2.3.0",
|
||||
"chokidar": "^1.7.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"mkdirp": "^0.5.1",
|
||||
"relative": "^3.0.2",
|
||||
"require-relative": "^0.8.7",
|
||||
"rimraf": "^2.6.2",
|
||||
"webpack": "^3.10.0"
|
||||
"webpack": "^3.10.0",
|
||||
"webpack-hot-middleware": "^2.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"css-loader": "^0.28.7",
|
||||
"eslint": "^4.13.1",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"express": "^4.16.2",
|
||||
"get-port": "^3.2.0",
|
||||
"mocha": "^4.0.1",
|
||||
"svelte": "^1.47.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^1.47.1"
|
||||
"nightmare": "^2.10.0",
|
||||
"node-fetch": "^1.7.3",
|
||||
"npm-run-all": "^4.1.2",
|
||||
"rollup": "^0.53.0",
|
||||
"rollup-plugin-typescript": "^0.8.1",
|
||||
"style-loader": "^0.19.1",
|
||||
"svelte": "^1.49.1",
|
||||
"svelte-loader": "^2.3.2",
|
||||
"tslib": "^1.8.1",
|
||||
"typescript": "^2.6.2",
|
||||
"wait-on": "^2.0.2"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "mocha --opts mocha.opts"
|
||||
"cy:open": "cypress open",
|
||||
"test": "mocha --opts mocha.opts",
|
||||
"pretest": "npm run build",
|
||||
"build": "rollup -c",
|
||||
"dev": "rollup -cw"
|
||||
},
|
||||
"repository": "https://github.com/sveltejs/sapper",
|
||||
"keywords": [
|
||||
@@ -36,8 +65,5 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/sveltejs/sapper/issues"
|
||||
},
|
||||
"homepage": "https://github.com/sveltejs/sapper#readme",
|
||||
"@std/esm": {
|
||||
"esm": "js"
|
||||
}
|
||||
"homepage": "https://github.com/sveltejs/sapper#readme"
|
||||
}
|
||||
|
||||
17
rollup.config.js
Normal file
17
rollup.config.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import typescript from 'rollup-plugin-typescript';
|
||||
|
||||
export default [
|
||||
// runtime.js
|
||||
{
|
||||
input: 'src/runtime/index.ts',
|
||||
output: {
|
||||
file: 'runtime.js',
|
||||
format: 'es'
|
||||
},
|
||||
plugins: [
|
||||
typescript({
|
||||
typescript: require('typescript')
|
||||
})
|
||||
]
|
||||
}
|
||||
];
|
||||
106
runtime/app.js
106
runtime/app.js
@@ -1,104 +1,2 @@
|
||||
const detach = node => {
|
||||
node.parentNode.removeChild(node);
|
||||
};
|
||||
|
||||
let component;
|
||||
|
||||
const app = {
|
||||
init(target, routes) {
|
||||
function select_route(url) {
|
||||
if (url.origin !== window.location.origin) return null;
|
||||
|
||||
for (const route of routes) {
|
||||
const match = route.pattern.exec(url.pathname);
|
||||
if (match) {
|
||||
const params = route.params(match);
|
||||
|
||||
const query = {};
|
||||
for (const [key, value] of url.searchParams) query[key] = value || true;
|
||||
|
||||
return { route, data: { params, query } };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function render(Component, data) {
|
||||
Promise.resolve(
|
||||
Component.preload ? Component.preload(data) : {}
|
||||
).then(preloaded => {
|
||||
if (component) {
|
||||
component.destroy();
|
||||
} else {
|
||||
// first load — remove SSR'd <head> contents
|
||||
const start = document.querySelector('#sapper-head-start');
|
||||
let end = document.querySelector('#sapper-head-end');
|
||||
|
||||
if (start && end) {
|
||||
while (start.nextSibling !== end) detach(start.nextSibling);
|
||||
detach(start);
|
||||
detach(end);
|
||||
}
|
||||
|
||||
// preload additional routes
|
||||
routes.reduce((promise, route) => promise.then(route.load), Promise.resolve());
|
||||
}
|
||||
|
||||
component = new Component({
|
||||
target,
|
||||
data: Object.assign(data, preloaded),
|
||||
hydrate: !!component
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function navigate(url) {
|
||||
const selected = select_route(url);
|
||||
if (selected) {
|
||||
selected.route.load().then(mod => {
|
||||
render(mod.default, selected.data);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function findAnchor(node) {
|
||||
while (node && node.nodeName !== 'A') node = node.parentNode;
|
||||
return node;
|
||||
}
|
||||
|
||||
window.addEventListener('click', event => {
|
||||
const a = findAnchor(event.target);
|
||||
if (!a) return;
|
||||
|
||||
if (navigate(new URL(a.href))) {
|
||||
event.preventDefault();
|
||||
history.pushState({}, '', a.href);
|
||||
}
|
||||
});
|
||||
|
||||
function preload(event) {
|
||||
const a = findAnchor(event.target);
|
||||
if (!a || a.rel !== 'prefetch') return;
|
||||
|
||||
const selected = select_route(new URL(a.href));
|
||||
|
||||
if (selected) {
|
||||
selected.route.load().then(mod => {
|
||||
if (mod.default.preload) mod.default.preload(selected.data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('touchstart', preload);
|
||||
window.addEventListener('mouseover', preload);
|
||||
|
||||
window.addEventListener('popstate', event => {
|
||||
navigate(new URL(window.location));
|
||||
});
|
||||
|
||||
navigate(new URL(window.location));
|
||||
}
|
||||
};
|
||||
|
||||
export default app;
|
||||
console.error('sapper/runtime/app.js has been deprecated in favour of sapper/runtime.js');
|
||||
export * from '../runtime.js';
|
||||
217
src/runtime/index.ts
Normal file
217
src/runtime/index.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
import { detach, findAnchor, scroll_state, which } from './utils';
|
||||
import { Component, ComponentConstructor, Params, Query, Route, RouteData, ScrollPosition } from './interfaces';
|
||||
|
||||
export let component: Component;
|
||||
let target: Node;
|
||||
let routes: Route[];
|
||||
|
||||
const history = typeof window !== 'undefined' ? window.history : {
|
||||
pushState: (state: any, title: string, href: string) => {},
|
||||
replaceState: (state: any, title: string, href: string) => {},
|
||||
scrollRestoration: ''
|
||||
};
|
||||
|
||||
const scroll_history: Record<string, ScrollPosition> = {};
|
||||
let uid = 1;
|
||||
let cid: number;
|
||||
|
||||
if ('scrollRestoration' in history) {
|
||||
history.scrollRestoration = 'manual';
|
||||
}
|
||||
|
||||
function select_route(url: URL): { route: Route, data: RouteData } {
|
||||
if (url.origin !== window.location.origin) return null;
|
||||
|
||||
for (const route of routes) {
|
||||
const match = route.pattern.exec(url.pathname);
|
||||
if (match) {
|
||||
const params = route.params(match);
|
||||
|
||||
const query: Record<string, string | true> = {};
|
||||
for (const [key, value] of url.searchParams) query[key] = value || true;
|
||||
|
||||
return { route, data: { params, query } };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let current_token: {};
|
||||
|
||||
function render(Component: ComponentConstructor, data: any, scroll: ScrollPosition, token: {}) {
|
||||
if (current_token !== token) return;
|
||||
|
||||
if (component) {
|
||||
component.destroy();
|
||||
} else {
|
||||
// first load — remove SSR'd <head> contents
|
||||
const start = document.querySelector('#sapper-head-start');
|
||||
const end = document.querySelector('#sapper-head-end');
|
||||
|
||||
if (start && end) {
|
||||
while (start.nextSibling !== end) detach(start.nextSibling);
|
||||
detach(start);
|
||||
detach(end);
|
||||
}
|
||||
|
||||
// preload additional routes
|
||||
routes.reduce((promise: Promise<any>, route) => promise.then(route.load), Promise.resolve());
|
||||
}
|
||||
|
||||
component = new Component({
|
||||
target,
|
||||
data,
|
||||
hydrate: !!component
|
||||
});
|
||||
|
||||
if (scroll) {
|
||||
window.scrollTo(scroll.x, scroll.y);
|
||||
}
|
||||
}
|
||||
|
||||
function prepare_route(Component, data) {
|
||||
return Promise.resolve(
|
||||
Component.preload ? Component.preload(data) : {}
|
||||
).then(preloaded => {
|
||||
Object.assign(data, preloaded)
|
||||
return { Component, data };
|
||||
});
|
||||
}
|
||||
|
||||
function navigate(url: URL, id: number) {
|
||||
const selected = select_route(url);
|
||||
if (selected) {
|
||||
if (id) {
|
||||
// popstate or initial navigation
|
||||
cid = id;
|
||||
} else {
|
||||
// clicked on a link. preserve scroll state
|
||||
scroll_history[cid] = scroll_state();
|
||||
|
||||
id = cid = ++uid;
|
||||
scroll_history[cid] = { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
const loaded = prefetching && prefetching.href === url.href ?
|
||||
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) {
|
||||
// Adapted from https://github.com/visionmedia/page.js
|
||||
// MIT license https://github.com/visionmedia/page.js#license
|
||||
if (which(event) !== 1) return;
|
||||
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
|
||||
if (event.defaultPrevented) return;
|
||||
|
||||
const a: HTMLAnchorElement | SVGAElement = <HTMLAnchorElement | SVGAElement>findAnchor(<Node>event.target);
|
||||
if (!a) return;
|
||||
|
||||
// check if link is inside an svg
|
||||
// in this case, both href and target are always inside an object
|
||||
const svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
|
||||
const href = String(svg ? (<SVGAElement>a).href.baseVal : a.href);
|
||||
|
||||
if (href === window.location.href) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore if tag has
|
||||
// 1. 'download' attribute
|
||||
// 2. rel='external' attribute
|
||||
if (a.hasAttribute('download') || a.getAttribute('rel') === 'external') return;
|
||||
|
||||
// Ignore if <a> has a target
|
||||
if (svg ? (<SVGAElement>a).target.baseVal : a.target) return;
|
||||
|
||||
const url = new URL(href);
|
||||
|
||||
// Don't handle hash changes
|
||||
if (url.pathname === window.location.pathname && url.search === window.location.search) return;
|
||||
|
||||
if (navigate(url, null)) {
|
||||
event.preventDefault();
|
||||
history.pushState({ id: cid }, '', url.href);
|
||||
}
|
||||
}
|
||||
|
||||
function handle_popstate(event: PopStateEvent) {
|
||||
scroll_history[cid] = scroll_state();
|
||||
|
||||
if (event.state) {
|
||||
navigate(new URL(window.location.href), event.state.id);
|
||||
} else {
|
||||
// hashchange
|
||||
cid = ++uid;
|
||||
history.replaceState({ id: cid }, '', window.location.href);
|
||||
}
|
||||
}
|
||||
|
||||
let prefetching: {
|
||||
href: string;
|
||||
promise: Promise<{ Component: ComponentConstructor, data: any }>;
|
||||
} = null;
|
||||
|
||||
function prefetch(event: MouseEvent | TouchEvent) {
|
||||
const a: HTMLAnchorElement = <HTMLAnchorElement>findAnchor(<Node>event.target);
|
||||
if (!a || a.rel !== 'prefetch') return;
|
||||
|
||||
const selected = select_route(new URL(a.href));
|
||||
|
||||
if (selected) {
|
||||
prefetching = {
|
||||
href: a.href,
|
||||
promise: selected.route.load().then(mod => prepare_route(mod.default, selected.data))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let inited: boolean;
|
||||
|
||||
export function init(_target: Node, _routes: Route[]) {
|
||||
target = _target;
|
||||
routes = _routes;
|
||||
|
||||
if (!inited) { // this check makes HMR possible
|
||||
window.addEventListener('click', handle_click);
|
||||
window.addEventListener('popstate', handle_popstate);
|
||||
|
||||
// prefetch
|
||||
window.addEventListener('touchstart', prefetch);
|
||||
window.addEventListener('mouseover', prefetch);
|
||||
|
||||
inited = true;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const { hash, href } = window.location;
|
||||
|
||||
const deep_linked = hash && document.querySelector(hash);
|
||||
scroll_history[uid] = deep_linked ?
|
||||
{ x: 0, y: deep_linked.getBoundingClientRect().top } :
|
||||
scroll_state();
|
||||
|
||||
history.replaceState({ id: uid }, '', href);
|
||||
navigate(new URL(window.location.href), uid);
|
||||
});
|
||||
}
|
||||
|
||||
export function goto(href: string, opts = { replaceState: false }) {
|
||||
if (navigate(new URL(href, window.location.href), null)) {
|
||||
if (history) history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
|
||||
} else {
|
||||
window.location.href = href;
|
||||
}
|
||||
}
|
||||
23
src/runtime/interfaces.ts
Normal file
23
src/runtime/interfaces.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export type Params = Record<string, string>;
|
||||
export type Query = Record<string, string | true>;
|
||||
export type RouteData = { params: Params, query: Query };
|
||||
|
||||
export interface ComponentConstructor {
|
||||
new (options: { target: Node, data: any, hydrate: boolean }): Component;
|
||||
preload: (data: { params: Params, query: Query }) => Promise<any>;
|
||||
};
|
||||
|
||||
export interface Component {
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
export type Route = {
|
||||
pattern: RegExp;
|
||||
params: (match: RegExpExecArray) => Record<string, string>;
|
||||
load: () => Promise<{ default: ComponentConstructor }>
|
||||
};
|
||||
|
||||
export type ScrollPosition = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
19
src/runtime/utils.ts
Normal file
19
src/runtime/utils.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export function detach(node: Node) {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
|
||||
export function findAnchor(node: Node) {
|
||||
while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG <a> elements have a lowercase name
|
||||
return node;
|
||||
}
|
||||
|
||||
export function which(event: MouseEvent) {
|
||||
return event.which === null ? event.button : event.which;
|
||||
}
|
||||
|
||||
export function scroll_state() {
|
||||
return {
|
||||
x: window.scrollX,
|
||||
y: window.scrollY
|
||||
};
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import app from '__app__';
|
||||
|
||||
app.init(document.querySelector('__selector__'), [
|
||||
__routes__
|
||||
]);
|
||||
6
test/app/.gitignore
vendored
Normal file
6
test/app/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
.sapper
|
||||
yarn.lock
|
||||
cypress/screenshots
|
||||
templates/.*
|
||||
81
test/app/README.md
Normal file
81
test/app/README.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# sapper-template
|
||||
|
||||
The default [Sapper](https://github.com/sveltejs/sapper) template. To clone it and get started:
|
||||
|
||||
```bash
|
||||
npx degit sveltejs/sapper-template my-app
|
||||
cd my-app
|
||||
npm install # or yarn!
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open up [localhost:3000](http://localhost:3000) and start clicking around.
|
||||
|
||||
|
||||
## Structure
|
||||
|
||||
Sapper expects to find three directories in the root of your project — `assets`, `routes` and `templates`.
|
||||
|
||||
|
||||
### assets
|
||||
|
||||
The [assets](assets) directory contains any static assets that should be available. These are served using [serve-static](https://github.com/expressjs/serve-static).
|
||||
|
||||
In your [service-worker.js](templates/service-worker.js) file, Sapper makes these files available as `__assets__` so that you can cache them (though you can choose not to, for example if you don't want to cache very large files).
|
||||
|
||||
|
||||
### routes
|
||||
|
||||
This is the heart of your Sapper app. There are two kinds of routes — *pages*, and *server routes*.
|
||||
|
||||
**Pages** are Svelte components written in `.html` files. When a user first visits the application, they will be served a server-rendered version of the route in question, plus some JavaScript that 'hydrates' the page and initialises a client-side router. From that point forward, navigating to other pages is handled entirely on the client for a fast, app-like feel. (Sapper will preload and cache the code for these subsequent pages, so that navigation is instantaneous.)
|
||||
|
||||
**Server routes** are modules written in `.js` files, that export functions corresponding to HTTP methods. Each function receives Express `request` and `response` objects as arguments, plus a `next` function. This is useful for creating a JSON API, for example.
|
||||
|
||||
There are three simple rules for naming the files that define your routes:
|
||||
|
||||
* A file called `routes/about.html` corresponds to the `/about` route. A file called `routes/blog/[slug].html` corresponds to the `/blog/:slug` route, in which case `params.slug` is available to the route
|
||||
* The file `routes/index.html` (or `routes/index.js`) corresponds to the root of your app. `routes/about/index.html` is treated the same as `routes/about.html`.
|
||||
* Files and directories with a leading underscore do *not* create routes. This allows you to colocate helper modules and components with the routes that depend on them — for example you could have a file called `routes/_helpers/datetime.js` and it would *not* create a `/_helpers/datetime` route
|
||||
|
||||
|
||||
### templates
|
||||
|
||||
This directory should contain the following files at a minimum:
|
||||
|
||||
* [2xx.html](templates/2xx.html) — a template for the page to serve for valid requests
|
||||
* [4xx.html](templates/4xx.html) — a template for 4xx-range errors (such as 404 Not Found)
|
||||
* [5xx.html](templates/5xx.html) — a template for 5xx-range errors (such as 500 Internal Server Error)
|
||||
* [main.js](templates/main.js) — this module initialises Sapper
|
||||
* [service-worker.js](templates/service-worker.js) — your app's service worker
|
||||
|
||||
Inside the HTML templates, Sapper will inject various values as indicated by `%sapper.xxxx%` tags. Inside JavaScript files, Sapper will replace strings like `__dev__` with the appropriate value.
|
||||
|
||||
In lieu of documentation (bear with us), consult the files to see what variables are available and how they're used.
|
||||
|
||||
|
||||
## Webpack config
|
||||
|
||||
Sapper uses webpack to provide code-splitting, dynamic imports and hot module reloading, as well as compiling your Svelte components. As long as you don't do anything daft, you can edit the configuration files to add whatever loaders and plugins you'd like.
|
||||
|
||||
|
||||
## Production mode and deployment
|
||||
|
||||
To start a production version of your app, run `npm start`. This will disable hot module replacement, and activate the appropriate webpack plugins.
|
||||
|
||||
You can deploy your application to any environment that supports Node 8 or above. As an example, to deploy to [Now](https://zeit.co/now), run these commands:
|
||||
|
||||
```bash
|
||||
npm install -g now
|
||||
now
|
||||
```
|
||||
|
||||
|
||||
## Bugs and feedback
|
||||
|
||||
Sapper is in early development, and may have the odd rough edge here and there. Please be vocal over on the [Sapper issue tracker](https://github.com/sveltejs/sapper/issues).
|
||||
|
||||
|
||||
## License
|
||||
|
||||
[LIL](LICENSE)
|
||||
BIN
test/app/assets/favicon.png
Normal file
BIN
test/app/assets/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
45
test/app/assets/global.css
Normal file
45
test/app/assets/global.css
Normal file
@@ -0,0 +1,45 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Roboto, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
main {
|
||||
position: relative;
|
||||
max-width: 56em;
|
||||
background-color: white;
|
||||
padding: 2em;
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 0 0 0.5em 0;
|
||||
font-weight: 400;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: menlo, inconsolata, monospace;
|
||||
font-size: calc(1em - 2px);
|
||||
color: #555;
|
||||
background-color: #f0f0f0;
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
@media (min-width: 400px) {
|
||||
body {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
BIN
test/app/assets/great-success.png
Normal file
BIN
test/app/assets/great-success.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
20
test/app/assets/manifest.json
Normal file
20
test/app/assets/manifest.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#aa1e1e",
|
||||
"name": "TODO",
|
||||
"short_name": "TODO",
|
||||
"display": "minimal-ui",
|
||||
"start_url": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "svelte-logo-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "svelte-logo-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
test/app/assets/svelte-logo-192.png
Normal file
BIN
test/app/assets/svelte-logo-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
test/app/assets/svelte-logo-512.png
Normal file
BIN
test/app/assets/svelte-logo-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
4931
test/app/package-lock.json
generated
Normal file
4931
test/app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
test/app/package.json
Normal file
28
test/app/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "TODO",
|
||||
"description": "TODO",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "node server.js",
|
||||
"build": "sapper build",
|
||||
"start": "cross-env NODE_ENV=production node server.js",
|
||||
"prestart": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"compression": "^1.7.1",
|
||||
"cross-env": "^5.1.1",
|
||||
"css-loader": "^0.28.7",
|
||||
"express": "^4.16.2",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"glob": "^7.1.2",
|
||||
"marked": "^0.3.7",
|
||||
"node-fetch": "^1.7.3",
|
||||
"npm-run-all": "^4.1.2",
|
||||
"serve-static": "^1.13.1",
|
||||
"style-loader": "^0.19.0",
|
||||
"svelte": "^1.49.1",
|
||||
"svelte-loader": "^2.2.1",
|
||||
"uglifyjs-webpack-plugin": "^1.1.2",
|
||||
"webpack": "^3.10.0"
|
||||
}
|
||||
}
|
||||
15
test/app/routes/_components/Layout.html
Normal file
15
test/app/routes/_components/Layout.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<Nav page={{page}}/>
|
||||
|
||||
<main>
|
||||
<slot></slot>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
import Nav from './Nav.html';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Nav
|
||||
}
|
||||
};
|
||||
</script>
|
||||
54
test/app/routes/_components/Nav.html
Normal file
54
test/app/routes/_components/Nav.html
Normal file
@@ -0,0 +1,54 @@
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href='/'>home</a></li>
|
||||
<li><a href='/about'>about</a></li>
|
||||
<li><a href='/slow-preload'>slow preload</a></li>
|
||||
<li><a rel=prefetch class='{{page === "blog" ? "selected" : ""}}' href='/blog'>blog</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
nav {
|
||||
border-bottom: 1px solid rgba(170,30,30,0.1);
|
||||
font-weight: 300;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* clearfix */
|
||||
ul::after {
|
||||
content: '';
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
li {
|
||||
display: block;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.selected {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.selected::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
width: calc(100% - 1em);
|
||||
height: 2px;
|
||||
background-color: rgb(170,30,30);
|
||||
display: block;
|
||||
bottom: -1px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
padding: 1em 0.5em;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
26
test/app/routes/about.html
Normal file
26
test/app/routes/about.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<:Head>
|
||||
<title>About</title>
|
||||
</:Head>
|
||||
|
||||
<Layout page='about'>
|
||||
<h1>About this site</h1>
|
||||
|
||||
<p>This is the 'about' page. There's not much here.</p>
|
||||
|
||||
<button on:click='goto("/blog/what-is-sapper")'>What is Sapper?</button>
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
import Layout from './_components/Layout.html';
|
||||
import { goto } from '../../../runtime.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Layout
|
||||
},
|
||||
|
||||
methods: {
|
||||
goto
|
||||
}
|
||||
};
|
||||
</script>
|
||||
23
test/app/routes/api/blog/[slug].js
Normal file
23
test/app/routes/api/blog/[slug].js
Normal file
@@ -0,0 +1,23 @@
|
||||
import posts from './_posts.js';
|
||||
|
||||
const lookup = {};
|
||||
posts.forEach(post => {
|
||||
lookup[post.slug] = JSON.stringify(post);
|
||||
});
|
||||
|
||||
export function get(req, res, next) {
|
||||
// the `slug` parameter is available because this file
|
||||
// is called [slug].js
|
||||
const { slug } = req.params;
|
||||
|
||||
if (slug in lookup) {
|
||||
res.set({
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': `no-cache`
|
||||
});
|
||||
|
||||
res.end(lookup[slug]);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
116
test/app/routes/api/blog/_posts.js
Normal file
116
test/app/routes/api/blog/_posts.js
Normal file
@@ -0,0 +1,116 @@
|
||||
// Ordinarily, you'd generate this data from markdown files in your
|
||||
// repo, or fetch them from a database of some kind. But in order to
|
||||
// avoid unnecessary dependencies in the starter template, and in the
|
||||
// service of obviousness, we're just going to leave it here.
|
||||
|
||||
// This file is called `_posts.js` rather than `posts.js`, because
|
||||
// we don't want to create an `/api/blog/posts` route — the leading
|
||||
// underscore tells Sapper not to do that.
|
||||
|
||||
const posts = [
|
||||
{
|
||||
title: 'What is Sapper?',
|
||||
slug: 'what-is-sapper',
|
||||
html: `
|
||||
<p>First, you have to know what <a href='https://svelte.technology'>Svelte</a> is. Svelte is a UI framework with a bold new idea: rather than providing a library that you write code with (like React or Vue, for example), it's a compiler that turns your components into highly optimized vanilla JavaScript. If you haven't already read the <a href='https://svelte.technology/blog/frameworks-without-the-framework'>introductory blog post</a>, you should!</p>
|
||||
|
||||
<p>Sapper is a Next.js-style framework (<a href='/blog/how-is-sapper-different-from-next'>more on that here</a>) built around Svelte. It makes it embarrassingly easy to create extremely high performance web apps. Out of the box, you get:</p>
|
||||
|
||||
<ul>
|
||||
<li>Code-splitting, dynamic imports and hot module replacement, powered by webpack</li>
|
||||
<li>Server-side rendering (SSR) with client-side hydration</li>
|
||||
<li>Service worker for offline support, and all the PWA bells and whistles</li>
|
||||
<li>The nicest development experience you've ever had, or your money back</li>
|
||||
</ul>
|
||||
|
||||
<p>It's implemented as Express middleware. Everything is set up and waiting for you to get started, but you keep complete control over the server, service worker, webpack config and everything else, so it's as flexible as you need it to be.</p>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
title: 'How to use Sapper',
|
||||
slug: 'how-to-use-sapper',
|
||||
html: `
|
||||
<h2>Step one</h2>
|
||||
<p>Create a new project, using <a href='https://github.com/Rich-Harris/degit'>degit</a>:</p>
|
||||
|
||||
<pre><code>npx degit sveltejs/sapper-template my-app
|
||||
cd my-app
|
||||
npm install # or yarn!
|
||||
npm run dev
|
||||
</code></pre>
|
||||
|
||||
<h2>Step two</h2>
|
||||
<p>Go to <a href='http://localhost:3000'>localhost:3000</a>. Open <code>my-app</code> in your editor. Edit the files in the <code>routes</code> directory or add new ones.</p>
|
||||
|
||||
<h2>Step three</h2>
|
||||
<p>...</p>
|
||||
|
||||
<h2>Step four</h2>
|
||||
<p>Resist overdone joke formats.</p>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Why the name?',
|
||||
slug: 'why-the-name',
|
||||
html: `
|
||||
<p>In war, the soldiers who build bridges, repair roads, clear minefields and conduct demolitions — all under combat conditions — are known as <em>sappers</em>.</p>
|
||||
|
||||
<p>For web developers, the stakes are generally lower than those for combat engineers. But we face our own hostile environment: underpowered devices, poor network connections, and the complexity inherent in front-end engineering. Sapper, which is short for <strong>S</strong>velte <strong>app</strong> mak<strong>er</strong>, is your courageous and dutiful ally.</p>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
title: 'How is Sapper different from Next.js?',
|
||||
slug: 'how-is-sapper-different-from-next',
|
||||
html: `
|
||||
<p><a href='https://github.com/zeit/next.js/'>Next.js</a> is a React framework from <a href='https://zeit.co'>Zeit</a>, and is the inspiration for Sapper. There are a few notable differences, however:</p>
|
||||
|
||||
<ul>
|
||||
<li>It's powered by <a href='https://svelte.technology'>Svelte</a> instead of React, so it's faster and your apps are smaller</li>
|
||||
<li>Instead of route masking, we encode route parameters in filenames. For example, the page you're looking at right now is <code>routes/blog/[slug].html</code></li>
|
||||
<li>As well as pages (Svelte components, which render on server or client), you can create <em>server routes</em> in your <code>routes</code> directory. These are just <code>.js</code> files that export functions corresponding to HTTP methods, and receive Express <code>request</code> and <code>response</code> objects as arguments. This makes it very easy to, for example, add a JSON API such as the one powering this very page (look in <code>routes/api/blog</code>)</li>
|
||||
<li>Links are just <code><a></code> elements, rather than framework-specific <code><Link></code> components. That means, for example, that <a href='/blog/how-can-i-get-involved'>this link right here</a>, despite being inside a blob of HTML, works with the router as you'd expect.</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
title: 'How can I get involved?',
|
||||
slug: 'how-can-i-get-involved',
|
||||
html: `
|
||||
<p>We're so glad you asked! Come on over to the <a href='https://github.com/sveltejs/svelte'>Svelte</a> and <a href='https://github.com/sveltejs/sapper'>Sapper</a> repos, and join us in the <a href='https://gitter.im/sveltejs/svelte'>Gitter chatroom</a>. Everyone is welcome, especially you!</p>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
title: 'A very long post with deep links',
|
||||
slug: 'a-very-long-post',
|
||||
html: `
|
||||
<h2 id='one'>One</h2>
|
||||
<p>I'll have a vodka rocks. (Mom, it's breakfast time.) And a piece of toast. Let me out that Queen. Fried cheese… with club sauce.</p>
|
||||
<p>Her lawyers are claiming the seal is worth $250,000. And that's not even including Buster's Swatch. This was a big get for God. What, so the guy we are meeting with can't even grow his own hair? COME ON! She's always got to wedge herself in the middle of us so that she can control everything. Yeah. Mom's awesome. It's, like, Hey, you want to go down to the whirlpool? Yeah, I don't have a husband. I call it Swing City. The CIA should've just Googled for his hideout, evidently. There are dozens of us! DOZENS! Yeah, like I'm going to take a whiz through this $5,000 suit. COME ON.</p>
|
||||
|
||||
<h2 id='two'>Two</h2>
|
||||
<p>Tobias Fünke costume. Heart attack never stopped old big bear.</p>
|
||||
<p>Nellie is blowing them all AWAY. I will be a bigger and hairier mole than the one on your inner left thigh! I'll sacrifice anything for my children.</p>
|
||||
<p>Up yours, granny! You couldn't handle it! Hey, Dad. Look at you. You're a year older…and a year closer to death. Buster: Oh yeah, I guess that's kind of funny. Bob Loblaw Law Blog. The guy runs a prison, he can have any piece of ass he wants.</p>
|
||||
|
||||
<h2 id='three'>Three</h2>
|
||||
<p>I prematurely shot my wad on what was supposed to be a dry run, so now I'm afraid I have something of a mess on my hands. Dead Dove DO NOT EAT. Never once touched my per diem. I'd go to Craft Service, get some raw veggies, bacon, Cup-A-Soup…baby, I got a stew goin'. You're losing blood, aren't you? Gob: Probably, my socks are wet. Sure, let the little fruit do it. HUZZAH! Although George Michael had only got to second base, he'd gone in head first, like Pete Rose. I will pack your sweet pink mouth with so much ice cream you'll be the envy of every Jerry and Jane on the block!</p>
|
||||
<p>Gosh Mom… after all these years, God's not going to take a call from you. Come on, this is a Bluth family celebration. It's no place for children.</p>
|
||||
<p>And I wouldn't just lie there, if that's what you're thinking. That's not what I WAS thinking. Who? i just dont want him to point out my cracker ass in front of ann. When a man needs to prove to a woman that he's actually… When a man loves a woman… Heyyyyyy Uncle Father Oscar. [Stabbing Gob] White power! Gob: I'm white! Let me take off my assistant's skirt and put on my Barbra-Streisand-in-The-Prince-of-Tides ass-masking therapist pantsuit. In the mid '90s, Tobias formed a folk music band with Lindsay and Maebe which he called Dr. Funke's 100 Percent Natural Good Time Family Band Solution. The group was underwritten by the Natural Food Life Company, a division of Chem-Grow, an Allen Crayne acqusition, which was part of the Squimm Group. Their motto was simple: We keep you alive.</p>
|
||||
|
||||
<h2 id='four'>Four</h2>
|
||||
<p>If you didn't have adult onset diabetes, I wouldn't mind giving you a little sugar. Everybody dance NOW. And the soup of the day is bread. Great, now I'm gonna smell to high heaven like a tuna melt!</p>
|
||||
<p>That's how Tony Wonder lost a nut. She calls it a Mayonegg. Go ahead, touch the Cornballer. There's a new daddy in town. A discipline daddy.</p>
|
||||
`
|
||||
}
|
||||
];
|
||||
|
||||
posts.forEach(post => {
|
||||
post.html = post.html.replace(/^\t{3}/gm, '');
|
||||
});
|
||||
|
||||
export default posts;
|
||||
17
test/app/routes/api/blog/index.js
Normal file
17
test/app/routes/api/blog/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import posts from './_posts.js';
|
||||
|
||||
const contents = JSON.stringify(posts.map(post => {
|
||||
return {
|
||||
title: post.title,
|
||||
slug: post.slug
|
||||
};
|
||||
}));
|
||||
|
||||
export function get(req, res) {
|
||||
res.set({
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': `max-age=${30 * 60 * 1e3}` // cache for 30 minutes
|
||||
});
|
||||
|
||||
res.end(contents);
|
||||
}
|
||||
67
test/app/routes/blog/[slug].html
Normal file
67
test/app/routes/blog/[slug].html
Normal file
@@ -0,0 +1,67 @@
|
||||
<:Head>
|
||||
<title>{{post.title}}</title>
|
||||
</:Head>
|
||||
|
||||
<Layout page='blog'>
|
||||
<h1>{{post.title}}</h1>
|
||||
|
||||
<div class='content'>
|
||||
{{{post.html}}}
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
/*
|
||||
By default, CSS is locally scoped to the component,
|
||||
and any unused styles are dead-code-eliminated.
|
||||
In this page, Svelte can't know which elements are
|
||||
going to appear inside the {{{post.html}}} block,
|
||||
so we have to use the :global(...) modifier to target
|
||||
all elements inside .content
|
||||
*/
|
||||
.content :global(h2) {
|
||||
font-size: 1.4em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.content :global(pre) {
|
||||
background-color: #f9f9f9;
|
||||
box-shadow: inset 1px 1px 5px rgba(0,0,0,0.05);
|
||||
padding: 0.5em;
|
||||
border-radius: 2px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.content :global(pre) :global(code) {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.content :global(ul) {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.content :global(li) {
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Layout from '../_components/Layout.html';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Layout
|
||||
},
|
||||
|
||||
preload({ params, query }) {
|
||||
// the `slug` parameter is available because this file
|
||||
// is called [slug].html
|
||||
const { slug } = params;
|
||||
|
||||
return fetch(`/api/blog/${slug}`).then(r => r.json()).then(post => {
|
||||
return { post };
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
40
test/app/routes/blog/index.html
Normal file
40
test/app/routes/blog/index.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<:Head>
|
||||
<title>Blog</title>
|
||||
</:Head>
|
||||
|
||||
<Layout page='blog'>
|
||||
<h1>Recent posts</h1>
|
||||
|
||||
<ul>
|
||||
{{#each posts as post}}
|
||||
<!-- we're using the non-standard `rel=prefetch` attribute to
|
||||
tell Sapper to load the data for the page as soon as
|
||||
the user hovers over the link or taps it, instead of
|
||||
waiting for the 'click' event -->
|
||||
<li><a rel='prefetch' href='/blog/{{post.slug}}'>{{post.title}}</a></li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
ul {
|
||||
margin: 0 0 1em 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Layout from '../_components/Layout.html';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Layout
|
||||
},
|
||||
|
||||
preload({ params, query }) {
|
||||
return fetch(`/api/blog`).then(r => r.json()).then(posts => {
|
||||
return { posts };
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
58
test/app/routes/index.html
Normal file
58
test/app/routes/index.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<:Head>
|
||||
<title>Sapper project template</title>
|
||||
</:Head>
|
||||
|
||||
<Layout page='home'>
|
||||
<h1>Great success!</h1>
|
||||
|
||||
<figure>
|
||||
<img src='/great-success.png'>
|
||||
<figcaption>HIGH FIVE!</figcaption>
|
||||
</figure>
|
||||
|
||||
<p><strong>Try editing this file (routes/index.html) to test hot module reloading.</strong></p>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
h1, figure, p {
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.8em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
h1 {
|
||||
font-size: 4em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Layout from './_components/Layout.html';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Layout
|
||||
}
|
||||
};
|
||||
</script>
|
||||
9
test/app/routes/show-url.html
Normal file
9
test/app/routes/show-url.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<p>URL is {{url}}</p>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload({ url }) {
|
||||
return { url };
|
||||
}
|
||||
};
|
||||
</script>
|
||||
11
test/app/routes/slow-preload.html
Normal file
11
test/app/routes/slow-preload.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<h1>This page should never render</h1>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
preload() {
|
||||
return new Promise(fulfil => {
|
||||
window.fulfil = fulfil;
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
24
test/app/server.js
Normal file
24
test/app/server.js
Normal file
@@ -0,0 +1,24 @@
|
||||
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}`);
|
||||
});
|
||||
37
test/app/templates/2xx.html
Normal file
37
test/app/templates/2xx.html
Normal file
@@ -0,0 +1,37 @@
|
||||
<!doctype>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta name='viewport' content='width=device-width'>
|
||||
<meta name='theme-color' content='#aa1e1e'>
|
||||
|
||||
<link rel='stylesheet' href='/global.css'>
|
||||
<link rel='manifest' href='/manifest.json'>
|
||||
<link rel='icon' type='image/png' href='/favicon.png'>
|
||||
|
||||
<script>
|
||||
// if ('serviceWorker' in navigator) {
|
||||
// navigator.serviceWorker.register('/service-worker.js');
|
||||
// }
|
||||
</script>
|
||||
|
||||
<!-- Sapper generates a <style> tag containing critical CSS
|
||||
for the current page. CSS for the rest of the app is
|
||||
lazily loaded when it precaches secondary pages -->
|
||||
%sapper.styles%
|
||||
|
||||
<!-- This contains the contents of the <:Head> component, if
|
||||
the current page has one -->
|
||||
%sapper.head%
|
||||
</head>
|
||||
<body>
|
||||
<!-- The application will be rendered inside this element,
|
||||
because `templates/main.js` references it -->
|
||||
<div id='sapper'>%sapper.html%</div>
|
||||
|
||||
<!-- Sapper creates a <script> tag containing `templates/main.js`
|
||||
and anything else it needs to hydrate the app and
|
||||
initialise the router -->
|
||||
<script src='%sapper.main%'></script>
|
||||
</body>
|
||||
</html>
|
||||
44
test/app/templates/4xx.html
Normal file
44
test/app/templates/4xx.html
Normal file
@@ -0,0 +1,44 @@
|
||||
<!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>
|
||||
47
test/app/templates/5xx.html
Normal file
47
test/app/templates/5xx.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<!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>
|
||||
6
test/app/templates/main.js
Normal file
6
test/app/templates/main.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { init } from '../../../runtime.js';
|
||||
|
||||
// `routes` is an array of route objects injected by Sapper
|
||||
init(document.querySelector('#sapper'), __routes__);
|
||||
|
||||
window.READY = true;
|
||||
77
test/app/templates/service-worker.js
Normal file
77
test/app/templates/service-worker.js
Normal file
@@ -0,0 +1,77 @@
|
||||
const ASSETS = `cache__timestamp__`;
|
||||
|
||||
// `shell` is an array of all the files generated by webpack,
|
||||
// `assets` is an array of everything in the `assets` directory
|
||||
const to_cache = __shell__.concat(__assets__);
|
||||
const cached = new Set(to_cache);
|
||||
|
||||
// `routes` is an array of `{ pattern: RegExp }` objects that
|
||||
// match the pages in your app
|
||||
const routes = __routes__;
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.open(ASSETS)
|
||||
.then(cache => cache.addAll(to_cache))
|
||||
.then(() => {
|
||||
self.skipWaiting();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
caches.keys().then(async keys => {
|
||||
// delete old caches
|
||||
for (const key of keys) {
|
||||
if (key !== ASSETS) await caches.delete(key);
|
||||
}
|
||||
|
||||
await self.clients.claim();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
// don't try to handle e.g. data: URIs
|
||||
if (!url.protocol.startsWith('http')) return;
|
||||
|
||||
// always serve assets and webpack-generated files from cache
|
||||
if (cached.has(url.pathname)) {
|
||||
event.respondWith(caches.match(event.request));
|
||||
return;
|
||||
}
|
||||
|
||||
// for pages, you might want to serve a shell `index.html` file,
|
||||
// which Sapper has generated for you. It's not right for every
|
||||
// app, but if it's right for yours then uncomment this section
|
||||
/*
|
||||
if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) {
|
||||
event.respondWith(caches.match('/index.html'));
|
||||
return;
|
||||
}
|
||||
*/
|
||||
|
||||
// for everything else, try the network first, falling back to
|
||||
// cache if the user is offline. (If the pages never change, you
|
||||
// might prefer a cache-first approach to a network-first one.)
|
||||
event.respondWith(
|
||||
caches
|
||||
.open('offline')
|
||||
.then(async cache => {
|
||||
try {
|
||||
const response = await fetch(event.request);
|
||||
cache.put(event.request, response.clone());
|
||||
return response;
|
||||
} catch(err) {
|
||||
const response = await cache.match(event.request);
|
||||
if (response) return response;
|
||||
|
||||
throw err;
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
39
test/app/webpack.client.config.js
Normal file
39
test/app/webpack.client.config.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const config = require('../../webpack/config.js');
|
||||
const webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
entry: config.client.entry(),
|
||||
output: config.client.output(),
|
||||
resolve: {
|
||||
extensions: ['.js', '.html']
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.html$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'svelte-loader',
|
||||
options: {
|
||||
hydratable: true,
|
||||
emitCss: !config.dev,
|
||||
cascade: false,
|
||||
store: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
{ loader: "style-loader" },
|
||||
{ loader: "css-loader" }
|
||||
]
|
||||
}
|
||||
].filter(Boolean)
|
||||
},
|
||||
plugins: [
|
||||
config.dev && new webpack.HotModuleReplacementPlugin(),
|
||||
!config.dev && new webpack.optimize.ModuleConcatenationPlugin()
|
||||
].filter(Boolean),
|
||||
devtool: config.dev ? 'inline-source-map' : false
|
||||
};
|
||||
27
test/app/webpack.server.config.js
Normal file
27
test/app/webpack.server.config.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const config = require('../../webpack/config.js');
|
||||
|
||||
module.exports = {
|
||||
entry: config.server.entry(),
|
||||
output: config.server.output(),
|
||||
target: 'node',
|
||||
resolve: {
|
||||
extensions: ['.js', '.html']
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.html$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'svelte-loader',
|
||||
options: {
|
||||
css: false,
|
||||
cascade: false,
|
||||
store: true,
|
||||
generate: 'ssr'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
311
test/common/test.js
Normal file
311
test/common/test.js
Normal file
@@ -0,0 +1,311 @@
|
||||
const path = require('path');
|
||||
const assert = require('assert');
|
||||
const express = require('express');
|
||||
const serve = require('serve-static');
|
||||
const Nightmare = require('nightmare');
|
||||
const getPort = require('get-port');
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
run('production');
|
||||
run('development');
|
||||
|
||||
function run(env) {
|
||||
describe(`env=${env}`, function () {
|
||||
this.timeout(20000);
|
||||
|
||||
let PORT;
|
||||
let server;
|
||||
let nightmare;
|
||||
let middleware;
|
||||
let capture;
|
||||
|
||||
let base;
|
||||
|
||||
function get(url) {
|
||||
return new Promise(fulfil => {
|
||||
const req = {
|
||||
url,
|
||||
method: 'GET'
|
||||
};
|
||||
|
||||
const result = {
|
||||
headers: {},
|
||||
body: ''
|
||||
};
|
||||
|
||||
const res = {
|
||||
set: (headers, value) => {
|
||||
if (typeof headers === 'string') {
|
||||
return res.set({ [headers]: value });
|
||||
}
|
||||
|
||||
Object.assign(result.headers, headers);
|
||||
},
|
||||
|
||||
status: code => {
|
||||
result.status = code;
|
||||
},
|
||||
|
||||
write: data => {
|
||||
result.body += data;
|
||||
},
|
||||
|
||||
end: data => {
|
||||
result.body += data;
|
||||
fulfil(result);
|
||||
}
|
||||
};
|
||||
|
||||
middleware(req, res, () => {
|
||||
fulfil(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
process.chdir(path.resolve(__dirname, '../app'));
|
||||
|
||||
process.env.NODE_ENV = env;
|
||||
|
||||
if (env === 'production') {
|
||||
const cli = path.resolve(__dirname, '../../cli/index.js');
|
||||
await exec(`${cli} build`);
|
||||
}
|
||||
|
||||
const resolved = require.resolve('../..');
|
||||
delete require.cache[resolved];
|
||||
const sapper = require(resolved);
|
||||
|
||||
PORT = await getPort();
|
||||
base = `http://localhost:${PORT}`;
|
||||
|
||||
global.fetch = (url, opts) => {
|
||||
if (url[0] === '/') url = `${base}${url}`;
|
||||
return fetch(url, opts);
|
||||
};
|
||||
|
||||
let captured;
|
||||
capture = async fn => {
|
||||
const result = captured = [];
|
||||
await fn();
|
||||
captured = null;
|
||||
return result;
|
||||
};
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(serve('assets'));
|
||||
|
||||
app.use((req, res, next) => {
|
||||
if (captured) captured.push(req);
|
||||
next();
|
||||
});
|
||||
|
||||
middleware = sapper();
|
||||
app.use(middleware);
|
||||
|
||||
return new Promise((fulfil, reject) => {
|
||||
server = app.listen(PORT, err => {
|
||||
if (err) reject(err);
|
||||
else fulfil();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
server.close();
|
||||
middleware.close();
|
||||
|
||||
// give a chance to clean up
|
||||
return new Promise(fulfil => setTimeout(fulfil, 500));
|
||||
});
|
||||
|
||||
describe('basic functionality', () => {
|
||||
beforeEach(() => {
|
||||
nightmare = new Nightmare();
|
||||
|
||||
nightmare.on('console', (type, ...args) => {
|
||||
console[type](...args);
|
||||
});
|
||||
|
||||
nightmare.on('page', (type, ...args) => {
|
||||
if (type === 'error') {
|
||||
console.error(args[1]);
|
||||
} else {
|
||||
console.warn(type, args);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await nightmare.end();
|
||||
});
|
||||
|
||||
it('serves /', async () => {
|
||||
const title = await nightmare
|
||||
.goto(base)
|
||||
.evaluate(() => document.querySelector('h1').textContent);
|
||||
|
||||
assert.equal(title, 'Great success!');
|
||||
});
|
||||
|
||||
it('serves static route', async () => {
|
||||
const title = await nightmare
|
||||
.goto(`${base}/about`)
|
||||
.evaluate(() => document.querySelector('h1').textContent);
|
||||
|
||||
assert.equal(title, 'About this site');
|
||||
});
|
||||
|
||||
it('serves dynamic route', async () => {
|
||||
const title = await nightmare
|
||||
.goto(`${base}/blog/what-is-sapper`)
|
||||
.evaluate(() => document.querySelector('h1').textContent);
|
||||
|
||||
assert.equal(title, 'What is Sapper?');
|
||||
});
|
||||
|
||||
it('navigates to a new page without reloading', async () => {
|
||||
await nightmare.goto(base).wait(() => window.READY).wait(100);
|
||||
|
||||
const requests = await capture(async () => {
|
||||
await nightmare.click('a[href="/about"]');
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
await nightmare.path(),
|
||||
'/about'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
await nightmare.evaluate(() => document.title),
|
||||
'About'
|
||||
);
|
||||
|
||||
assert.deepEqual(requests.map(r => r.url), []);
|
||||
});
|
||||
|
||||
it('navigates programmatically', async () => {
|
||||
await nightmare
|
||||
.goto(`${base}/about`)
|
||||
.wait(() => window.READY)
|
||||
.click('button')
|
||||
.wait(() => window.location.pathname === '/blog/what-is-sapper')
|
||||
.wait(100);
|
||||
|
||||
assert.equal(
|
||||
await nightmare.evaluate(() => document.title),
|
||||
'What is Sapper?'
|
||||
);
|
||||
});
|
||||
|
||||
it('scrolls to active deeplink', async () => {
|
||||
const scrollY = await nightmare
|
||||
.goto(`${base}/blog/a-very-long-post#four`)
|
||||
.wait(() => window.READY)
|
||||
.wait(100)
|
||||
.evaluate(() => window.scrollY);
|
||||
|
||||
assert.ok(scrollY > 0, scrollY);
|
||||
});
|
||||
|
||||
it('reuses prefetch promise', async () => {
|
||||
await nightmare
|
||||
.goto(`${base}/blog`)
|
||||
.wait(() => window.READY)
|
||||
.wait(200);
|
||||
|
||||
const mouseover_requests = (await capture(async () => {
|
||||
await nightmare
|
||||
.mouseover('[href="/blog/what-is-sapper"]')
|
||||
.wait(200);
|
||||
})).map(r => r.url);
|
||||
|
||||
assert.deepEqual(mouseover_requests, [
|
||||
'/api/blog/what-is-sapper'
|
||||
]);
|
||||
|
||||
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 () => {
|
||||
await nightmare
|
||||
.goto(base)
|
||||
.wait(() => window.READY)
|
||||
.click('a[href="/slow-preload"]')
|
||||
.wait(100)
|
||||
.click('a[href="/about"]')
|
||||
.wait(100);
|
||||
|
||||
assert.equal(
|
||||
await nightmare.path(),
|
||||
'/about'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
await nightmare.evaluate(() => document.querySelector('h1').textContent),
|
||||
'About this site'
|
||||
);
|
||||
|
||||
await nightmare
|
||||
.evaluate(() => window.fulfil({}))
|
||||
.wait(100);
|
||||
|
||||
assert.equal(
|
||||
await nightmare.path(),
|
||||
'/about'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
await nightmare.evaluate(() => document.querySelector('h1').textContent),
|
||||
'About this site'
|
||||
);
|
||||
});
|
||||
|
||||
it('passes entire request object to preload', async () => {
|
||||
const html = await nightmare
|
||||
.goto(`${base}/show-url`)
|
||||
.evaluate(() => document.querySelector('p').innerHTML);
|
||||
|
||||
assert.equal(html, `URL is /show-url`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('headers', () => {
|
||||
it('sets Content-Type and Link...preload headers', async () => {
|
||||
const { headers } = await get('/');
|
||||
|
||||
assert.equal(
|
||||
headers['Content-Type'],
|
||||
'text/html'
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
/<\/client\/main.\w+\.js>;rel="preload";as="script", <\/client\/_.\d+.\w+.js>;rel="preload";as="script"/.test(headers['Link']),
|
||||
headers['Link']
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function exec(cmd) {
|
||||
return new Promise((fulfil, reject) => {
|
||||
require('child_process').exec(cmd, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
process.stdout.write(stdout);
|
||||
process.stderr.write(stderr);
|
||||
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
fulfil();
|
||||
});
|
||||
});
|
||||
}
|
||||
114
test/unit/create_routes.test.js
Normal file
114
test/unit/create_routes.test.js
Normal file
@@ -0,0 +1,114 @@
|
||||
const path = require('path');
|
||||
const assert = require('assert');
|
||||
|
||||
const create_routes = require('../../lib/utils/create_routes.js');
|
||||
|
||||
describe('create_routes', () => {
|
||||
it('sorts routes correctly', () => {
|
||||
const routes = create_routes(['index.html', 'about.html', '[wildcard].html', 'post/foo.html', 'post/[id].html', 'post/bar.html']);
|
||||
|
||||
assert.deepEqual(
|
||||
routes.map(r => r.file),
|
||||
[
|
||||
'index.html',
|
||||
'about.html',
|
||||
'post/foo.html',
|
||||
'post/bar.html',
|
||||
'post/[id].html',
|
||||
'[wildcard].html'
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it('generates params', () => {
|
||||
const routes = create_routes(['index.html', 'about.html', '[wildcard].html', 'post/[id].html']);
|
||||
|
||||
let file;
|
||||
let params;
|
||||
for (let i = 0; i < routes.length; i += 1) {
|
||||
const route = routes[i];
|
||||
if (params = route.exec('/post/123')) {
|
||||
file = route.file;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assert.equal(file, 'post/[id].html');
|
||||
assert.deepEqual(params, {
|
||||
id: '123'
|
||||
});
|
||||
});
|
||||
|
||||
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']);
|
||||
|
||||
assert.deepEqual(
|
||||
routes.map(r => r.file),
|
||||
[
|
||||
'index.html',
|
||||
'e/f/g/h.html'
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it('matches /foo/:bar before /:baz/qux', () => {
|
||||
const a = create_routes(['foo/[bar].html', '[baz]/qux.html']);
|
||||
const b = create_routes(['[baz]/qux.html', 'foo/[bar].html']);
|
||||
|
||||
assert.deepEqual(
|
||||
a.map(r => r.file),
|
||||
['foo/[bar].html', '[baz]/qux.html']
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
b.map(r => r.file),
|
||||
['foo/[bar].html', '[baz]/qux.html']
|
||||
);
|
||||
});
|
||||
|
||||
it('fails if routes are indistinguishable', () => {
|
||||
assert.throws(() => {
|
||||
create_routes(['[foo].html', '[bar]/index.html']);
|
||||
}, /The \[foo\].html and \[bar\]\/index.html routes clash/);
|
||||
});
|
||||
|
||||
it('matches nested routes', () => {
|
||||
const route = create_routes(['settings/[submenu].html'])[0];
|
||||
|
||||
assert.deepEqual(route.exec('/settings/foo'), {
|
||||
submenu: 'foo'
|
||||
});
|
||||
|
||||
assert.deepEqual(route.exec('/settings'), {
|
||||
submenu: null
|
||||
});
|
||||
});
|
||||
|
||||
it('prefers index routes to nested routes', () => {
|
||||
const routes = create_routes(['settings/[submenu].html', 'settings.html']);
|
||||
|
||||
assert.deepEqual(
|
||||
routes.map(r => r.file),
|
||||
['settings.html', 'settings/[submenu].html']
|
||||
);
|
||||
});
|
||||
|
||||
it('matches deeply nested routes', () => {
|
||||
const route = create_routes(['settings/[a]/[b]/index.html'])[0];
|
||||
|
||||
assert.deepEqual(route.exec('/settings/foo/bar'), {
|
||||
a: 'foo',
|
||||
b: 'bar'
|
||||
});
|
||||
|
||||
assert.deepEqual(route.exec('/settings/foo'), {
|
||||
a: 'foo',
|
||||
b: null
|
||||
});
|
||||
|
||||
assert.deepEqual(route.exec('/settings'), {
|
||||
a: null,
|
||||
b: null
|
||||
});
|
||||
});
|
||||
});
|
||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"diagnostics": true,
|
||||
"noImplicitThis": true,
|
||||
"noImplicitAny": true,
|
||||
"noEmitOnError": true,
|
||||
"allowJs": true,
|
||||
"lib": ["es5", "es6", "dom"],
|
||||
"importHelpers": true
|
||||
},
|
||||
"target": "ES5",
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
const path = require('path');
|
||||
const route_manager = require('../lib/route_manager.js');
|
||||
const { src, dest, dev } = require('../lib/config.js');
|
||||
const { dest, dev, entry } = require('../lib/config.js');
|
||||
|
||||
module.exports = {
|
||||
dev,
|
||||
@@ -8,7 +6,7 @@ module.exports = {
|
||||
client: {
|
||||
entry: () => {
|
||||
return {
|
||||
main: `${dest}/main.js`
|
||||
main: entry.client
|
||||
};
|
||||
},
|
||||
|
||||
@@ -16,7 +14,7 @@ module.exports = {
|
||||
return {
|
||||
path: `${dest}/client`,
|
||||
filename: '[name].[hash].js',
|
||||
chunkFilename: '[name].[id].js',
|
||||
chunkFilename: '[name].[id].[hash].js',
|
||||
publicPath: '/client/'
|
||||
};
|
||||
}
|
||||
@@ -24,18 +22,16 @@ module.exports = {
|
||||
|
||||
server: {
|
||||
entry: () => {
|
||||
const entries = {};
|
||||
route_manager.routes.forEach(route => {
|
||||
entries[route.id] = path.resolve(src, route.file);
|
||||
});
|
||||
return entries;
|
||||
return {
|
||||
main: entry.server
|
||||
};
|
||||
},
|
||||
|
||||
output: () => {
|
||||
return {
|
||||
path: `${dest}/server`,
|
||||
filename: '[name].[hash].js',
|
||||
chunkFilename: '[name].[id].js',
|
||||
chunkFilename: '[name].[id].[hash].js',
|
||||
libraryTarget: 'commonjs2'
|
||||
};
|
||||
}
|
||||
|
||||
1
webpack/hmr.js
Normal file
1
webpack/hmr.js
Normal file
@@ -0,0 +1 @@
|
||||
import 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000';
|
||||
Reference in New Issue
Block a user