mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-14 12:04:39 +00:00
Compare commits
121 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b1b545194 | ||
|
|
7b01242f3e | ||
|
|
15b1fbf8a6 | ||
|
|
8f1d2e0a04 | ||
|
|
dfb8692d78 | ||
|
|
09d3c4d85e | ||
|
|
1e623dde29 | ||
|
|
5104abf329 | ||
|
|
6554fc8616 | ||
|
|
cd01b7e6db | ||
|
|
bfa3da6d3d | ||
|
|
6ee092f8d4 | ||
|
|
ac70004f77 | ||
|
|
3449f1eb37 | ||
|
|
16cb1fccc6 | ||
|
|
b20c1c029f | ||
|
|
7abfb1aab1 | ||
|
|
205c2defe4 | ||
|
|
09a6eec83e | ||
|
|
2cabf61ea7 | ||
|
|
71cfdd2907 | ||
|
|
297f4276de | ||
|
|
422e31e183 | ||
|
|
b53ee061c0 | ||
|
|
8bad37205d | ||
|
|
fd0dd4fe58 | ||
|
|
4940644ae3 | ||
|
|
fb8d952eeb | ||
|
|
fc631c4866 | ||
|
|
03ce2ea998 | ||
|
|
dd8deb2d8a | ||
|
|
7d721abb2a | ||
|
|
39b1fa89ce | ||
|
|
7a3506420f | ||
|
|
72ae4a1c64 | ||
|
|
a09c33d6a5 | ||
|
|
4590aa313c | ||
|
|
d11bd954e0 | ||
|
|
c15959710b | ||
|
|
bb8ff74f68 | ||
|
|
2cbbe91490 | ||
|
|
faeddd8add | ||
|
|
d77722c042 | ||
|
|
61daba7a64 | ||
|
|
54ff8cc2e6 | ||
|
|
e6fcafe09b | ||
|
|
a305d3cea1 | ||
|
|
75e70207b8 | ||
|
|
8a8526d9ed | ||
|
|
9a76229bb6 | ||
|
|
f4e46e6e6c | ||
|
|
90cd347112 | ||
|
|
5adfdd6fe0 | ||
|
|
a6dc61a182 | ||
|
|
96666d05ec | ||
|
|
6390ba692b | ||
|
|
0e131cc81e | ||
|
|
bd3d5713cb | ||
|
|
9ec23c47ad | ||
|
|
b7bb69925e | ||
|
|
25124f6ee7 | ||
|
|
73d491cd19 | ||
|
|
e25fceb4b8 | ||
|
|
3807147c57 | ||
|
|
a523ba58ff | ||
|
|
fe03fd3a52 | ||
|
|
89c430a0cb | ||
|
|
8ef312849c | ||
|
|
4200446684 | ||
|
|
681ed005b8 | ||
|
|
d457af8d51 | ||
|
|
0c158b9e1f | ||
|
|
50011e2077 | ||
|
|
f27b7973e3 | ||
|
|
2af2ab3cb9 | ||
|
|
6a4dc1901c | ||
|
|
fbbc0e9e19 | ||
|
|
1213c3da46 | ||
|
|
4cc2104088 | ||
|
|
d6dda371ca | ||
|
|
304c06085e | ||
|
|
33b6450e34 | ||
|
|
8faa98af6a | ||
|
|
14df138528 | ||
|
|
44285cdb2f | ||
|
|
bd656cfd5b | ||
|
|
c4b4bd587d | ||
|
|
2abfdb03d5 | ||
|
|
a80ac3a8b8 | ||
|
|
887cb09386 | ||
|
|
cfeeafded4 | ||
|
|
2cae674033 | ||
|
|
7c0f32662d | ||
|
|
b4fb1c3268 | ||
|
|
ecd0f673a9 | ||
|
|
40d16852f7 | ||
|
|
133be03791 | ||
|
|
727a76ebb5 | ||
|
|
e3c047831a | ||
|
|
81b5e0d764 | ||
|
|
98e904dcfc | ||
|
|
ca51372150 | ||
|
|
7cef1f1120 | ||
|
|
1b73baabce | ||
|
|
5aa01b922b | ||
|
|
f0bc68be88 | ||
|
|
be7c53becc | ||
|
|
9ea4137b87 | ||
|
|
7588911108 | ||
|
|
fc8280adea | ||
|
|
d08f9eb5a4 | ||
|
|
2b3472b1b1 | ||
|
|
30ddb3dd7e | ||
|
|
0c891ba79e | ||
|
|
ee94f355d5 | ||
|
|
bea9b7965a | ||
|
|
1312aede1f | ||
|
|
50e307e0c0 | ||
|
|
e87ac1f367 | ||
|
|
5da9d0926a | ||
|
|
9538499d51 |
12
.gitignore
vendored
12
.gitignore
vendored
@@ -1,5 +1,15 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
yarn.lock
|
||||||
node_modules
|
node_modules
|
||||||
cypress/screenshots
|
cypress/screenshots
|
||||||
test/app/.sapper
|
test/app/.sapper
|
||||||
runtime.js
|
runtime.js
|
||||||
|
runtime.js.map
|
||||||
|
cli.js
|
||||||
|
cli.js.map
|
||||||
|
middleware.js
|
||||||
|
middleware.js.map
|
||||||
|
core.js
|
||||||
|
core.js.map
|
||||||
|
webpack/config.js
|
||||||
|
webpack/config.js.map
|
||||||
@@ -3,6 +3,7 @@ sudo: false
|
|||||||
language: node_js
|
language: node_js
|
||||||
|
|
||||||
node_js:
|
node_js:
|
||||||
|
- "6"
|
||||||
- "stable"
|
- "stable"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
|||||||
49
CHANGELOG.md
49
CHANGELOG.md
@@ -1,5 +1,54 @@
|
|||||||
# sapper changelog
|
# sapper changelog
|
||||||
|
|
||||||
|
## 0.6.3
|
||||||
|
|
||||||
|
* Ignore non-HTML responses when crawling during `export`
|
||||||
|
* Build in prod mode for `export`
|
||||||
|
|
||||||
|
## 0.6.2
|
||||||
|
|
||||||
|
* Handle unspecified type in `sapper export`
|
||||||
|
|
||||||
|
## 0.6.1
|
||||||
|
|
||||||
|
* Fix `pkg.files` and `pkg.bin`
|
||||||
|
|
||||||
|
## 0.6.0
|
||||||
|
|
||||||
|
* Hydrate on first load, and only on first load ([#93](https://github.com/sveltejs/sapper/pull/93))
|
||||||
|
* Identify clashes between page and server routes ([#96](https://github.com/sveltejs/sapper/pull/96))
|
||||||
|
* Remove Express-specific utilities, for compatbility with Polka et al ([#94](https://github.com/sveltejs/sapper/issues/94))
|
||||||
|
* Return a promise from `init` when first page has rendered ([#99](https://github.com/sveltejs/sapper/issues/99))
|
||||||
|
* Handle invalid hash links ([#104](https://github.com/sveltejs/sapper/pull/104))
|
||||||
|
* Avoid `URLSearchParams` ([#107](https://github.com/sveltejs/sapper/pull/107))
|
||||||
|
* Don't automatically set `Content-Type` for server routes ([#111](https://github.com/sveltejs/sapper/pull/111))
|
||||||
|
* Handle empty query string routes, e.g. `/?` ([#105](https://github.com/sveltejs/sapper/pull/105))
|
||||||
|
|
||||||
|
## 0.5.1
|
||||||
|
|
||||||
|
* Only write service-worker.js to filesystem in dev mode ([#90](https://github.com/sveltejs/sapper/issues/90))
|
||||||
|
|
||||||
|
## 0.5.0
|
||||||
|
|
||||||
|
* Experimental support for `sapper export` ([#9](https://github.com/sveltejs/sapper/issues/9))
|
||||||
|
* Lazily load chokidar, for faster startup ([#64](https://github.com/sveltejs/sapper/pull/64))
|
||||||
|
|
||||||
|
## 0.4.0
|
||||||
|
|
||||||
|
* `%sapper.main%` has been replaced with `%sapper.scripts%` ([#86](https://github.com/sveltejs/sapper/issues/86))
|
||||||
|
* Node 6 support ([#67](https://github.com/sveltejs/sapper/pull/67))
|
||||||
|
* Explicitly load css-loader and style-loader ([#72](https://github.com/sveltejs/sapper/pull/72))
|
||||||
|
* DELETE requests are handled with `del` exports ([#77](https://github.com/sveltejs/sapper/issues/77))
|
||||||
|
* Send preloaded data for first route to client, where possible ([#3](https://github.com/sveltejs/sapper/issues/3))
|
||||||
|
|
||||||
|
## 0.3.2
|
||||||
|
|
||||||
|
* Expose `prefetch` function ([#61](https://github.com/sveltejs/sapper/pull/61))
|
||||||
|
|
||||||
|
## 0.3.1
|
||||||
|
|
||||||
|
* Fix missing `runtime.js`
|
||||||
|
|
||||||
## 0.3.0
|
## 0.3.0
|
||||||
|
|
||||||
* Move `sapper/runtime/app.js` to `sapper/runtime.js`
|
* Move `sapper/runtime/app.js` to `sapper/runtime.js`
|
||||||
|
|||||||
9
LICENSE
Normal file
9
LICENSE
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Copyright (c) 2017 [these people](https://github.com/sveltejs/sapper/graphs/contributors).
|
||||||
|
|
||||||
|
Permission is hereby granted by the authors of this software, to any person, to use the software for any purpose, free of charge, including the rights to run, read, copy, change, distribute and sell it, and including usage rights to any patents the authors may hold on it, subject to the following conditions:
|
||||||
|
|
||||||
|
This license, or a link to its text, must be included with all copies of the software and any derivative works.
|
||||||
|
|
||||||
|
Any modification to the software submitted to the authors may be incorporated into the software under the terms of this license.
|
||||||
|
|
||||||
|
The software is provided "as is", without warranty of any kind, including but not limited to the warranties of title, fitness, merchantability and non-infringement. The authors have no obligation to provide support or updates for the software, and may not be held liable for any damages, claims or other liability arising from its use.
|
||||||
151
README.md
151
README.md
@@ -1,150 +1,37 @@
|
|||||||
# sapper
|
# sapper
|
||||||
|
|
||||||
Combat-ready apps, engineered by Svelte.
|
[Military-grade progressive web apps, powered by Svelte.](https://sapper.svelte.technology)
|
||||||
|
|
||||||
## This is not a thing yet
|
|
||||||
|
|
||||||
If you visit this README in a few weeks, hopefully it will have blossomed into the app development framework we deserve. Right now, it's just a set of ideas.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
[Next.js](https://github.com/zeit/next.js/) introduced a beautiful idea — that you should be able to build your app as universal React components in a special `pages` directory, and the framework should take care of routing and rendering on both client and server. What if we did the same thing for Svelte?
|
|
||||||
|
|
||||||
High-level goals:
|
|
||||||
|
|
||||||
* Extreme ease of development
|
|
||||||
* Code-splitting and HMR out of the box (probably via webpack)
|
|
||||||
* Best-in-class performance
|
|
||||||
* As little magic as possible. Anyone should be able to understand how everything fits together, and e.g. make changes to the webpack config
|
|
||||||
* Links are just `<a>` tags, no special `<Link>` components
|
|
||||||
|
|
||||||
|
|
||||||
## Design
|
## What is Sapper?
|
||||||
|
|
||||||
A Sapper app is just an Express app (conventionally, `server.js`) that uses the `sapper` middleware:
|
Sapper is a framework for building high-performance universal web apps. [Read the guide](https://sapper.svelte.technology/guide) or the [introductory blog post](https://svelte.technology/blog/sapper-towards-the-ideal-web-app-framework) to learn more.
|
||||||
|
|
||||||
```js
|
|
||||||
const app = require('express')();
|
|
||||||
const sapper = require('sapper');
|
|
||||||
|
|
||||||
app.use(sapper());
|
## Get started
|
||||||
|
|
||||||
const { PORT = 3000 } = process.env;
|
Clone the [starter project template](https://github.com/sveltejs/sapper-template) with [degit](https://github.com/rich-harris/degit)...
|
||||||
app.listen(PORT, () => {
|
|
||||||
console.log(`listening on port ${PORT}`);
|
```bash
|
||||||
});
|
npx degit sveltejs/sapper-template my-app
|
||||||
```
|
```
|
||||||
|
|
||||||
The middleware serves pages that match files in the `routes` directory, and assets generated by webpack. In development mode, the middleware once activated watches `routes` to keep the app up-to-date.
|
...then install dependencies and start the dev server...
|
||||||
|
|
||||||
|
```bash
|
||||||
## Routing
|
cd my-app
|
||||||
|
npm install
|
||||||
Like Next, routes are defined by the project directory structure, but with some crucial differences:
|
npm run dev
|
||||||
|
|
||||||
* Files with an `.html` extension are treated as Svelte components. The `routes/about.html` (or `routes/about/index.html`) would create the `/about` route.
|
|
||||||
* Files with a `.js` or `.mjs` extension are more generic route handlers. These files should export functions corresponding to the HTTP methods they support (example below).
|
|
||||||
* Instead of route masking, we embed parameters in the filename. For example `post/[id].html` maps to `/post/:id`, and the component will be rendered with the appropriate parameter.
|
|
||||||
* Nested routes (read [this article](https://joshduff.com/2015-06-why-you-need-a-state-router.md)) can be handled by creating a file that matches the subroute — for example, `routes/app/settings/[submenu].html` would match `/app/settings/profile` *and* `app/settings`, but in the latter case the `submenu` parameter would be `null`.
|
|
||||||
|
|
||||||
An example of a generic route:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// routes/api/post/[id].js
|
|
||||||
export async function get(req, res) {
|
|
||||||
try {
|
|
||||||
const data = await getPostFromDatabase(req.params.id);
|
|
||||||
const json = JSON.stringify(data);
|
|
||||||
|
|
||||||
res.set({
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Content-Length': json.length
|
|
||||||
});
|
|
||||||
|
|
||||||
res.send(json);
|
|
||||||
} catch (err) {
|
|
||||||
res.status(500).send(err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Or, if you omit the `res` argument, it can use the return value:
|
...and navigate to [localhost:3000](http://localhost:3000). To build and run in production mode:
|
||||||
|
|
||||||
```js
|
```bash
|
||||||
// routes/api/post/[id].js
|
npm run build
|
||||||
export async function get(req) {
|
npm start
|
||||||
return await getPostFromDatabase(req.params.id);
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Client-side app
|
## License
|
||||||
|
|
||||||
Sapper will create (and in development mode, update) a barebones `main.js` file that dynamically imports individual routes and renders them — something like this:
|
[LIL](LICENSE)
|
||||||
|
|
||||||
```js
|
|
||||||
window.addEventListener('click', event => {
|
|
||||||
let a = event.target;
|
|
||||||
while (a && a.nodeName !== 'A') a = a.parentNode;
|
|
||||||
if (!a) return;
|
|
||||||
|
|
||||||
if (navigate(new URL(a.href))) event.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
const target = document.querySelector('#sapper');
|
|
||||||
let component;
|
|
||||||
|
|
||||||
function navigate(url) {
|
|
||||||
if (url.origin !== window.location.origin) return;
|
|
||||||
|
|
||||||
let match;
|
|
||||||
let params = {};
|
|
||||||
const query = {};
|
|
||||||
|
|
||||||
function render(mod) {
|
|
||||||
if (component) {
|
|
||||||
component.destroy();
|
|
||||||
} else {
|
|
||||||
target.innerHTML = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
component = new mod.default({
|
|
||||||
target,
|
|
||||||
data: { query, params },
|
|
||||||
hydrate: !!component
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.pathname === '/about') {
|
|
||||||
import('/about/index.html').then(render);
|
|
||||||
} else if (url.pathname === '/') {
|
|
||||||
import('/index.js').then(render);
|
|
||||||
} else if (match = /^\/post\/([^\/]+)$/.exec(url.pathname)) {
|
|
||||||
params.id = match[1];
|
|
||||||
import('/post/[id].html').then(render);
|
|
||||||
} else if (match = /^\/([^\/]+)$/.exec(url.pathname)) {
|
|
||||||
params.wildcard = match[1];
|
|
||||||
import('/[wildcard].html').then(render);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate(window.location);
|
|
||||||
```
|
|
||||||
|
|
||||||
We're glossing over a lot of important stuff here — e.g. handling `popstate` — but you get the idea. Knowledge of all the possible routes means we can generate optimal code, much in the same way that statically analysing Svelte templates allows the compiler to generate optimal code.
|
|
||||||
|
|
||||||
|
|
||||||
## Things to figure out
|
|
||||||
|
|
||||||
* How to customise the overall page template
|
|
||||||
* An equivalent of `getInitialProps`
|
|
||||||
* Critical CSS
|
|
||||||
* `store` integration
|
|
||||||
* Route transitions
|
|
||||||
* Equivalent of `next export`
|
|
||||||
* A good story for realtime/GraphQL stuff
|
|
||||||
* Service worker
|
|
||||||
* Using `Link...rel=preload` headers to push main.js/[route].js plus styles
|
|
||||||
* ...and lots of other things that haven't occurred to me yet.
|
|
||||||
@@ -15,3 +15,7 @@ environment:
|
|||||||
install:
|
install:
|
||||||
- ps: Install-Product node $env:nodejs_version
|
- ps: Install-Product node $env:nodejs_version
|
||||||
- npm install
|
- npm install
|
||||||
|
|
||||||
|
test_script:
|
||||||
|
- node --version && npm --version
|
||||||
|
- npm test
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const cmd = process.argv[2];
|
|
||||||
|
|
||||||
if (cmd === 'build') {
|
|
||||||
process.env.NODE_ENV = 'production';
|
|
||||||
require('../lib/build.js')();
|
|
||||||
}
|
|
||||||
42
lib/build.js
42
lib/build.js
@@ -1,42 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const mkdirp = require('mkdirp');
|
|
||||||
const rimraf = require('rimraf');
|
|
||||||
const { client, server } = require('./utils/compilers.js');
|
|
||||||
const create_app = require('./utils/create_app.js');
|
|
||||||
const generate_asset_cache = require('./utils/generate_asset_cache.js');
|
|
||||||
const { dest } = require('./config.js');
|
|
||||||
|
|
||||||
module.exports = () => {
|
|
||||||
mkdirp.sync(dest);
|
|
||||||
rimraf.sync(path.join(dest, '**/*'));
|
|
||||||
|
|
||||||
// create main.js and server-routes.js
|
|
||||||
create_app();
|
|
||||||
|
|
||||||
function handleErrors(err, stats) {
|
|
||||||
if (err) {
|
|
||||||
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stats.hasErrors()) {
|
|
||||||
console.log(stats.toString({ colors: true }));
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
client.run((err, clientStats) => {
|
|
||||||
handleErrors(err, clientStats);
|
|
||||||
const clientInfo = clientStats.toJson();
|
|
||||||
fs.writeFileSync(path.join(dest, 'stats.client.json'), JSON.stringify(clientInfo, null, ' '));
|
|
||||||
|
|
||||||
server.run((err, serverStats) => {
|
|
||||||
handleErrors(err, serverStats);
|
|
||||||
const serverInfo = serverStats.toJson();
|
|
||||||
fs.writeFileSync(path.join(dest, 'stats.server.json'), JSON.stringify(serverInfo, null, ' '));
|
|
||||||
|
|
||||||
generate_asset_cache(clientInfo, serverInfo);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const mkdirp = require('mkdirp');
|
|
||||||
const rimraf = require('rimraf');
|
|
||||||
|
|
||||||
exports.dev = process.env.NODE_ENV !== 'production';
|
|
||||||
|
|
||||||
exports.templates = path.resolve(process.env.SAPPER_TEMPLATES || 'templates');
|
|
||||||
|
|
||||||
exports.src = path.resolve(process.env.SAPPER_ROUTES || 'routes');
|
|
||||||
|
|
||||||
exports.dest = path.resolve(process.env.SAPPER_DEST || '.sapper');
|
|
||||||
|
|
||||||
if (exports.dev) {
|
|
||||||
mkdirp.sync(exports.dest);
|
|
||||||
rimraf.sync(path.join(exports.dest, '**/*'));
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.entry = {
|
|
||||||
client: path.resolve(exports.templates, '.main.rendered.js'),
|
|
||||||
server: path.resolve(exports.dest, 'server-entry.js')
|
|
||||||
};
|
|
||||||
238
lib/index.js
238
lib/index.js
@@ -1,238 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const route_manager = require('./route_manager.js');
|
|
||||||
const templates = require('./templates.js');
|
|
||||||
const create_app = require('./utils/create_app.js');
|
|
||||||
const create_watcher = require('./utils/create_watcher.js');
|
|
||||||
const compilers = require('./utils/compilers.js');
|
|
||||||
const generate_asset_cache = require('./utils/generate_asset_cache.js');
|
|
||||||
const escape_html = require('escape-html');
|
|
||||||
const { dest, dev } = require('./config.js');
|
|
||||||
|
|
||||||
function connect_dev() {
|
|
||||||
create_app();
|
|
||||||
|
|
||||||
const watcher = create_watcher();
|
|
||||||
|
|
||||||
let asset_cache;
|
|
||||||
|
|
||||||
const middleware = compose_handlers([
|
|
||||||
require('webpack-hot-middleware')(compilers.client, {
|
|
||||||
reload: true,
|
|
||||||
path: '/__webpack_hmr',
|
|
||||||
heartbeat: 10 * 1000
|
|
||||||
}),
|
|
||||||
|
|
||||||
async (req, res, next) => {
|
|
||||||
asset_cache = await watcher.ready;
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
|
|
||||||
set_req_pathname,
|
|
||||||
|
|
||||||
get_asset_handler({
|
|
||||||
filter: pathname => pathname === '/index.html',
|
|
||||||
type: 'text/html',
|
|
||||||
cache: 'max-age=600',
|
|
||||||
fn: () => asset_cache.client.index
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_asset_handler({
|
|
||||||
filter: pathname => pathname === '/service-worker.js',
|
|
||||||
type: 'application/javascript',
|
|
||||||
cache: 'max-age=600',
|
|
||||||
fn: () => asset_cache.client.service_worker
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_asset_handler({
|
|
||||||
filter: pathname => pathname.startsWith('/client/'),
|
|
||||||
type: 'application/javascript',
|
|
||||||
cache: 'max-age=31536000',
|
|
||||||
fn: pathname => asset_cache.client.chunks[pathname]
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_route_handler(() => asset_cache),
|
|
||||||
|
|
||||||
get_not_found_handler(() => asset_cache)
|
|
||||||
]);
|
|
||||||
|
|
||||||
middleware.close = () => {
|
|
||||||
watcher.close();
|
|
||||||
// TODO shut down chokidar
|
|
||||||
};
|
|
||||||
|
|
||||||
return middleware;
|
|
||||||
}
|
|
||||||
|
|
||||||
function connect_prod() {
|
|
||||||
const asset_cache = generate_asset_cache(
|
|
||||||
read_json(path.join(dest, 'stats.client.json')),
|
|
||||||
read_json(path.join(dest, 'stats.server.json'))
|
|
||||||
);
|
|
||||||
|
|
||||||
const middleware = compose_handlers([
|
|
||||||
set_req_pathname,
|
|
||||||
|
|
||||||
get_asset_handler({
|
|
||||||
filter: pathname => pathname === '/index.html',
|
|
||||||
type: 'text/html',
|
|
||||||
cache: 'max-age=600',
|
|
||||||
fn: () => asset_cache.client.index
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_asset_handler({
|
|
||||||
filter: pathname => pathname === '/service-worker.js',
|
|
||||||
type: 'application/javascript',
|
|
||||||
cache: 'max-age=600',
|
|
||||||
fn: () => asset_cache.client.service_worker
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_asset_handler({
|
|
||||||
filter: pathname => pathname.startsWith('/client/'),
|
|
||||||
type: 'application/javascript',
|
|
||||||
cache: 'max-age=31536000',
|
|
||||||
fn: pathname => asset_cache.client.chunks[pathname]
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_route_handler(() => asset_cache),
|
|
||||||
|
|
||||||
get_not_found_handler(() => asset_cache)
|
|
||||||
]);
|
|
||||||
|
|
||||||
// here for API consistency between dev, and prod, but
|
|
||||||
// doesn't actually need to do anything
|
|
||||||
middleware.close = () => {};
|
|
||||||
|
|
||||||
return middleware;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = dev ? connect_dev : connect_prod;
|
|
||||||
|
|
||||||
function set_req_pathname(req, res, next) {
|
|
||||||
req.pathname = req.url.replace(/\?.+/, '');
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_asset_handler(opts) {
|
|
||||||
return (req, res, next) => {
|
|
||||||
if (!opts.filter(req.pathname)) return next();
|
|
||||||
|
|
||||||
res.set({
|
|
||||||
'Content-Type': opts.type,
|
|
||||||
'Cache-Control': opts.cache
|
|
||||||
});
|
|
||||||
res.end(opts.fn(req.pathname));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_route_handler(fn) {
|
|
||||||
return async function handle_route(req, res, next) {
|
|
||||||
const url = req.pathname;
|
|
||||||
|
|
||||||
const { client, server } = fn();
|
|
||||||
|
|
||||||
// whatever happens, we're going to serve some HTML
|
|
||||||
res.set({
|
|
||||||
'Content-Type': 'text/html'
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (const route of route_manager.routes) {
|
|
||||||
if (route.test(url)) {
|
|
||||||
req.params = route.exec(url);
|
|
||||||
|
|
||||||
const mod = require(server.entry)[route.id];
|
|
||||||
|
|
||||||
if (route.type === 'page') {
|
|
||||||
// preload main.js and current route
|
|
||||||
// TODO detect other stuff we can preload? images, CSS, fonts?
|
|
||||||
res.set('Link', `<${client.main_file}>;rel="preload";as="script", <${client.routes[route.id]}>;rel="preload";as="script"`);
|
|
||||||
|
|
||||||
const data = { params: req.params, query: req.query };
|
|
||||||
|
|
||||||
if (mod.preload) {
|
|
||||||
const promise = Promise.resolve(mod.preload(req)).then(preloaded => {
|
|
||||||
Object.assign(data, preloaded);
|
|
||||||
return mod.render(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
await templates.stream(res, 200, {
|
|
||||||
main: client.main_file,
|
|
||||||
html: promise.then(rendered => rendered.html),
|
|
||||||
head: promise.then(({ head }) => `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`),
|
|
||||||
styles: promise.then(({ css }) => (css && css.code ? `<style>${css.code}</style>` : ''))
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const { html, head, css } = mod.render(data);
|
|
||||||
|
|
||||||
const page = templates.render(200, {
|
|
||||||
main: client.main_file,
|
|
||||||
html,
|
|
||||||
head: `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`,
|
|
||||||
styles: (css && css.code ? `<style>${css.code}</style>` : '')
|
|
||||||
});
|
|
||||||
|
|
||||||
res.end(page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
const handler = mod[req.method.toLowerCase()];
|
|
||||||
if (handler) handler(req, res, next);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
} catch(err) {
|
|
||||||
res.status(500);
|
|
||||||
res.end(templates.render(500, {
|
|
||||||
title: (err && err.name) || 'Internal server error',
|
|
||||||
url,
|
|
||||||
error: escape_html(err && (err.details || err.message || err) || 'Unknown error'),
|
|
||||||
stack: err && err.stack.split('\n').slice(1).join('\n')
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_not_found_handler(fn) {
|
|
||||||
return function handle_not_found(req, res) {
|
|
||||||
const asset_cache = fn();
|
|
||||||
|
|
||||||
res.status(404);
|
|
||||||
res.end(templates.render(404, {
|
|
||||||
title: 'Not found',
|
|
||||||
status: 404,
|
|
||||||
method: req.method,
|
|
||||||
main: asset_cache.client.main_file,
|
|
||||||
url: req.url
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function compose_handlers(handlers) {
|
|
||||||
return (req, res, next) => {
|
|
||||||
let i = 0;
|
|
||||||
function go() {
|
|
||||||
const handler = handlers[i];
|
|
||||||
|
|
||||||
if (handler) {
|
|
||||||
handler(req, res, () => {
|
|
||||||
i += 1;
|
|
||||||
go();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
go();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function read_json(file) {
|
|
||||||
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
const glob = require('glob');
|
|
||||||
const chokidar = require('chokidar');
|
|
||||||
const create_routes = require('./utils/create_routes.js');
|
|
||||||
const { src, dev } = require('./config.js');
|
|
||||||
|
|
||||||
const callbacks = [];
|
|
||||||
|
|
||||||
exports.onchange = fn => {
|
|
||||||
callbacks.push(fn);
|
|
||||||
};
|
|
||||||
|
|
||||||
function update() {
|
|
||||||
exports.routes = create_routes(
|
|
||||||
glob.sync('**/*.+(html|js|mjs)', { cwd: src })
|
|
||||||
);
|
|
||||||
|
|
||||||
callbacks.forEach(fn => fn());
|
|
||||||
}
|
|
||||||
|
|
||||||
update();
|
|
||||||
|
|
||||||
if (dev) {
|
|
||||||
const watcher = chokidar.watch(`${src}/**/*.+(html|js|mjs)`, {
|
|
||||||
ignoreInitial: true,
|
|
||||||
persistent: false
|
|
||||||
});
|
|
||||||
|
|
||||||
watcher.on('add', update);
|
|
||||||
watcher.on('change', update);
|
|
||||||
watcher.on('unlink', update);
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const relative = require('require-relative');
|
|
||||||
const webpack = relative('webpack', process.cwd());
|
|
||||||
|
|
||||||
exports.client = webpack(
|
|
||||||
require(path.resolve('webpack.client.config.js'))
|
|
||||||
);
|
|
||||||
|
|
||||||
exports.server = webpack(
|
|
||||||
require(path.resolve('webpack.server.config.js'))
|
|
||||||
);
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const chokidar = require('chokidar');
|
|
||||||
const route_manager = require('../route_manager.js');
|
|
||||||
const { src, entry, dev } = require('../config.js');
|
|
||||||
|
|
||||||
function posixify(file) {
|
|
||||||
return file.replace(/[/\\]/g, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
function create_app() {
|
|
||||||
const { routes } = route_manager;
|
|
||||||
|
|
||||||
function create_client_main() {
|
|
||||||
const template = fs.readFileSync('templates/main.js', 'utf-8');
|
|
||||||
|
|
||||||
const code = `[${
|
|
||||||
routes
|
|
||||||
.filter(route => route.type === 'page')
|
|
||||||
.map(route => {
|
|
||||||
const params = route.dynamic.length === 0 ?
|
|
||||||
'{}' :
|
|
||||||
`{ ${route.dynamic.map((part, i) => `${part}: match[${i + 1}]`).join(', ') } }`;
|
|
||||||
|
|
||||||
const file = posixify(`${src}/${route.file}`);
|
|
||||||
return `{ pattern: ${route.pattern}, params: match => (${params}), load: () => import(/* webpackChunkName: "${route.id}" */ '${file}') }`
|
|
||||||
})
|
|
||||||
.join(', ')
|
|
||||||
}]`;
|
|
||||||
|
|
||||||
let main = template
|
|
||||||
.replace(/__app__/g, posixify(path.resolve(__dirname, '../../runtime/app.js')))
|
|
||||||
.replace(/__routes__/g, code)
|
|
||||||
.replace(/__dev__/g, String(dev));
|
|
||||||
|
|
||||||
if (dev) {
|
|
||||||
const hmr_client = posixify(require.resolve(`webpack-hot-middleware/client`));
|
|
||||||
main += `\n\nimport('${hmr_client}?path=/__webpack_hmr&timeout=20000'); if (module.hot) module.hot.accept();`
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(entry.client, main);
|
|
||||||
|
|
||||||
// need to fudge the mtime, because webpack is soft in the head
|
|
||||||
const { atime, mtime } = fs.statSync(entry.client);
|
|
||||||
fs.utimesSync(entry.client, new Date(atime.getTime() - 999999), new Date(mtime.getTime() - 999999));
|
|
||||||
}
|
|
||||||
|
|
||||||
function create_server_routes() {
|
|
||||||
const imports = routes
|
|
||||||
.map(route => {
|
|
||||||
const file = posixify(`${src}/${route.file}`);
|
|
||||||
return route.type === 'page' ?
|
|
||||||
`import ${route.id} from '${file}';` :
|
|
||||||
`import * as ${route.id} from '${file}';`;
|
|
||||||
})
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
const exports = `export { ${routes.map(route => route.id)} };`;
|
|
||||||
|
|
||||||
fs.writeFileSync(entry.server, `${imports}\n\n${exports}`);
|
|
||||||
|
|
||||||
const { atime, mtime } = fs.statSync(entry.server);
|
|
||||||
fs.utimesSync(entry.server, new Date(atime.getTime() - 999999), new Date(mtime.getTime() - 999999));
|
|
||||||
}
|
|
||||||
|
|
||||||
create_client_main();
|
|
||||||
create_server_routes();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dev) {
|
|
||||||
route_manager.onchange(create_app);
|
|
||||||
|
|
||||||
const watcher = chokidar.watch(`templates/main.js`, {
|
|
||||||
ignoreInitial: true,
|
|
||||||
persistent: false
|
|
||||||
});
|
|
||||||
|
|
||||||
watcher.on('add', create_app);
|
|
||||||
watcher.on('change', create_app);
|
|
||||||
watcher.on('unlink', create_app);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = create_app;
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
--require source-map-support/register
|
||||||
--recursive
|
--recursive
|
||||||
test/unit/**/*.js
|
test/unit/**/*.js
|
||||||
test/common/test.js
|
test/common/test.js
|
||||||
934
package-lock.json
generated
934
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
33
package.json
33
package.json
@@ -1,15 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "sapper",
|
"name": "sapper",
|
||||||
"version": "0.3.0",
|
"version": "0.6.3",
|
||||||
"description": "Military-grade apps, engineered by Svelte",
|
"description": "Military-grade apps, engineered by Svelte",
|
||||||
"main": "lib/index.js",
|
"main": "middleware.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"sapper": "cli/index.js"
|
"sapper": "cli.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"cli",
|
"cli.js",
|
||||||
"lib",
|
"core.js",
|
||||||
|
"middleware.js",
|
||||||
"runtime",
|
"runtime",
|
||||||
|
"runtime.js",
|
||||||
"webpack"
|
"webpack"
|
||||||
],
|
],
|
||||||
"directories": {
|
"directories": {
|
||||||
@@ -17,30 +19,44 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^2.3.0",
|
"chalk": "^2.3.0",
|
||||||
|
"cheerio": "^1.0.0-rc.2",
|
||||||
"chokidar": "^1.7.0",
|
"chokidar": "^1.7.0",
|
||||||
|
"code-frame": "^5.0.0",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
|
"express": "^4.16.2",
|
||||||
|
"glob": "^7.1.2",
|
||||||
|
"locate-character": "^2.0.5",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
|
"node-fetch": "^1.7.3",
|
||||||
"relative": "^3.0.2",
|
"relative": "^3.0.2",
|
||||||
"require-relative": "^0.8.7",
|
"require-relative": "^0.8.7",
|
||||||
"rimraf": "^2.6.2",
|
"rimraf": "^2.6.2",
|
||||||
|
"sander": "^0.6.0",
|
||||||
|
"serialize-javascript": "^1.4.0",
|
||||||
|
"url-parse": "^1.2.0",
|
||||||
|
"walk-sync": "^0.3.2",
|
||||||
"webpack": "^3.10.0",
|
"webpack": "^3.10.0",
|
||||||
"webpack-hot-middleware": "^2.21.0"
|
"webpack-hot-middleware": "^2.21.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@std/esm": "^0.19.7",
|
||||||
|
"@types/glob": "^5.0.34",
|
||||||
|
"@types/mkdirp": "^0.5.2",
|
||||||
|
"@types/rimraf": "^2.0.2",
|
||||||
"css-loader": "^0.28.7",
|
"css-loader": "^0.28.7",
|
||||||
"eslint": "^4.13.1",
|
"eslint": "^4.13.1",
|
||||||
"eslint-plugin-import": "^2.8.0",
|
"eslint-plugin-import": "^2.8.0",
|
||||||
"express": "^4.16.2",
|
|
||||||
"get-port": "^3.2.0",
|
"get-port": "^3.2.0",
|
||||||
"mocha": "^4.0.1",
|
"mocha": "^4.0.1",
|
||||||
"nightmare": "^2.10.0",
|
"nightmare": "^2.10.0",
|
||||||
"node-fetch": "^1.7.3",
|
|
||||||
"npm-run-all": "^4.1.2",
|
"npm-run-all": "^4.1.2",
|
||||||
"rollup": "^0.53.0",
|
"rollup": "^0.53.0",
|
||||||
"rollup-plugin-typescript": "^0.8.1",
|
"rollup-plugin-typescript": "^0.8.1",
|
||||||
|
"source-map-support": "^0.5.2",
|
||||||
"style-loader": "^0.19.1",
|
"style-loader": "^0.19.1",
|
||||||
"svelte": "^1.49.1",
|
"svelte": "^1.49.1",
|
||||||
"svelte-loader": "^2.3.2",
|
"svelte-loader": "^2.3.2",
|
||||||
|
"ts-node": "^4.1.0",
|
||||||
"tslib": "^1.8.1",
|
"tslib": "^1.8.1",
|
||||||
"typescript": "^2.6.2",
|
"typescript": "^2.6.2",
|
||||||
"wait-on": "^2.0.2"
|
"wait-on": "^2.0.2"
|
||||||
@@ -50,7 +66,8 @@
|
|||||||
"test": "mocha --opts mocha.opts",
|
"test": "mocha --opts mocha.opts",
|
||||||
"pretest": "npm run build",
|
"pretest": "npm run build",
|
||||||
"build": "rollup -c",
|
"build": "rollup -c",
|
||||||
"dev": "rollup -cw"
|
"dev": "rollup -cw",
|
||||||
|
"prepublish": "npm test"
|
||||||
},
|
},
|
||||||
"repository": "https://github.com/sveltejs/sapper",
|
"repository": "https://github.com/sveltejs/sapper",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,13 +1,97 @@
|
|||||||
import typescript from 'rollup-plugin-typescript';
|
import typescript from 'rollup-plugin-typescript';
|
||||||
|
import pkg from './package.json';
|
||||||
|
|
||||||
|
const external = [].concat(
|
||||||
|
Object.keys(pkg.dependencies),
|
||||||
|
Object.keys(process.binding('natives')),
|
||||||
|
'sapper/core.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
const paths = {
|
||||||
|
'sapper/core.js': './core.js'
|
||||||
|
};
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
|
// cli.js
|
||||||
|
{
|
||||||
|
input: 'src/cli/index.ts',
|
||||||
|
output: {
|
||||||
|
file: 'cli.js',
|
||||||
|
format: 'cjs',
|
||||||
|
banner: '#!/usr/bin/env node',
|
||||||
|
paths,
|
||||||
|
sourcemap: true
|
||||||
|
},
|
||||||
|
external,
|
||||||
|
plugins: [
|
||||||
|
typescript({
|
||||||
|
typescript: require('typescript')
|
||||||
|
})
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// core.js
|
||||||
|
{
|
||||||
|
input: 'src/core/index.ts',
|
||||||
|
output: {
|
||||||
|
file: 'core.js',
|
||||||
|
format: 'cjs',
|
||||||
|
banner: '#!/usr/bin/env node',
|
||||||
|
paths,
|
||||||
|
sourcemap: true
|
||||||
|
},
|
||||||
|
external,
|
||||||
|
plugins: [
|
||||||
|
typescript({
|
||||||
|
typescript: require('typescript')
|
||||||
|
})
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// middleware.js
|
||||||
|
{
|
||||||
|
input: 'src/middleware/index.ts',
|
||||||
|
output: {
|
||||||
|
file: 'middleware.js',
|
||||||
|
format: 'cjs',
|
||||||
|
paths,
|
||||||
|
sourcemap: true
|
||||||
|
},
|
||||||
|
external,
|
||||||
|
plugins: [
|
||||||
|
typescript({
|
||||||
|
typescript: require('typescript')
|
||||||
|
})
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
// runtime.js
|
// runtime.js
|
||||||
{
|
{
|
||||||
input: 'src/runtime/index.ts',
|
input: 'src/runtime/index.ts',
|
||||||
output: {
|
output: {
|
||||||
file: 'runtime.js',
|
file: 'runtime.js',
|
||||||
format: 'es'
|
format: 'es',
|
||||||
|
paths,
|
||||||
|
sourcemap: true
|
||||||
},
|
},
|
||||||
|
external,
|
||||||
|
plugins: [
|
||||||
|
typescript({
|
||||||
|
typescript: require('typescript')
|
||||||
|
})
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// webpack/config.js
|
||||||
|
{
|
||||||
|
input: 'src/webpack/index.ts',
|
||||||
|
output: {
|
||||||
|
file: 'webpack/config.js',
|
||||||
|
format: 'cjs',
|
||||||
|
paths,
|
||||||
|
sourcemap: true
|
||||||
|
},
|
||||||
|
external,
|
||||||
plugins: [
|
plugins: [
|
||||||
typescript({
|
typescript({
|
||||||
typescript: require('typescript')
|
typescript: require('typescript')
|
||||||
|
|||||||
1
runtime/README.md
Normal file
1
runtime/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
This directory exists for legacy reasons and should be deleted before releasing version 1.
|
||||||
28
src/cli/index.ts
Executable file
28
src/cli/index.ts
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
import { build, export as exporter } from 'sapper/core.js';
|
||||||
|
import { dest, dev, entry, src } from '../config';
|
||||||
|
|
||||||
|
process.env.NODE_ENV = 'production';
|
||||||
|
|
||||||
|
const cmd = process.argv[2];
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
if (cmd === 'build') {
|
||||||
|
build({ dest, dev, entry, src })
|
||||||
|
.then(() => {
|
||||||
|
const elapsed = Date.now() - start;
|
||||||
|
console.error(`built in ${elapsed}ms`); // TODO beautify this, e.g. 'built in 4.7 seconds'
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
|
||||||
|
});
|
||||||
|
} else if (cmd === 'export') {
|
||||||
|
build({ dest, dev: false, entry, src })
|
||||||
|
.then(() => exporter({ src, dest }))
|
||||||
|
.then(() => {
|
||||||
|
const elapsed = Date.now() - start;
|
||||||
|
console.error(`extracted in ${elapsed}ms`); // TODO beautify this, e.g. 'built in 4.7 seconds'
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err ? err.details || err.stack || err.message || err : 'Unknown error');
|
||||||
|
});
|
||||||
|
}
|
||||||
12
src/config.ts
Normal file
12
src/config.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
export const dev = process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
|
export const templates = path.resolve(process.env.SAPPER_TEMPLATES || 'templates');
|
||||||
|
export const src = path.resolve(process.env.SAPPER_ROUTES || 'routes');
|
||||||
|
export const dest = path.resolve(process.env.SAPPER_DEST || '.sapper');
|
||||||
|
|
||||||
|
export const entry = {
|
||||||
|
client: path.resolve(templates, '.main.rendered.js'),
|
||||||
|
server: path.resolve(dest, 'server-entry.js')
|
||||||
|
};
|
||||||
62
src/core/build.ts
Normal file
62
src/core/build.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import mkdirp from 'mkdirp';
|
||||||
|
import rimraf from 'rimraf';
|
||||||
|
import create_compilers from './create_compilers.js';
|
||||||
|
import create_app from './create_app.js';
|
||||||
|
import create_assets from './create_assets.js';
|
||||||
|
|
||||||
|
export default function build({
|
||||||
|
src,
|
||||||
|
dest,
|
||||||
|
dev,
|
||||||
|
entry
|
||||||
|
}: {
|
||||||
|
src: string;
|
||||||
|
dest: string;
|
||||||
|
dev: boolean;
|
||||||
|
entry: { client: string, server: string }
|
||||||
|
}) {
|
||||||
|
mkdirp.sync(dest);
|
||||||
|
rimraf.sync(path.join(dest, '**/*'));
|
||||||
|
|
||||||
|
// create main.js and server-routes.js
|
||||||
|
create_app({ dev, entry, src });
|
||||||
|
|
||||||
|
return new Promise((fulfil, reject) => {
|
||||||
|
function handleErrors(err, stats) {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.hasErrors()) {
|
||||||
|
console.error(stats.toString({ colors: true }));
|
||||||
|
reject(new Error(`Encountered errors while building app`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { client, server } = create_compilers();
|
||||||
|
|
||||||
|
client.run((err, client_stats) => {
|
||||||
|
handleErrors(err, client_stats);
|
||||||
|
const client_info = client_stats.toJson();
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dest, 'stats.client.json'),
|
||||||
|
JSON.stringify(client_info, null, ' ')
|
||||||
|
);
|
||||||
|
|
||||||
|
server.run((err, server_stats) => {
|
||||||
|
handleErrors(err, server_stats);
|
||||||
|
const server_info = server_stats.toJson();
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dest, 'stats.server.json'),
|
||||||
|
JSON.stringify(server_info, null, ' ')
|
||||||
|
);
|
||||||
|
|
||||||
|
create_assets({ src, dest, dev, client_info, server_info });
|
||||||
|
fulfil();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
90
src/core/create_app.ts
Normal file
90
src/core/create_app.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import create_routes from './create_routes';
|
||||||
|
|
||||||
|
function posixify(file: string) {
|
||||||
|
return file.replace(/[/\\]/g, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
function fudge_mtime(file: string) {
|
||||||
|
// need to fudge the mtime so that webpack doesn't go doolally
|
||||||
|
const { atime, mtime } = fs.statSync(file);
|
||||||
|
fs.utimesSync(
|
||||||
|
file,
|
||||||
|
new Date(atime.getTime() - 999999),
|
||||||
|
new Date(mtime.getTime() - 999999)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function create_app({
|
||||||
|
src,
|
||||||
|
dev,
|
||||||
|
entry
|
||||||
|
}: {
|
||||||
|
src: string;
|
||||||
|
dev: boolean;
|
||||||
|
entry: { client: string; server: string };
|
||||||
|
}) {
|
||||||
|
const routes = create_routes({ src });
|
||||||
|
|
||||||
|
function create_client_main() {
|
||||||
|
const code = `[${routes
|
||||||
|
.filter(route => route.type === 'page')
|
||||||
|
.map(route => {
|
||||||
|
const params =
|
||||||
|
route.dynamic.length === 0
|
||||||
|
? '{}'
|
||||||
|
: `{ ${route.dynamic
|
||||||
|
.map((part, i) => `${part}: match[${i + 1}]`)
|
||||||
|
.join(', ')} }`;
|
||||||
|
|
||||||
|
const file = posixify(`${src}/${route.file}`);
|
||||||
|
return `{ pattern: ${
|
||||||
|
route.pattern
|
||||||
|
}, params: match => (${params}), load: () => import(/* webpackChunkName: "${
|
||||||
|
route.id
|
||||||
|
}" */ '${file}') }`;
|
||||||
|
})
|
||||||
|
.join(', ')}]`;
|
||||||
|
|
||||||
|
let main = fs
|
||||||
|
.readFileSync('templates/main.js', 'utf-8')
|
||||||
|
.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);
|
||||||
|
fudge_mtime(entry.client);
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`);
|
||||||
|
fudge_mtime(entry.server);
|
||||||
|
}
|
||||||
|
|
||||||
|
create_client_main();
|
||||||
|
create_server_routes();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default create_app;
|
||||||
@@ -1,23 +1,26 @@
|
|||||||
const fs = require('fs');
|
import * as fs from 'fs';
|
||||||
const path = require('path');
|
import * as path from 'path';
|
||||||
const glob = require('glob');
|
import glob from 'glob';
|
||||||
const templates = require('../templates.js');
|
import { create_templates, render } from './templates';
|
||||||
const route_manager = require('../route_manager.js');
|
import create_routes from './create_routes';
|
||||||
const { dest, dev } = require('../config.js');
|
|
||||||
|
|
||||||
function ensure_array(thing) {
|
function ensure_array(thing) {
|
||||||
return Array.isArray(thing) ? thing : [thing]; // omg webpack what the HELL are you doing
|
return Array.isArray(thing) ? thing : [thing]; // omg webpack what the HELL are you doing
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = function generate_asset_cache(clientInfo, serverInfo) {
|
export default function create_assets({ src, dest, dev, client_info, server_info }) {
|
||||||
const main_file = `/client/${ensure_array(clientInfo.assetsByChunkName.main)[0]}`;
|
create_templates(); // TODO refactor this...
|
||||||
|
|
||||||
const chunk_files = clientInfo.assets.map(chunk => `/client/${chunk.name}`);
|
const main_file = `/client/${ensure_array(client_info.assetsByChunkName.main)[0]}`;
|
||||||
|
|
||||||
const service_worker = generate_service_worker(chunk_files);
|
const chunk_files = client_info.assets.map(chunk => `/client/${chunk.name}`);
|
||||||
|
|
||||||
|
const service_worker = generate_service_worker({ chunk_files, src });
|
||||||
const index = generate_index(main_file);
|
const index = generate_index(main_file);
|
||||||
|
|
||||||
if (dev) {
|
const routes = create_routes({ src });
|
||||||
|
|
||||||
|
if (dev) { // TODO move this into calling code
|
||||||
fs.writeFileSync(path.join(dest, 'service-worker.js'), service_worker);
|
fs.writeFileSync(path.join(dest, 'service-worker.js'), service_worker);
|
||||||
fs.writeFileSync(path.join(dest, 'index.html'), index);
|
fs.writeFileSync(path.join(dest, 'index.html'), index);
|
||||||
}
|
}
|
||||||
@@ -33,8 +36,9 @@ module.exports = function generate_asset_cache(clientInfo, serverInfo) {
|
|||||||
return lookup;
|
return lookup;
|
||||||
}, {}),
|
}, {}),
|
||||||
|
|
||||||
routes: route_manager.routes.reduce((lookup, route) => {
|
// TODO confusing that `routes` refers to an array *and* a lookup
|
||||||
lookup[route.id] = `/client/${ensure_array(clientInfo.assetsByChunkName[route.id])[0]}`;
|
routes: routes.reduce((lookup, route) => {
|
||||||
|
lookup[route.id] = `/client/${ensure_array(client_info.assetsByChunkName[route.id])[0]}`;
|
||||||
return lookup;
|
return lookup;
|
||||||
}, {}),
|
}, {}),
|
||||||
|
|
||||||
@@ -43,16 +47,20 @@ module.exports = function generate_asset_cache(clientInfo, serverInfo) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
server: {
|
server: {
|
||||||
entry: path.resolve(dest, 'server', serverInfo.assetsByChunkName.main)
|
entry: path.resolve(dest, 'server', server_info.assetsByChunkName.main)
|
||||||
}
|
},
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
function generate_service_worker(chunk_files) {
|
service_worker
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function generate_service_worker({ chunk_files, src }) {
|
||||||
const assets = glob.sync('**', { cwd: 'assets', nodir: true });
|
const assets = glob.sync('**', { cwd: 'assets', nodir: true });
|
||||||
|
|
||||||
|
const routes = create_routes({ src });
|
||||||
|
|
||||||
const route_code = `[${
|
const route_code = `[${
|
||||||
route_manager.routes
|
routes
|
||||||
.filter(route => route.type === 'page')
|
.filter(route => route.type === 'page')
|
||||||
.map(route => `{ pattern: ${route.pattern} }`)
|
.map(route => `{ pattern: ${route.pattern} }`)
|
||||||
.join(', ')
|
.join(', ')
|
||||||
@@ -66,7 +74,7 @@ function generate_service_worker(chunk_files) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function generate_index(main_file) {
|
function generate_index(main_file) {
|
||||||
return templates.render(200, {
|
return render(200, {
|
||||||
styles: '',
|
styles: '',
|
||||||
head: '',
|
head: '',
|
||||||
html: '<noscript>Please enable JavaScript!</noscript>',
|
html: '<noscript>Please enable JavaScript!</noscript>',
|
||||||
16
src/core/create_compilers.ts
Normal file
16
src/core/create_compilers.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
import relative from 'require-relative';
|
||||||
|
|
||||||
|
export default function create_compilers() {
|
||||||
|
const webpack = relative('webpack', process.cwd());
|
||||||
|
|
||||||
|
return {
|
||||||
|
client: webpack(
|
||||||
|
require(path.resolve('webpack.client.config.js'))
|
||||||
|
),
|
||||||
|
|
||||||
|
server: webpack(
|
||||||
|
require(path.resolve('webpack.server.config.js'))
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
const path = require('path');
|
import * as path from 'path';
|
||||||
|
import glob from 'glob';
|
||||||
|
|
||||||
module.exports = function create_matchers(files) {
|
export default function create_routes({ src, files = glob.sync('**/*.+(html|js|mjs)', { cwd: src }) }) {
|
||||||
const routes = files
|
const routes = files
|
||||||
.map(file => {
|
.map(file => {
|
||||||
if (/(^|\/|\\)_/.test(file)) return;
|
if (/(^|\/|\\)_/.test(file)) return;
|
||||||
@@ -31,7 +32,7 @@ module.exports = function create_matchers(files) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pattern = new RegExp(`^${pattern_string || '\\/'}$`);
|
const pattern = new RegExp(`^${pattern_string}\\/?$`);
|
||||||
|
|
||||||
const test = url => pattern.test(url);
|
const test = url => pattern.test(url);
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ module.exports = function create_matchers(files) {
|
|||||||
const b_is_dynamic = b_part[0] === '[';
|
const b_is_dynamic = b_part[0] === '[';
|
||||||
|
|
||||||
if (a_is_dynamic === b_is_dynamic) {
|
if (a_is_dynamic === b_is_dynamic) {
|
||||||
if (!a_is_dynamic) same = false;
|
if (!a_is_dynamic && a_part !== b_part) same = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,4 +88,4 @@ module.exports = function create_matchers(files) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return routes;
|
return routes;
|
||||||
};
|
}
|
||||||
104
src/core/export.ts
Normal file
104
src/core/export.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
import * as sander from 'sander';
|
||||||
|
import express from 'express';
|
||||||
|
import cheerio from 'cheerio';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import URL from 'url-parse';
|
||||||
|
import create_assets from './create_assets.js';
|
||||||
|
// import middleware from '../middleware/index.js';
|
||||||
|
|
||||||
|
const { PORT = 3000, OUTPUT_DIR = 'dist' } = process.env;
|
||||||
|
|
||||||
|
const origin = `http://localhost:${PORT}`;
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
function read_json(file) {
|
||||||
|
return JSON.parse(sander.readFileSync(file, { encoding: 'utf-8' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function exporter({ src, dest }) { // TODO dest is a terrible name in this context
|
||||||
|
// Prep output directory
|
||||||
|
sander.rimrafSync(OUTPUT_DIR);
|
||||||
|
|
||||||
|
const { service_worker } = create_assets({
|
||||||
|
src, dest,
|
||||||
|
dev: false,
|
||||||
|
client_info: read_json(path.join(dest, 'stats.client.json')),
|
||||||
|
server_info: read_json(path.join(dest, 'stats.server.json'))
|
||||||
|
});
|
||||||
|
|
||||||
|
sander.copydirSync('assets').to(OUTPUT_DIR);
|
||||||
|
sander.copydirSync(dest, 'client').to(OUTPUT_DIR, 'client');
|
||||||
|
sander.writeFileSync(OUTPUT_DIR, 'service-worker.js', service_worker);
|
||||||
|
|
||||||
|
// Intercept server route fetches
|
||||||
|
function save(res) {
|
||||||
|
res = res.clone();
|
||||||
|
|
||||||
|
return res.text().then(body => {
|
||||||
|
const { pathname } = new URL(res.url);
|
||||||
|
let dest = OUTPUT_DIR + pathname;
|
||||||
|
|
||||||
|
const type = res.headers.get('Content-Type');
|
||||||
|
if (type && type.startsWith('text/html')) dest += '/index.html';
|
||||||
|
|
||||||
|
sander.writeFileSync(dest, body);
|
||||||
|
|
||||||
|
return body;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
global.fetch = (url, opts) => {
|
||||||
|
if (url[0] === '/') {
|
||||||
|
url = `http://localhost:${PORT}${url}`;
|
||||||
|
|
||||||
|
return fetch(url, opts)
|
||||||
|
.then(r => {
|
||||||
|
save(r);
|
||||||
|
return r;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(url, opts);
|
||||||
|
};
|
||||||
|
|
||||||
|
const middleware = require('./middleware')({ dev: false }); // TODO this is filthy
|
||||||
|
app.use(middleware);
|
||||||
|
const server = app.listen(PORT);
|
||||||
|
|
||||||
|
const seen = new Set();
|
||||||
|
|
||||||
|
function handle(url) {
|
||||||
|
if (url.origin !== origin) return;
|
||||||
|
|
||||||
|
if (seen.has(url.pathname)) return;
|
||||||
|
seen.add(url.pathname);
|
||||||
|
|
||||||
|
return fetch(url.href)
|
||||||
|
.then(r => {
|
||||||
|
save(r);
|
||||||
|
|
||||||
|
if (r.headers.get('Content-Type') === 'text/html') {
|
||||||
|
return r.text().then(body => {
|
||||||
|
const $ = cheerio.load(body);
|
||||||
|
const hrefs = [];
|
||||||
|
|
||||||
|
$('a[href]').each((i, $a) => {
|
||||||
|
hrefs.push($a.attribs.href);
|
||||||
|
});
|
||||||
|
|
||||||
|
return hrefs.reduce((promise, href) => {
|
||||||
|
return promise.then(() => handle(new URL(href, url.href)));
|
||||||
|
}, Promise.resolve());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(`Error rendering ${url.pathname}: ${err.message}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return handle(new URL(origin)) // TODO all static routes
|
||||||
|
.then(() => server.close());
|
||||||
|
}
|
||||||
11
src/core/index.ts
Normal file
11
src/core/index.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { create_templates, render, stream } from './templates'; // TODO templates is an anomaly... fix post-#91
|
||||||
|
|
||||||
|
export { default as build } from './build';
|
||||||
|
export { default as export } from './export.js';
|
||||||
|
|
||||||
|
export { default as create_app } from './create_app';
|
||||||
|
export { default as create_assets } from './create_assets';
|
||||||
|
export { default as create_compilers } from './create_compilers';
|
||||||
|
export { default as create_routes } from './create_routes';
|
||||||
|
|
||||||
|
export const templates = { create_templates, render, stream };
|
||||||
@@ -1,18 +1,45 @@
|
|||||||
const fs = require('fs');
|
import * as fs from 'fs';
|
||||||
const glob = require('glob');
|
import glob from 'glob';
|
||||||
const chokidar = require('chokidar');
|
import chalk from 'chalk';
|
||||||
const { dev } = require('./config.js');
|
import framer from 'code-frame';
|
||||||
|
import { locate } from 'locate-character';
|
||||||
|
|
||||||
let templates;
|
let templates;
|
||||||
|
|
||||||
function create_templates() {
|
function error(e) {
|
||||||
|
if (e.title) console.error(chalk.bold.red(e.title));
|
||||||
|
if (e.body) console.error(chalk.red(e.body));
|
||||||
|
if (e.url) console.error(chalk.cyan(e.url));
|
||||||
|
if (e.frame) console.error(chalk.grey(e.frame));
|
||||||
|
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function create_templates() {
|
||||||
templates = glob.sync('*.html', { cwd: 'templates' })
|
templates = glob.sync('*.html', { cwd: 'templates' })
|
||||||
.map(file => {
|
.map(file => {
|
||||||
const template = fs.readFileSync(`templates/${file}`, 'utf-8');
|
const template = fs.readFileSync(`templates/${file}`, 'utf-8');
|
||||||
const status = file.replace('.html', '').toLowerCase();
|
const status = file.replace('.html', '').toLowerCase();
|
||||||
|
|
||||||
if (!/^[0-9x]{3}$/.test(status)) {
|
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`);
|
error({
|
||||||
|
title: `templates/${file}`,
|
||||||
|
body: `Bad template — should be a valid status code like 404.html, or a wildcard like 2xx.html`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = template.indexOf('%sapper.main%');
|
||||||
|
if (index !== -1) {
|
||||||
|
// TODO remove this in a future version
|
||||||
|
const { line, column } = locate(template, index, { offsetLine: 1 });
|
||||||
|
const frame = framer(template, line, column);
|
||||||
|
|
||||||
|
error({
|
||||||
|
title: `templates/${file}`,
|
||||||
|
body: `<script src='%sapper.main%'> is unsupported — use %sapper.scripts% (without the <script> tag) instead`,
|
||||||
|
url: 'https://github.com/sveltejs/sapper/issues/86',
|
||||||
|
frame
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const specificity = (
|
const specificity = (
|
||||||
@@ -31,10 +58,14 @@ function create_templates() {
|
|||||||
return key in data ? data[key] : '';
|
return key in data ? data[key] : '';
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
stream: async (res, data) => {
|
stream: (res, data) => {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
do {
|
function stream_inner() {
|
||||||
|
if (i >= template.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const start = template.indexOf('%sapper', i);
|
const start = template.indexOf('%sapper', i);
|
||||||
|
|
||||||
if (start === -1) {
|
if (start === -1) {
|
||||||
@@ -53,38 +84,32 @@ function create_templates() {
|
|||||||
const match = /sapper\.(\w+)/.exec(tag);
|
const match = /sapper\.(\w+)/.exec(tag);
|
||||||
if (!match || !(match[1] in data)) throw new Error(`Bad template`); // TODO ditto
|
if (!match || !(match[1] in data)) throw new Error(`Bad template`); // TODO ditto
|
||||||
|
|
||||||
res.write(await data[match[1]]);
|
return Promise.resolve(data[match[1]]).then(datamatch => {
|
||||||
i = end + 1;
|
res.write(datamatch);
|
||||||
} while (i < template.length);
|
i = end + 1;
|
||||||
|
return stream_inner();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve().then(stream_inner);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.sort((a, b) => b.specificity - a.specificity);
|
.sort((a, b) => b.specificity - a.specificity);
|
||||||
|
|
||||||
|
return templates;
|
||||||
}
|
}
|
||||||
|
|
||||||
create_templates();
|
export function render(status, data) {
|
||||||
|
|
||||||
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));
|
const template = templates.find(template => template.test(status));
|
||||||
if (template) return template.render(data);
|
if (template) return template.render(data);
|
||||||
|
|
||||||
return `Missing template for status code ${status}`;
|
return `Missing template for status code ${status}`;
|
||||||
};
|
}
|
||||||
|
|
||||||
exports.stream = (res, status, data) => {
|
export function stream(res, status, data) {
|
||||||
const template = templates.find(template => template.test(status));
|
const template = templates.find(template => template.test(status));
|
||||||
if (template) return template.stream(res, data);
|
if (template) return template.stream(res, data);
|
||||||
|
|
||||||
return `Missing template for status code ${status}`;
|
return `Missing template for status code ${status}`;
|
||||||
};
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
const fs = require('fs');
|
import * as fs from 'fs';
|
||||||
const path = require('path');
|
import * as path from 'path';
|
||||||
const chalk = require('chalk');
|
import chalk from 'chalk';
|
||||||
const compilers = require('./compilers.js');
|
import { create_app, create_assets, create_routes, templates } from 'sapper/core.js';
|
||||||
const generate_asset_cache = require('./generate_asset_cache.js');
|
import { dest } from '../config.js';
|
||||||
const { dest } = require('../config.js');
|
|
||||||
|
|
||||||
function deferred() {
|
function deferred() {
|
||||||
const d = {};
|
const d = {};
|
||||||
@@ -16,7 +15,7 @@ function deferred() {
|
|||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = function create_watcher() {
|
export default function create_watcher({ compilers, dev, entry, src, onroutes }) {
|
||||||
const deferreds = {
|
const deferreds = {
|
||||||
client: deferred(),
|
client: deferred(),
|
||||||
server: deferred()
|
server: deferred()
|
||||||
@@ -32,10 +31,11 @@ module.exports = function create_watcher() {
|
|||||||
const server_info = server_stats.toJson();
|
const server_info = server_stats.toJson();
|
||||||
fs.writeFileSync(path.join(dest, 'stats.server.json'), JSON.stringify(server_info, null, ' '));
|
fs.writeFileSync(path.join(dest, 'stats.server.json'), JSON.stringify(server_info, null, ' '));
|
||||||
|
|
||||||
return generate_asset_cache(
|
return create_assets({
|
||||||
client_stats.toJson(),
|
src, dest, dev,
|
||||||
server_stats.toJson()
|
client_info: client_stats.toJson(),
|
||||||
);
|
server_info: server_stats.toJson()
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function watch_compiler(type) {
|
function watch_compiler(type) {
|
||||||
@@ -60,6 +60,34 @@ module.exports = function create_watcher() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const chokidar = require('chokidar');
|
||||||
|
|
||||||
|
function watch_files(pattern, callback) {
|
||||||
|
const watcher = chokidar.watch(pattern, {
|
||||||
|
persistent: false
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher.on('add', callback);
|
||||||
|
watcher.on('change', callback);
|
||||||
|
watcher.on('unlink', callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch_files('routes/**/*.+(html|js|mjs)', () => {
|
||||||
|
const routes = create_routes({ src });
|
||||||
|
onroutes(routes);
|
||||||
|
|
||||||
|
create_app({ dev, entry, src }); // TODO this calls `create_routes` again, we should pass `routes` to `create_app` instead
|
||||||
|
});
|
||||||
|
|
||||||
|
watch_files('templates/main.js', () => {
|
||||||
|
create_app({ dev, entry, src });
|
||||||
|
});
|
||||||
|
|
||||||
|
watch_files('templates/**.html', () => {
|
||||||
|
templates.create_templates();
|
||||||
|
// TODO reload current page?
|
||||||
|
});
|
||||||
|
|
||||||
const watcher = {
|
const watcher = {
|
||||||
ready: invalidate(),
|
ready: invalidate(),
|
||||||
client: watch_compiler('client'),
|
client: watch_compiler('client'),
|
||||||
@@ -72,4 +100,4 @@ module.exports = function create_watcher() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return watcher;
|
return watcher;
|
||||||
};
|
}
|
||||||
285
src/middleware/index.ts
Normal file
285
src/middleware/index.ts
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import mkdirp from 'mkdirp';
|
||||||
|
import rimraf from 'rimraf';
|
||||||
|
import serialize from 'serialize-javascript';
|
||||||
|
import escape_html from 'escape-html';
|
||||||
|
import { create_routes, templates, create_compilers, create_assets } from 'sapper/core.js';
|
||||||
|
import create_watcher from './create_watcher';
|
||||||
|
import { dest, dev, entry, src } from '../config';
|
||||||
|
|
||||||
|
function connect_dev() {
|
||||||
|
mkdirp.sync(dest);
|
||||||
|
rimraf.sync(path.join(dest, '**/*'));
|
||||||
|
|
||||||
|
const compilers = create_compilers();
|
||||||
|
|
||||||
|
let routes;
|
||||||
|
|
||||||
|
const watcher = create_watcher({
|
||||||
|
dev, entry, src,
|
||||||
|
compilers,
|
||||||
|
onroutes: _ => {
|
||||||
|
routes = _;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let asset_cache;
|
||||||
|
|
||||||
|
const middleware = compose_handlers([
|
||||||
|
require('webpack-hot-middleware')(compilers.client, {
|
||||||
|
reload: true,
|
||||||
|
path: '/__webpack_hmr',
|
||||||
|
heartbeat: 10 * 1000
|
||||||
|
}),
|
||||||
|
|
||||||
|
(req, res, next) => {
|
||||||
|
watcher.ready.then(cache => {
|
||||||
|
asset_cache = cache;
|
||||||
|
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, () => routes),
|
||||||
|
|
||||||
|
get_not_found_handler(() => asset_cache)
|
||||||
|
]);
|
||||||
|
|
||||||
|
middleware.close = () => {
|
||||||
|
watcher.close();
|
||||||
|
// TODO shut down chokidar
|
||||||
|
};
|
||||||
|
|
||||||
|
return middleware;
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect_prod() {
|
||||||
|
const asset_cache = create_assets({
|
||||||
|
src, dest,
|
||||||
|
dev: false,
|
||||||
|
client_info: read_json(path.join(dest, 'stats.client.json')),
|
||||||
|
server_info: read_json(path.join(dest, 'stats.server.json'))
|
||||||
|
});
|
||||||
|
|
||||||
|
const routes = create_routes({ src }); // TODO rename update
|
||||||
|
|
||||||
|
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, () => routes),
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function connect({ dev: _dev = dev } = {}) {
|
||||||
|
console.log({ dev, _dev });
|
||||||
|
return _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.setHeader('Content-Type', opts.type);
|
||||||
|
res.setHeader('Cache-Control', opts.cache);
|
||||||
|
|
||||||
|
res.end(opts.fn(req.pathname));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved = Promise.resolve();
|
||||||
|
|
||||||
|
function get_route_handler(get_assets, get_routes) {
|
||||||
|
function handle_route(route, req, res, next, { client, server }) {
|
||||||
|
req.params = route.exec(req.pathname);
|
||||||
|
|
||||||
|
const mod = require(server.entry)[route.id];
|
||||||
|
|
||||||
|
if (route.type === 'page') {
|
||||||
|
// for page routes, we're going to serve some HTML
|
||||||
|
res.setHeader('Content-Type', 'text/html');
|
||||||
|
|
||||||
|
// preload main.js and current route
|
||||||
|
// TODO detect other stuff we can preload? images, CSS, fonts?
|
||||||
|
res.setHeader('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 => {
|
||||||
|
const serialized = try_serialize(preloaded);
|
||||||
|
Object.assign(data, preloaded);
|
||||||
|
|
||||||
|
return { rendered: mod.render(data), serialized };
|
||||||
|
});
|
||||||
|
|
||||||
|
return templates.stream(res, 200, {
|
||||||
|
scripts: promise.then(({ serialized }) => {
|
||||||
|
const main = `<script src='${client.main_file}'></script>`;
|
||||||
|
|
||||||
|
if (serialized) {
|
||||||
|
return `<script>__SAPPER__ = { preloaded: ${serialized} };</script>${main}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return main;
|
||||||
|
}),
|
||||||
|
html: promise.then(({ rendered }) => rendered.html),
|
||||||
|
head: promise.then(({ rendered }) => `<noscript id='sapper-head-start'></noscript>${rendered.head}<noscript id='sapper-head-end'></noscript>`),
|
||||||
|
styles: promise.then(({ rendered }) => (rendered.css && rendered.css.code ? `<style>${rendered.css.code}</style>` : ''))
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const { html, head, css } = mod.render(data);
|
||||||
|
|
||||||
|
const page = templates.render(200, {
|
||||||
|
scripts: `<script src='${client.main_file}'></script>`,
|
||||||
|
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 method = req.method.toLowerCase();
|
||||||
|
// 'delete' cannot be exported from a module because it is a keyword,
|
||||||
|
// so check for 'del' instead
|
||||||
|
const method_export = method === 'delete' ? 'del' : method;
|
||||||
|
const handler = mod[method_export];
|
||||||
|
if (handler) {
|
||||||
|
handler(req, res, next);
|
||||||
|
} else {
|
||||||
|
// no matching handler for method — 404
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return function find_route(req, res, next) {
|
||||||
|
const url = req.pathname;
|
||||||
|
|
||||||
|
resolved
|
||||||
|
.then(() => {
|
||||||
|
const routes = get_routes();
|
||||||
|
for (const route of routes) {
|
||||||
|
if (route.test(url)) return handle_route(route, req, res, next, get_assets());
|
||||||
|
}
|
||||||
|
|
||||||
|
// no matching route — 404
|
||||||
|
next();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.statusCode = 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.statusCode = 404;
|
||||||
|
res.end(templates.render(404, {
|
||||||
|
title: 'Not found',
|
||||||
|
status: 404,
|
||||||
|
method: req.method,
|
||||||
|
scripts: `<script src='${asset_cache.client.main_file}'></script>`,
|
||||||
|
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'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function try_serialize(data) {
|
||||||
|
try {
|
||||||
|
return serialize(data);
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { detach, findAnchor, scroll_state, which } from './utils';
|
import { detach, findAnchor, scroll_state, which } from './utils';
|
||||||
import { Component, ComponentConstructor, Params, Query, Route, RouteData, ScrollPosition } from './interfaces';
|
import { Component, ComponentConstructor, Params, Query, Route, RouteData, ScrollPosition, Target } from './interfaces';
|
||||||
|
|
||||||
export let component: Component;
|
export let component: Component;
|
||||||
let target: Node;
|
let target: Node;
|
||||||
@@ -19,7 +19,7 @@ if ('scrollRestoration' in history) {
|
|||||||
history.scrollRestoration = 'manual';
|
history.scrollRestoration = 'manual';
|
||||||
}
|
}
|
||||||
|
|
||||||
function select_route(url: URL): { route: Route, data: RouteData } {
|
function select_route(url: URL): Target {
|
||||||
if (url.origin !== window.location.origin) return null;
|
if (url.origin !== window.location.origin) return null;
|
||||||
|
|
||||||
for (const route of routes) {
|
for (const route of routes) {
|
||||||
@@ -28,9 +28,13 @@ function select_route(url: URL): { route: Route, data: RouteData } {
|
|||||||
const params = route.params(match);
|
const params = route.params(match);
|
||||||
|
|
||||||
const query: Record<string, string | true> = {};
|
const query: Record<string, string | true> = {};
|
||||||
for (const [key, value] of url.searchParams) query[key] = value || true;
|
if (url.search.length > 0) {
|
||||||
|
url.search.slice(1).split('&').forEach(searchParam => {
|
||||||
return { route, data: { params, query } };
|
const [, key, value] = /([^=]+)=(.*)/.exec(searchParam);
|
||||||
|
query[key] = value || true;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return { url, route, data: { params, query } };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,7 +64,7 @@ function render(Component: ComponentConstructor, data: any, scroll: ScrollPositi
|
|||||||
component = new Component({
|
component = new Component({
|
||||||
target,
|
target,
|
||||||
data,
|
data,
|
||||||
hydrate: !!component
|
hydrate: !component
|
||||||
});
|
});
|
||||||
|
|
||||||
if (scroll) {
|
if (scroll) {
|
||||||
@@ -68,44 +72,46 @@ function render(Component: ComponentConstructor, data: any, scroll: ScrollPositi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepare_route(Component, data) {
|
function prepare_route(Component: ComponentConstructor, data: RouteData) {
|
||||||
return Promise.resolve(
|
if (!Component.preload) {
|
||||||
Component.preload ? Component.preload(data) : {}
|
return { Component, data };
|
||||||
).then(preloaded => {
|
}
|
||||||
|
|
||||||
|
if (!component && window.__SAPPER__ && window.__SAPPER__.preloaded) {
|
||||||
|
return { Component, data: Object.assign(data, window.__SAPPER__.preloaded) };
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(Component.preload(data)).then(preloaded => {
|
||||||
Object.assign(data, preloaded)
|
Object.assign(data, preloaded)
|
||||||
return { Component, data };
|
return { Component, data };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigate(url: URL, id: number) {
|
function navigate(target: Target, id: number) {
|
||||||
const selected = select_route(url);
|
if (id) {
|
||||||
if (selected) {
|
// popstate or initial navigation
|
||||||
if (id) {
|
|
||||||
// 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;
|
cid = id;
|
||||||
return true;
|
} else {
|
||||||
|
// clicked on a link. preserve scroll state
|
||||||
|
scroll_history[cid] = scroll_state();
|
||||||
|
|
||||||
|
id = cid = ++uid;
|
||||||
|
scroll_history[cid] = { x: 0, y: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cid = id;
|
||||||
|
|
||||||
|
const loaded = prefetching && prefetching.href === target.url.href ?
|
||||||
|
prefetching.promise :
|
||||||
|
target.route.load().then(mod => prepare_route(mod.default, target.data));
|
||||||
|
|
||||||
|
prefetching = null;
|
||||||
|
|
||||||
|
const token = current_token = {};
|
||||||
|
|
||||||
|
return loaded.then(({ Component, data }) => {
|
||||||
|
render(Component, data, scroll_history[id], token);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handle_click(event: MouseEvent) {
|
function handle_click(event: MouseEvent) {
|
||||||
@@ -141,7 +147,9 @@ function handle_click(event: MouseEvent) {
|
|||||||
// Don't handle hash changes
|
// Don't handle hash changes
|
||||||
if (url.pathname === window.location.pathname && url.search === window.location.search) return;
|
if (url.pathname === window.location.pathname && url.search === window.location.search) return;
|
||||||
|
|
||||||
if (navigate(url, null)) {
|
const target = select_route(url);
|
||||||
|
if (target) {
|
||||||
|
navigate(target, null);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
history.pushState({ id: cid }, '', url.href);
|
history.pushState({ id: cid }, '', url.href);
|
||||||
}
|
}
|
||||||
@@ -151,7 +159,9 @@ function handle_popstate(event: PopStateEvent) {
|
|||||||
scroll_history[cid] = scroll_state();
|
scroll_history[cid] = scroll_state();
|
||||||
|
|
||||||
if (event.state) {
|
if (event.state) {
|
||||||
navigate(new URL(window.location.href), event.state.id);
|
const url = new URL(window.location.href);
|
||||||
|
const target = select_route(url);
|
||||||
|
navigate(target, event.state.id);
|
||||||
} else {
|
} else {
|
||||||
// hashchange
|
// hashchange
|
||||||
cid = ++uid;
|
cid = ++uid;
|
||||||
@@ -164,20 +174,24 @@ let prefetching: {
|
|||||||
promise: Promise<{ Component: ComponentConstructor, data: any }>;
|
promise: Promise<{ Component: ComponentConstructor, data: any }>;
|
||||||
} = null;
|
} = null;
|
||||||
|
|
||||||
function prefetch(event: MouseEvent | TouchEvent) {
|
export function prefetch(href: string) {
|
||||||
const a: HTMLAnchorElement = <HTMLAnchorElement>findAnchor(<Node>event.target);
|
const selected = select_route(new URL(href));
|
||||||
if (!a || a.rel !== 'prefetch') return;
|
|
||||||
|
|
||||||
const selected = select_route(new URL(a.href));
|
|
||||||
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
prefetching = {
|
prefetching = {
|
||||||
href: a.href,
|
href,
|
||||||
promise: selected.route.load().then(mod => prepare_route(mod.default, selected.data))
|
promise: selected.route.load().then(mod => prepare_route(mod.default, selected.data))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handle_touchstart_mouseover(event: MouseEvent | TouchEvent) {
|
||||||
|
const a: HTMLAnchorElement = <HTMLAnchorElement>findAnchor(<Node>event.target);
|
||||||
|
if (!a || a.rel !== 'prefetch') return;
|
||||||
|
|
||||||
|
prefetch(a.href);
|
||||||
|
}
|
||||||
|
|
||||||
let inited: boolean;
|
let inited: boolean;
|
||||||
|
|
||||||
export function init(_target: Node, _routes: Route[]) {
|
export function init(_target: Node, _routes: Route[]) {
|
||||||
@@ -189,29 +203,33 @@ export function init(_target: Node, _routes: Route[]) {
|
|||||||
window.addEventListener('popstate', handle_popstate);
|
window.addEventListener('popstate', handle_popstate);
|
||||||
|
|
||||||
// prefetch
|
// prefetch
|
||||||
window.addEventListener('touchstart', prefetch);
|
window.addEventListener('touchstart', handle_touchstart_mouseover);
|
||||||
window.addEventListener('mouseover', prefetch);
|
window.addEventListener('mouseover', handle_touchstart_mouseover);
|
||||||
|
|
||||||
inited = true;
|
inited = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
return Promise.resolve().then(() => {
|
||||||
const { hash, href } = window.location;
|
const { hash, href } = window.location;
|
||||||
|
|
||||||
const deep_linked = hash && document.querySelector(hash);
|
const deep_linked = hash && document.getElementById(hash.slice(1));
|
||||||
scroll_history[uid] = deep_linked ?
|
scroll_history[uid] = deep_linked ?
|
||||||
{ x: 0, y: deep_linked.getBoundingClientRect().top } :
|
{ x: 0, y: deep_linked.getBoundingClientRect().top } :
|
||||||
scroll_state();
|
scroll_state();
|
||||||
|
|
||||||
history.replaceState({ id: uid }, '', href);
|
history.replaceState({ id: uid }, '', href);
|
||||||
navigate(new URL(window.location.href), uid);
|
|
||||||
|
const target = select_route(new URL(window.location.href));
|
||||||
|
return navigate(target, uid);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function goto(href: string, opts = { replaceState: false }) {
|
export function goto(href: string, opts = { replaceState: false }) {
|
||||||
if (navigate(new URL(href, window.location.href), null)) {
|
const target = select_route(new URL(href, window.location.href));
|
||||||
|
if (target) {
|
||||||
|
navigate(target, null);
|
||||||
if (history) history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
|
if (history) history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
|
||||||
} else {
|
} else {
|
||||||
window.location.href = href;
|
window.location.href = href;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,4 +20,10 @@ export type Route = {
|
|||||||
export type ScrollPosition = {
|
export type ScrollPosition = {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Target = {
|
||||||
|
url: URL;
|
||||||
|
route: Route;
|
||||||
|
data: RouteData;
|
||||||
};
|
};
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
const { dest, dev, entry } = require('../lib/config.js');
|
import { dest, dev, entry } from '../config';
|
||||||
|
|
||||||
module.exports = {
|
export default {
|
||||||
dev,
|
dev,
|
||||||
|
|
||||||
client: {
|
client: {
|
||||||
entry: () => {
|
entry: () => {
|
||||||
return {
|
return {
|
||||||
main: entry.client
|
main: [
|
||||||
|
entry.client,
|
||||||
|
// workaround for https://github.com/webpack-contrib/extract-text-webpack-plugin/issues/456
|
||||||
|
'style-loader/lib/addStyles',
|
||||||
|
'css-loader/lib/css-base'
|
||||||
|
]
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -36,4 +41,4 @@ module.exports = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
3
test/app/.gitignore
vendored
3
test/app/.gitignore
vendored
@@ -3,4 +3,5 @@ node_modules
|
|||||||
.sapper
|
.sapper
|
||||||
yarn.lock
|
yarn.lock
|
||||||
cypress/screenshots
|
cypress/screenshots
|
||||||
templates/.*
|
templates/.*
|
||||||
|
dist
|
||||||
|
|||||||
6
test/app/package-lock.json
generated
6
test/app/package-lock.json
generated
@@ -2722,9 +2722,9 @@
|
|||||||
"integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ="
|
"integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ="
|
||||||
},
|
},
|
||||||
"marked": {
|
"marked": {
|
||||||
"version": "0.3.7",
|
"version": "0.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/marked/-/marked-0.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/marked/-/marked-0.3.9.tgz",
|
||||||
"integrity": "sha512-zBEP4qO1YQp5aXHt8S5wTiOv9i2X74V/LQL0zhUNvVaklt6Ywa6lChxIvS+ibYlCGgADwKwZFhjC3+XfpsvQvQ=="
|
"integrity": "sha512-nW5u0dxpXxHfkHzzrveY45gCbi+R4PaO4WRZYqZNl+vB0hVGeqlFn0aOg1c8AKL63TrNFn9Bm2UP4AdiZ9TPLw=="
|
||||||
},
|
},
|
||||||
"math-expression-evaluator": {
|
"math-expression-evaluator": {
|
||||||
"version": "1.2.17",
|
"version": "1.2.17",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"express": "^4.16.2",
|
"express": "^4.16.2",
|
||||||
"extract-text-webpack-plugin": "^3.0.2",
|
"extract-text-webpack-plugin": "^3.0.2",
|
||||||
"glob": "^7.1.2",
|
"glob": "^7.1.2",
|
||||||
"marked": "^0.3.7",
|
"marked": "^0.3.9",
|
||||||
"node-fetch": "^1.7.3",
|
"node-fetch": "^1.7.3",
|
||||||
"npm-run-all": "^4.1.2",
|
"npm-run-all": "^4.1.2",
|
||||||
"serve-static": "^1.13.1",
|
"serve-static": "^1.13.1",
|
||||||
|
|||||||
@@ -7,12 +7,13 @@
|
|||||||
|
|
||||||
<p>This is the 'about' page. There's not much here.</p>
|
<p>This is the 'about' page. There's not much here.</p>
|
||||||
|
|
||||||
<button on:click='goto("/blog/what-is-sapper")'>What is Sapper?</button>
|
<button class='goto' on:click='goto("/blog/what-is-sapper")'>What is Sapper?</button>
|
||||||
|
<button class='prefetch' on:click='goto("/blog/why-the-name")'>Why the name?</button>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Layout from './_components/Layout.html';
|
import Layout from './_components/Layout.html';
|
||||||
import { goto } from '../../../runtime.js';
|
import { goto, prefetch } from '../../../runtime.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -20,7 +21,8 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
goto
|
goto,
|
||||||
|
prefetch
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
9
test/app/routes/api/delete/[id].js
Normal file
9
test/app/routes/api/delete/[id].js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export function del(req, res) {
|
||||||
|
res.set({
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
});
|
||||||
|
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
id: req.params.id
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
preload({ params, query }) {
|
preload({ params, query }) {
|
||||||
return fetch(`/api/blog`).then(r => r.json()).then(posts => {
|
return fetch(`/api/blog/contents`).then(r => r.json()).then(posts => {
|
||||||
return { posts };
|
return { posts };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
15
test/app/routes/delete-test.html
Normal file
15
test/app/routes/delete-test.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<button class='del' on:click='del()'>delete</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
del() {
|
||||||
|
fetch(`/api/delete/42`, { method: 'DELETE' })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
window.deleted = data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -6,13 +6,15 @@
|
|||||||
<h1>Great success!</h1>
|
<h1>Great success!</h1>
|
||||||
|
|
||||||
<figure>
|
<figure>
|
||||||
<img src='/great-success.png'>
|
<img alt='borat' src='/great-success.png'>
|
||||||
<figcaption>HIGH FIVE!</figcaption>
|
<figcaption>HIGH FIVE!</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<p><strong>Try editing this file (routes/index.html) to test hot module reloading.</strong></p>
|
<p><strong>Try editing this file (routes/index.html) to test hot module reloading.</strong></p>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
<div class='hydrate-test'></div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
h1, figure, p {
|
h1, figure, p {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
preload({ url }) {
|
preload({ url }) {
|
||||||
return { url };
|
if (url) return { url };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,7 +4,11 @@
|
|||||||
export default {
|
export default {
|
||||||
preload() {
|
preload() {
|
||||||
return new Promise(fulfil => {
|
return new Promise(fulfil => {
|
||||||
window.fulfil = fulfil;
|
if (typeof window !== 'undefined') {
|
||||||
|
window.fulfil = fulfil;
|
||||||
|
} else {
|
||||||
|
fulfil({});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,6 +32,6 @@
|
|||||||
<!-- Sapper creates a <script> tag containing `templates/main.js`
|
<!-- Sapper creates a <script> tag containing `templates/main.js`
|
||||||
and anything else it needs to hydrate the app and
|
and anything else it needs to hydrate the app and
|
||||||
initialise the router -->
|
initialise the router -->
|
||||||
<script src='%sapper.main%'></script>
|
%sapper.scripts%
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { init } from '../../../runtime.js';
|
import { init } from '../../../runtime.js';
|
||||||
|
|
||||||
// `routes` is an array of route objects injected by Sapper
|
window.init = () => {
|
||||||
init(document.querySelector('#sapper'), __routes__);
|
return init(document.querySelector('#sapper'), __routes__);
|
||||||
|
};
|
||||||
window.READY = true;
|
|
||||||
@@ -1,14 +1,20 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
|
const Nightmare = require('nightmare');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const serve = require('serve-static');
|
const serve = require('serve-static');
|
||||||
const Nightmare = require('nightmare');
|
const walkSync = require('walk-sync');
|
||||||
const getPort = require('get-port');
|
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
run('production');
|
run('production');
|
||||||
run('development');
|
run('development');
|
||||||
|
|
||||||
|
Nightmare.action('page', {
|
||||||
|
title(done) {
|
||||||
|
this.evaluate_now(() => document.querySelector('h1').textContent, done);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function run(env) {
|
function run(env) {
|
||||||
describe(`env=${env}`, function () {
|
describe(`env=${env}`, function () {
|
||||||
this.timeout(20000);
|
this.timeout(20000);
|
||||||
@@ -34,7 +40,11 @@ function run(env) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const res = {
|
const res = {
|
||||||
set: (headers, value) => {
|
setHeader(header, value) {
|
||||||
|
result.headers[header] = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
set(headers, value) {
|
||||||
if (typeof headers === 'string') {
|
if (typeof headers === 'string') {
|
||||||
return res.set({ [headers]: value });
|
return res.set({ [headers]: value });
|
||||||
}
|
}
|
||||||
@@ -42,15 +52,15 @@ function run(env) {
|
|||||||
Object.assign(result.headers, headers);
|
Object.assign(result.headers, headers);
|
||||||
},
|
},
|
||||||
|
|
||||||
status: code => {
|
status(code) {
|
||||||
result.status = code;
|
result.status = code;
|
||||||
},
|
},
|
||||||
|
|
||||||
write: data => {
|
write(data) {
|
||||||
result.body += data;
|
result.body += data;
|
||||||
},
|
},
|
||||||
|
|
||||||
end: data => {
|
end(data) {
|
||||||
result.body += data;
|
result.body += data;
|
||||||
fulfil(result);
|
fulfil(result);
|
||||||
}
|
}
|
||||||
@@ -62,52 +72,66 @@ function run(env) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
before(async () => {
|
before(() => {
|
||||||
process.chdir(path.resolve(__dirname, '../app'));
|
process.chdir(path.resolve(__dirname, '../app'));
|
||||||
|
|
||||||
process.env.NODE_ENV = env;
|
process.env.NODE_ENV = env;
|
||||||
|
|
||||||
|
let exec_promise = Promise.resolve();
|
||||||
|
let sapper;
|
||||||
|
|
||||||
if (env === 'production') {
|
if (env === 'production') {
|
||||||
const cli = path.resolve(__dirname, '../../cli/index.js');
|
const cli = path.resolve(__dirname, '../../cli.js');
|
||||||
await exec(`${cli} build`);
|
exec_promise = exec(`node ${cli} export`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolved = require.resolve('../..');
|
return exec_promise.then(() => {
|
||||||
delete require.cache[resolved];
|
const resolved = require.resolve('../../middleware.js');
|
||||||
const sapper = require(resolved);
|
delete require.cache[resolved];
|
||||||
|
delete require.cache[require.resolve('../../core.js')]; // TODO remove this
|
||||||
|
|
||||||
PORT = await getPort();
|
sapper = require(resolved);
|
||||||
base = `http://localhost:${PORT}`;
|
|
||||||
|
|
||||||
global.fetch = (url, opts) => {
|
return require('get-port')();
|
||||||
if (url[0] === '/') url = `${base}${url}`;
|
}).then(port => {
|
||||||
return fetch(url, opts);
|
PORT = port;
|
||||||
};
|
base = `http://localhost:${PORT}`;
|
||||||
|
|
||||||
let captured;
|
Nightmare.action('init', function(done) {
|
||||||
capture = async fn => {
|
this.evaluate_now(() => window.init(), done);
|
||||||
const result = captured = [];
|
});
|
||||||
await fn();
|
|
||||||
captured = null;
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const app = express();
|
global.fetch = (url, opts) => {
|
||||||
|
if (url[0] === '/') url = `${base}${url}`;
|
||||||
|
return fetch(url, opts);
|
||||||
|
};
|
||||||
|
|
||||||
app.use(serve('assets'));
|
let captured;
|
||||||
|
capture = fn => {
|
||||||
|
const result = captured = [];
|
||||||
|
return fn().then(() => {
|
||||||
|
captured = null;
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
const app = express();
|
||||||
if (captured) captured.push(req);
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
middleware = sapper();
|
app.use(serve('assets'));
|
||||||
app.use(middleware);
|
|
||||||
|
|
||||||
return new Promise((fulfil, reject) => {
|
app.use((req, res, next) => {
|
||||||
server = app.listen(PORT, err => {
|
if (captured) captured.push(req);
|
||||||
if (err) reject(err);
|
next();
|
||||||
else fulfil();
|
});
|
||||||
|
|
||||||
|
middleware = sapper();
|
||||||
|
app.use(middleware);
|
||||||
|
|
||||||
|
return new Promise((fulfil, reject) => {
|
||||||
|
server = app.listen(PORT, err => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else fulfil();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -137,175 +161,284 @@ function run(env) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(() => {
|
||||||
await nightmare.end();
|
return nightmare.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('serves /', async () => {
|
it('serves /', () => {
|
||||||
const title = await nightmare
|
return nightmare.goto(base).page.title().then(title => {
|
||||||
.goto(base)
|
assert.equal(title, 'Great success!');
|
||||||
.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 () => {
|
it('serves /?', () => {
|
||||||
await nightmare
|
return nightmare.goto(`${base}?`).page.title().then(title => {
|
||||||
|
assert.equal(title, 'Great success!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serves static route', () => {
|
||||||
|
return nightmare.goto(`${base}/about`).page.title().then(title => {
|
||||||
|
assert.equal(title, 'About this site');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serves dynamic route', () => {
|
||||||
|
return nightmare.goto(`${base}/blog/what-is-sapper`).page.title().then(title => {
|
||||||
|
assert.equal(title, 'What is Sapper?');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('navigates to a new page without reloading', () => {
|
||||||
|
return nightmare.goto(base).init().wait(100)
|
||||||
|
.then(() => {
|
||||||
|
return capture(() => nightmare.click('a[href="/about"]'));
|
||||||
|
})
|
||||||
|
.then(requests => {
|
||||||
|
assert.deepEqual(requests.map(r => r.url), []);
|
||||||
|
return nightmare.path();
|
||||||
|
})
|
||||||
|
.then(path => {
|
||||||
|
assert.equal(path, '/about');
|
||||||
|
return nightmare.title();
|
||||||
|
})
|
||||||
|
.then(title => {
|
||||||
|
assert.equal(title, 'About');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('navigates programmatically', () => {
|
||||||
|
return nightmare
|
||||||
.goto(`${base}/about`)
|
.goto(`${base}/about`)
|
||||||
.wait(() => window.READY)
|
.init()
|
||||||
.click('button')
|
.click('.goto')
|
||||||
.wait(() => window.location.pathname === '/blog/what-is-sapper')
|
.wait(() => window.location.pathname === '/blog/what-is-sapper')
|
||||||
.wait(100);
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
await nightmare.evaluate(() => document.title),
|
|
||||||
'What is Sapper?'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('scrolls to active deeplink', async () => {
|
|
||||||
const scrollY = await nightmare
|
|
||||||
.goto(`${base}/blog/a-very-long-post#four`)
|
|
||||||
.wait(() => window.READY)
|
|
||||||
.wait(100)
|
.wait(100)
|
||||||
.evaluate(() => window.scrollY);
|
.title()
|
||||||
|
.then(title => {
|
||||||
assert.ok(scrollY > 0, scrollY);
|
assert.equal(title, 'What is Sapper?');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reuses prefetch promise', async () => {
|
it('prefetches programmatically', () => {
|
||||||
await nightmare
|
return nightmare
|
||||||
|
.goto(`${base}/about`)
|
||||||
|
.init()
|
||||||
|
.then(() => {
|
||||||
|
return capture(() => {
|
||||||
|
return nightmare
|
||||||
|
.click('.prefetch')
|
||||||
|
.wait(100);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(requests => {
|
||||||
|
assert.ok(!!requests.find(r => r.url === '/api/blog/why-the-name'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scrolls to active deeplink', () => {
|
||||||
|
return nightmare
|
||||||
|
.goto(`${base}/blog/a-very-long-post#four`)
|
||||||
|
.init()
|
||||||
|
.evaluate(() => window.scrollY)
|
||||||
|
.then(scrollY => {
|
||||||
|
assert.ok(scrollY > 0, scrollY);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reuses prefetch promise', () => {
|
||||||
|
return nightmare
|
||||||
.goto(`${base}/blog`)
|
.goto(`${base}/blog`)
|
||||||
.wait(() => window.READY)
|
.init().wait(100)
|
||||||
.wait(200);
|
.then(() => {
|
||||||
|
return capture(() => {
|
||||||
|
return nightmare
|
||||||
|
.mouseover('[href="/blog/what-is-sapper"]')
|
||||||
|
.wait(200);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(mouseover_requests => {
|
||||||
|
assert.deepEqual(mouseover_requests.map(r => r.url), [
|
||||||
|
'/api/blog/what-is-sapper'
|
||||||
|
]);
|
||||||
|
|
||||||
const mouseover_requests = (await capture(async () => {
|
return capture(() => {
|
||||||
await nightmare
|
return nightmare
|
||||||
.mouseover('[href="/blog/what-is-sapper"]')
|
.click('[href="/blog/what-is-sapper"]')
|
||||||
.wait(200);
|
.wait(200);
|
||||||
})).map(r => r.url);
|
});
|
||||||
|
})
|
||||||
assert.deepEqual(mouseover_requests, [
|
.then(click_requests => {
|
||||||
'/api/blog/what-is-sapper'
|
assert.deepEqual(click_requests.map(r => r.url), []);
|
||||||
]);
|
});
|
||||||
|
|
||||||
const click_requests = (await capture(async () => {
|
|
||||||
await nightmare
|
|
||||||
.click('[href="/blog/what-is-sapper"]')
|
|
||||||
.wait(200);
|
|
||||||
})).map(r => r.url);
|
|
||||||
|
|
||||||
assert.deepEqual(click_requests, []);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cancels navigation if subsequent navigation occurs during preload', async () => {
|
it('cancels navigation if subsequent navigation occurs during preload', () => {
|
||||||
await nightmare
|
return nightmare
|
||||||
.goto(base)
|
.goto(base)
|
||||||
.wait(() => window.READY)
|
.init()
|
||||||
.click('a[href="/slow-preload"]')
|
.click('a[href="/slow-preload"]')
|
||||||
.wait(100)
|
.wait(100)
|
||||||
.click('a[href="/about"]')
|
.click('a[href="/about"]')
|
||||||
.wait(100);
|
.wait(100)
|
||||||
|
.then(() => nightmare.path())
|
||||||
assert.equal(
|
.then(path => {
|
||||||
await nightmare.path(),
|
assert.equal(path, '/about');
|
||||||
'/about'
|
return nightmare.title();
|
||||||
);
|
})
|
||||||
|
.then(title => {
|
||||||
assert.equal(
|
assert.equal(title, 'About');
|
||||||
await nightmare.evaluate(() => document.querySelector('h1').textContent),
|
return nightmare.evaluate(() => window.fulfil({})).wait(100);
|
||||||
'About this site'
|
})
|
||||||
);
|
.then(() => nightmare.path())
|
||||||
|
.then(path => {
|
||||||
await nightmare
|
assert.equal(path, '/about');
|
||||||
.evaluate(() => window.fulfil({}))
|
return nightmare.title();
|
||||||
.wait(100);
|
})
|
||||||
|
.then(title => {
|
||||||
assert.equal(
|
assert.equal(title, 'About');
|
||||||
await nightmare.path(),
|
});
|
||||||
'/about'
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
await nightmare.evaluate(() => document.querySelector('h1').textContent),
|
|
||||||
'About this site'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes entire request object to preload', async () => {
|
it('passes entire request object to preload', () => {
|
||||||
const html = await nightmare
|
return nightmare
|
||||||
.goto(`${base}/show-url`)
|
.goto(`${base}/show-url`)
|
||||||
.evaluate(() => document.querySelector('p').innerHTML);
|
.init()
|
||||||
|
.evaluate(() => document.querySelector('p').innerHTML)
|
||||||
|
.end().then(html => {
|
||||||
|
assert.equal(html, `URL is /show-url`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
assert.equal(html, `URL is /show-url`);
|
it('calls a delete handler', () => {
|
||||||
|
return nightmare
|
||||||
|
.goto(`${base}/delete-test`)
|
||||||
|
.init()
|
||||||
|
.click('.del')
|
||||||
|
.wait(() => window.deleted)
|
||||||
|
.evaluate(() => window.deleted.id)
|
||||||
|
.then(id => {
|
||||||
|
assert.equal(id, 42);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hydrates initial route', () => {
|
||||||
|
return nightmare.goto(base)
|
||||||
|
.wait('.hydrate-test')
|
||||||
|
.evaluate(() => {
|
||||||
|
window.el = document.querySelector('.hydrate-test');
|
||||||
|
})
|
||||||
|
.init()
|
||||||
|
.evaluate(() => {
|
||||||
|
return document.querySelector('.hydrate-test') === window.el;
|
||||||
|
})
|
||||||
|
.then(matches => {
|
||||||
|
assert.ok(matches);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('headers', () => {
|
describe('headers', () => {
|
||||||
it('sets Content-Type and Link...preload headers', async () => {
|
it('sets Content-Type and Link...preload headers', () => {
|
||||||
const { headers } = await get('/');
|
return get('/').then(({ headers }) => {
|
||||||
|
assert.equal(
|
||||||
|
headers['Content-Type'],
|
||||||
|
'text/html'
|
||||||
|
);
|
||||||
|
|
||||||
assert.equal(
|
assert.ok(
|
||||||
headers['Content-Type'],
|
/<\/client\/main.\w+\.js>;rel="preload";as="script", <\/client\/_.\d+.\w+.js>;rel="preload";as="script"/.test(headers['Link']),
|
||||||
'text/html'
|
headers['Link']
|
||||||
);
|
);
|
||||||
|
});
|
||||||
assert.ok(
|
|
||||||
/<\/client\/main.\w+\.js>;rel="preload";as="script", <\/client\/_.\d+.\w+.js>;rel="preload";as="script"/.test(headers['Link']),
|
|
||||||
headers['Link']
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (env === 'production') {
|
||||||
|
describe('export', () => {
|
||||||
|
it('export all pages', () => {
|
||||||
|
const dest = path.resolve(__dirname, '../app/dist');
|
||||||
|
|
||||||
|
// Pages that should show up in the extraction directory.
|
||||||
|
const expectedPages = [
|
||||||
|
'index.html',
|
||||||
|
'about/index.html',
|
||||||
|
'slow-preload/index.html',
|
||||||
|
|
||||||
|
'blog/index.html',
|
||||||
|
'blog/a-very-long-post/index.html',
|
||||||
|
'blog/how-can-i-get-involved/index.html',
|
||||||
|
'blog/how-is-sapper-different-from-next/index.html',
|
||||||
|
'blog/how-to-use-sapper/index.html',
|
||||||
|
'blog/what-is-sapper/index.html',
|
||||||
|
'blog/why-the-name/index.html',
|
||||||
|
|
||||||
|
'api/blog/contents',
|
||||||
|
'api/blog/a-very-long-post',
|
||||||
|
'api/blog/how-can-i-get-involved',
|
||||||
|
'api/blog/how-is-sapper-different-from-next',
|
||||||
|
'api/blog/how-to-use-sapper',
|
||||||
|
'api/blog/what-is-sapper',
|
||||||
|
'api/blog/why-the-name',
|
||||||
|
|
||||||
|
'favicon.png',
|
||||||
|
'global.css',
|
||||||
|
'great-success.png',
|
||||||
|
'manifest.json',
|
||||||
|
'service-worker.js',
|
||||||
|
'svelte-logo-192.png',
|
||||||
|
'svelte-logo-512.png',
|
||||||
|
];
|
||||||
|
// Client scripts that should show up in the extraction directory.
|
||||||
|
const expectedClientRegexes = [
|
||||||
|
/client\/_\..*?\.js/,
|
||||||
|
/client\/about\..*?\.js/,
|
||||||
|
/client\/blog_\$slug\$\..*?\.js/,
|
||||||
|
/client\/blog\..*?\.js/,
|
||||||
|
/client\/main\..*?\.js/,
|
||||||
|
/client\/show_url\..*?\.js/,
|
||||||
|
/client\/slow_preload\..*?\.js/,
|
||||||
|
];
|
||||||
|
const allPages = walkSync(dest);
|
||||||
|
|
||||||
|
expectedPages.forEach((expectedPage) => {
|
||||||
|
assert.ok(allPages.includes(expectedPage),
|
||||||
|
`Could not find page matching ${expectedPage}`);
|
||||||
|
});
|
||||||
|
expectedClientRegexes.forEach((expectedRegex) => {
|
||||||
|
// Ensure each client page regular expression matches at least one
|
||||||
|
// generated page.
|
||||||
|
let matched = false;
|
||||||
|
for (const page of allPages) {
|
||||||
|
if (expectedRegex.test(page)) {
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.ok(matched,
|
||||||
|
`Could not find client page matching ${expectedRegex}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function exec(cmd) {
|
function exec(cmd) {
|
||||||
return new Promise((fulfil, reject) => {
|
return new Promise((fulfil, reject) => {
|
||||||
require('child_process').exec(cmd, (err, stdout, stderr) => {
|
const parts = cmd.split(' ');
|
||||||
if (err) {
|
const proc = require('child_process').spawn(parts.shift(), parts);
|
||||||
process.stdout.write(stdout);
|
|
||||||
process.stderr.write(stderr);
|
|
||||||
|
|
||||||
return reject(err);
|
proc.stdout.on('data', data => {
|
||||||
}
|
process.stdout.write(data);
|
||||||
|
|
||||||
fulfil();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
proc.stderr.on('data', data => {
|
||||||
|
process.stderr.write(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on('error', reject);
|
||||||
|
|
||||||
|
proc.on('close', () => fulfil());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
const path = require('path');
|
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
|
const { create_routes } = require('../../core.js');
|
||||||
const create_routes = require('../../lib/utils/create_routes.js');
|
|
||||||
|
|
||||||
describe('create_routes', () => {
|
describe('create_routes', () => {
|
||||||
it('sorts routes correctly', () => {
|
it('sorts routes correctly', () => {
|
||||||
const routes = create_routes(['index.html', 'about.html', '[wildcard].html', 'post/foo.html', 'post/[id].html', 'post/bar.html']);
|
const routes = create_routes({
|
||||||
|
files: ['index.html', 'about.html', '[wildcard].html', 'post/foo.html', 'post/[id].html', 'post/bar.html']
|
||||||
|
});
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
routes.map(r => r.file),
|
routes.map(r => r.file),
|
||||||
@@ -21,7 +21,9 @@ describe('create_routes', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('generates params', () => {
|
it('generates params', () => {
|
||||||
const routes = create_routes(['index.html', 'about.html', '[wildcard].html', 'post/[id].html']);
|
const routes = create_routes({
|
||||||
|
files: ['index.html', 'about.html', '[wildcard].html', 'post/[id].html']
|
||||||
|
});
|
||||||
|
|
||||||
let file;
|
let file;
|
||||||
let params;
|
let params;
|
||||||
@@ -40,7 +42,9 @@ describe('create_routes', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('ignores files and directories with leading underscores', () => {
|
it('ignores files and directories with leading underscores', () => {
|
||||||
const routes = create_routes(['index.html', '_foo.html', 'a/_b/c/d.html', 'e/f/g/h.html', 'i/_j.html']);
|
const routes = create_routes({
|
||||||
|
files: ['index.html', '_foo.html', 'a/_b/c/d.html', 'e/f/g/h.html', 'i/_j.html']
|
||||||
|
});
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
routes.map(r => r.file),
|
routes.map(r => r.file),
|
||||||
@@ -52,8 +56,12 @@ describe('create_routes', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('matches /foo/:bar before /:baz/qux', () => {
|
it('matches /foo/:bar before /:baz/qux', () => {
|
||||||
const a = create_routes(['foo/[bar].html', '[baz]/qux.html']);
|
const a = create_routes({
|
||||||
const b = create_routes(['[baz]/qux.html', 'foo/[bar].html']);
|
files: ['foo/[bar].html', '[baz]/qux.html']
|
||||||
|
});
|
||||||
|
const b = create_routes({
|
||||||
|
files: ['[baz]/qux.html', 'foo/[bar].html']
|
||||||
|
});
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
a.map(r => r.file),
|
a.map(r => r.file),
|
||||||
@@ -68,12 +76,22 @@ describe('create_routes', () => {
|
|||||||
|
|
||||||
it('fails if routes are indistinguishable', () => {
|
it('fails if routes are indistinguishable', () => {
|
||||||
assert.throws(() => {
|
assert.throws(() => {
|
||||||
create_routes(['[foo].html', '[bar]/index.html']);
|
create_routes({
|
||||||
|
files: ['[foo].html', '[bar]/index.html']
|
||||||
|
});
|
||||||
}, /The \[foo\].html and \[bar\]\/index.html routes clash/);
|
}, /The \[foo\].html and \[bar\]\/index.html routes clash/);
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
create_routes({
|
||||||
|
files: ['foo.html', 'foo.js']
|
||||||
|
});
|
||||||
|
}, /The foo.html and foo.js routes clash/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('matches nested routes', () => {
|
it('matches nested routes', () => {
|
||||||
const route = create_routes(['settings/[submenu].html'])[0];
|
const route = create_routes({
|
||||||
|
files: ['settings/[submenu].html']
|
||||||
|
})[0];
|
||||||
|
|
||||||
assert.deepEqual(route.exec('/settings/foo'), {
|
assert.deepEqual(route.exec('/settings/foo'), {
|
||||||
submenu: 'foo'
|
submenu: 'foo'
|
||||||
@@ -85,7 +103,9 @@ describe('create_routes', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('prefers index routes to nested routes', () => {
|
it('prefers index routes to nested routes', () => {
|
||||||
const routes = create_routes(['settings/[submenu].html', 'settings.html']);
|
const routes = create_routes({
|
||||||
|
files: ['settings/[submenu].html', 'settings.html']
|
||||||
|
});
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
routes.map(r => r.file),
|
routes.map(r => r.file),
|
||||||
@@ -94,7 +114,9 @@ describe('create_routes', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('matches deeply nested routes', () => {
|
it('matches deeply nested routes', () => {
|
||||||
const route = create_routes(['settings/[a]/[b]/index.html'])[0];
|
const route = create_routes({
|
||||||
|
files: ['settings/[a]/[b]/index.html']
|
||||||
|
})[0];
|
||||||
|
|
||||||
assert.deepEqual(route.exec('/settings/foo/bar'), {
|
assert.deepEqual(route.exec('/settings/foo/bar'), {
|
||||||
a: 'foo',
|
a: 'foo',
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
import 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000';
|
|
||||||
Reference in New Issue
Block a user