Compare commits

...

122 Commits

Author SHA1 Message Date
Rich Harris
8e79e706e6 -> v0.2.6 2017-12-21 11:56:04 -05:00
Rich Harris
4b495f44fd Merge pull request #42 from sveltejs/gh-40
render main.js back to templates, to allow relative imports
2017-12-21 11:54:53 -05:00
Rich Harris
222a750b7b render main.js back to templates, to allow relative imports - fixes #40 2017-12-21 11:38:03 -05:00
Rich Harris
5b214c964c -> v0.2.5 2017-12-21 08:59:00 -05:00
Rich Harris
95f99fd378 Merge pull request #39 from sveltejs/windows-nested-routes
use /, not path.sep, when creating routes
2017-12-21 08:55:41 -05:00
Rich Harris
1bed4b0670 use /, not path.sep, when creating routes 2017-12-21 08:46:40 -05:00
Rich Harris
9d4890913a Merge pull request #37 from sveltejs/gh-33
error if routes clash
2017-12-20 21:00:04 -05:00
Rich Harris
f50d3c4262 error if routes clash - fixes #33 2017-12-20 20:46:28 -05:00
Rich Harris
8925e541d5 Merge pull request #36 from sveltejs/gh-19
stream responses
2017-12-20 18:50:06 -05:00
Rich Harris
a48afb77d3 stream responses - fixes #19 2017-12-20 18:39:39 -05:00
Rich Harris
45e845ee92 Merge pull request #35 from sveltejs/gh-2
add preload header for main.js and current route
2017-12-20 18:13:07 -05:00
Rich Harris
492f024d2a add preload header for main.js and current route - fixes #2 2017-12-20 18:04:11 -05:00
Rich Harris
8d40992cf1 Merge pull request #34 from sveltejs/gh-13
Rebundle when routes/templates change
2017-12-20 17:51:04 -05:00
Rich Harris
4232f75b19 rebundle when main.js changes 2017-12-20 17:36:22 -05:00
Rich Harris
fefb0d96d7 rebundle when routes and templates change 2017-12-20 17:34:23 -05:00
Rich Harris
cd91bf2ca4 -> v0.2.4 2017-12-20 16:33:54 -05:00
Rich Harris
7466e8da82 -> v0.2.3 2017-12-20 09:31:51 -05:00
Rich Harris
463307db86 pass URL to 404 handler 2017-12-20 08:29:28 -05:00
Rich Harris
2a68394dce Merge pull request #31 from sveltejs/possible-windows-fix-3
posixify import paths
2017-12-20 08:28:32 -05:00
Rich Harris
c8fe0679ae posixify import paths 2017-12-19 20:48:44 -05:00
Rich Harris
51d45cf38f -> v0.2.2 2017-12-19 08:55:40 -05:00
Rich Harris
92fbc28e11 make dest dir in buid as well as dev 2017-12-19 08:54:25 -05:00
Rich Harris
c1b1b3ed63 add some tests - closes #10 2017-12-18 21:39:17 -05:00
Rich Harris
cd8b9ddb14 -> v0.2.1 2017-12-18 17:11:17 -05:00
Rich Harris
924855d248 Merge pull request #26 from sveltejs/gh-14
inject HMR import
2017-12-18 17:08:08 -05:00
Rich Harris
8b516ef9bd inject HMR import - fixes #14 2017-12-18 17:07:17 -05:00
Rich Harris
8604088f3d -> v0.2.0 2017-12-18 15:48:28 -05:00
Rich Harris
8da3ca16ab Separate build step (closes #21) 2017-12-18 15:38:56 -05:00
Rich Harris
d0dd1d6cc9 Merge pull request #23 from sveltejs/remove-webpack-dev-middleware
remove webpack-dev-middleware
2017-12-18 12:02:27 -05:00
Rich Harris
8270463281 remove webpack-dev-middleware 2017-12-18 11:41:02 -05:00
Rich Harris
514331b5e3 -> v0.1.5 2017-12-17 21:39:55 -05:00
Rich Harris
63d39575be starting to lose my sense of humour 2017-12-17 21:39:43 -05:00
Rich Harris
3af5503009 -> v0.1.4 2017-12-17 21:34:28 -05:00
Rich Harris
c1de442dd1 argh not again 2017-12-17 21:34:16 -05:00
Rich Harris
0c6b7e3836 -> v0.1.3 2017-12-17 21:07:55 -05:00
Rich Harris
dc5e2543cb argh copypasta fail 2017-12-17 21:07:34 -05:00
Rich Harris
ecc7b80d91 -> v0.1.2 2017-12-17 20:43:52 -05:00
Rich Harris
40024e7d86 Merge pull request #18 from sveltejs/make-dest-dir
create dest dir immediately - should fix some CI failures
2017-12-17 20:41:55 -05:00
Rich Harris
6f71f7ad4d Merge pull request #17 from sveltejs/gh-11
use atime.getTime() and mtime.getTime() - hopefully fixes #11
2017-12-17 20:41:30 -05:00
Rich Harris
6eb99b195e use atime.getTime() and mtime.getTime() - hopefully fixes #11 2017-12-17 20:39:18 -05:00
Rich Harris
9e08fee9a1 create dest dir immediately - should fix some CI failures 2017-12-17 20:24:58 -05:00
Rich Harris
442ce366e2 -> v0.1.1 2017-12-17 15:18:03 -05:00
Rich Harris
dc929fcd83 inject app path, since dest might be /tmp causing resolution failure 2017-12-17 15:17:23 -05:00
Rich Harris
2dc246398b -> v0.1.0 2017-12-17 15:05:51 -05:00
Rich Harris
b7ac067459 always write to .sapper, not templates, since we cant guarantee fs access 2017-12-17 15:04:20 -05:00
Rich Harris
8b50ff34b8 named exports, for tree-shakeability when we add new stuff 2017-12-17 15:01:26 -05:00
Rich Harris
62abdb2a87 remove magic return values from server routes 2017-12-17 15:01:05 -05:00
Rich Harris
34d0bae4a1 -> v0.0.22 2017-12-16 20:31:37 -05:00
Rich Harris
4f0b336627 fix prod mode 2017-12-16 20:30:14 -05:00
Rich Harris
e71bf298fb inject __dev__ variable 2017-12-16 20:19:02 -05:00
Rich Harris
e4936375db support HMR 2017-12-16 20:10:49 -05:00
Rich Harris
08ff7ad234 shuffle things around 2017-12-16 17:19:28 -05:00
Rich Harris
5995b7ae6a run webpack in watch mode during dev 2017-12-16 16:50:46 -05:00
Rich Harris
71ed3864b7 run webpack in watch mode 2017-12-16 16:24:36 -05:00
Rich Harris
bd7f6e2b1a expose main.js 2017-12-16 15:40:22 -05:00
Rich Harris
dd1f2d79ff -> v0.0.21 2017-12-16 12:18:34 -05:00
Rich Harris
dccd3cdeb0 prevent default when navigating to current location 2017-12-16 10:59:46 -05:00
Rich Harris
b3b5d9f352 handle hashchange correctly 2017-12-16 08:53:42 -05:00
Rich Harris
10ddaeb7a3 dont navigate on hashchange 2017-12-16 08:16:09 -05:00
Rich Harris
060f9b2f5e tidy up dependencies 2017-12-16 08:07:40 -05:00
Rich Harris
32dfa94247 dont include directories in cache manifest 2017-12-16 08:07:22 -05:00
Rich Harris
797cc3cde1 remove esm stuff - no longer used 2017-12-16 08:07:10 -05:00
Rich Harris
9eca90067c -> v0.0.20 2017-12-15 18:37:19 -05:00
Rich Harris
57f293e872 dont cache shell/sw in dev 2017-12-15 18:37:08 -05:00
Rich Harris
7e65c481d8 doh 2017-12-15 18:34:38 -05:00
Rich Harris
0fe93cd177 -> v0.0.19 2017-12-15 18:27:04 -05:00
Rich Harris
67fe570f6d dont try to prevent event where none exists 2017-12-15 18:26:55 -05:00
Rich Harris
a3d44aba31 -> v0.0.18 2017-12-15 17:20:36 -05:00
Rich Harris
80ae909b73 serve assets from memory, use caching 2017-12-15 17:20:24 -05:00
Rich Harris
892b18cf80 -> v0.0.17 2017-12-15 16:47:34 -05:00
Rich Harris
0eb96bf01f oops, remove logging 2017-12-15 16:47:29 -05:00
Rich Harris
419f5c5235 -> v0.0.16 2017-12-15 16:46:27 -05:00
Rich Harris
4c61ed5fdd better link click handling, track scroll position 2017-12-15 16:46:11 -05:00
Rich Harris
c19447cf05 -> v0.0.15 2017-12-15 15:29:30 -05:00
Rich Harris
cb2364f476 add noscript message (todo - make it configurable) 2017-12-15 15:29:08 -05:00
Rich Harris
de427d400e -> v0.0.14 2017-12-15 14:49:56 -05:00
Rich Harris
e810ead93f move files around 2017-12-15 14:46:23 -05:00
Rich Harris
f5a19ef34b tweak service worker stuff 2017-12-15 14:43:38 -05:00
Rich Harris
b8c03d330b refactor some webpack stuff, support service worker generation 2017-12-14 15:38:41 -05:00
Rich Harris
6e769496ec -> v0.0.13 2017-12-14 08:23:00 -05:00
Rich Harris
e46aceb2fe implement prefetching anchors with rel=prefetch 2017-12-14 08:21:32 -05:00
Rich Harris
a87cac2481 refactor some stuff 2017-12-14 07:53:17 -05:00
Rich Harris
608fdb7533 -> v0.0.12 2017-12-14 07:15:07 -05:00
Rich Harris
80166b5a7d -> v0.0.11 2017-12-14 07:12:01 -05:00
Rich Harris
24b259f80b fix location, so that runtime can be found from /tmp 2017-12-14 07:11:52 -05:00
Rich Harris
8a9f4bd268 -> v0.0.10 2017-12-14 07:07:47 -05:00
Rich Harris
d940da1a77 serve errors 2017-12-14 07:07:29 -05:00
Rich Harris
91269f5705 -> v0.0.9 2017-12-13 22:38:18 -05:00
Rich Harris
80a9818e95 dont assume error was caught 2017-12-13 22:38:09 -05:00
Rich Harris
478ccf53cc -> v0.0.8 2017-12-13 22:33:24 -05:00
Rich Harris
b32278e88b argh 2017-12-13 22:33:12 -05:00
Rich Harris
881d411db0 -> v0.0.7 2017-12-13 22:30:15 -05:00
Rich Harris
2a5786d7d7 -> v0.0.6 2017-12-13 22:22:03 -05:00
Rich Harris
8b89a5f27e make now-friendly 2017-12-13 22:21:55 -05:00
Rich Harris
d46a270cf0 remove requires 2017-12-13 22:11:28 -05:00
Rich Harris
72efde9515 -> v0.0.5 2017-12-13 22:08:29 -05:00
Rich Harris
7c0789cabf move webpack config out of sapper, into the app 2017-12-13 21:35:56 -05:00
Rich Harris
bffffe0035 detach SSRd <head> contents 2017-12-13 20:24:58 -05:00
Rich Harris
22d3cb2b1e serve 404 pages etc 2017-12-13 14:22:46 -05:00
Rich Harris
e1ed1896e6 easier templates 2017-12-13 14:14:06 -05:00
Rich Harris
6d4c26d15d server-side preloading, and critical CSS rendering 2017-12-13 13:30:27 -05:00
Rich Harris
941867f0a4 add client-side preloading logic, move router into runtime module 2017-12-13 13:29:38 -05:00
Rich Harris
c7e3fc4493 handle errors. (TODO: handle errors *nicely*) 2017-12-13 12:24:12 -05:00
Rich Harris
a8373c1568 always create valid route id 2017-12-13 12:23:49 -05:00
Rich Harris
db4223133e -> v0.0.4 2017-12-12 11:42:35 -05:00
Rich Harris
ef3a3e83e8 install rimraf 2017-12-12 11:42:18 -05:00
Rich Harris
86292d119b create all routes simultaneously, differentiate with type property 2017-12-12 11:42:06 -05:00
Rich Harris
2549477e05 rename create_matchers -> create_routes 2017-12-12 11:41:45 -05:00
Rich Harris
cf0ab4b9c7 bundle server code as well 2017-12-12 11:41:18 -05:00
Rich Harris
58768ae27d make selector customisable 2017-12-12 06:19:53 -05:00
Rich Harris
fa70024a92 dont treat files and dirs with leading _ as routes 2017-12-12 06:19:28 -05:00
Rich Harris
33fcb865c8 fix navigation and ESM stuff 2017-12-11 18:05:07 -05:00
Rich Harris
c6778c961b add devDependencies 2017-12-11 17:23:30 -05:00
Rich Harris
cea14b4b53 tidy up 2017-12-11 17:23:22 -05:00
Rich Harris
12661449c2 fix deps 2017-12-11 17:05:16 -05:00
Rich Harris
2f51435d93 render with params 2017-12-11 17:05:01 -05:00
Rich Harris
642c2904df switch to using [param].html style filenames 2017-12-11 17:04:21 -05:00
Rich Harris
727782aca2 -> v0.0.3 2017-12-11 13:13:00 -05:00
Rich Harris
b6f789e50c use absolute path 2017-12-11 13:12:51 -05:00
Rich Harris
e723f781a8 -> v0.0.2 2017-12-11 13:08:35 -05:00
Rich Harris
a22e28a11f start generating client-side bundle. WIP 2017-12-11 13:08:25 -05:00
Rich Harris
ad8a410ba4 update README 2017-12-11 10:39:17 -05:00
64 changed files with 15806 additions and 208 deletions

39
.eslintrc.json Normal file
View File

@@ -0,0 +1,39 @@
{
"root": true,
"rules": {
"indent": [ 2, "tab", { "SwitchCase": 1 } ],
"semi": [ 2, "always" ],
"space-before-blocks": [ 2, "always" ],
"no-mixed-spaces-and-tabs": [ 2, "smart-tabs" ],
"no-cond-assign": 0,
"no-unused-vars": 2,
"object-shorthand": [ 2, "always" ],
"no-const-assign": 2,
"no-class-assign": 2,
"no-this-before-super": 2,
"no-var": 2,
"no-unreachable": 2,
"valid-typeof": 2,
"quote-props": [ 2, "as-needed" ],
"one-var": [ 2, "never" ],
"prefer-arrow-callback": 2,
"prefer-const": [ 2, { "destructuring": "all" } ],
"arrow-spacing": 2,
"no-inner-declarations": 0
},
"env": {
"es6": true,
"browser": true,
"node": true,
"mocha": true
},
"extends": [
"eslint:recommended",
"plugin:import/errors",
"plugin:import/warnings"
],
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
}
}

4
.gitignore vendored
View File

@@ -1,2 +1,4 @@
.DS_Store
node_modules
node_modules
cypress/screenshots
test/app/.sapper

10
.travis.yml Normal file
View File

@@ -0,0 +1,10 @@
sudo: false
language: node_js
node_js:
- "stable"
env:
global:
- BUILD_TIMEOUT=10000
install:
- npm install
- (cd test/app && npm install)

52
CHANGELOG.md Normal file
View File

@@ -0,0 +1,52 @@
# sapper changelog
## 0.2.6
* Render main.js back to templates, to allow relative imports ([#40](https://github.com/sveltejs/sapper/issues/40))
## 0.2.5
* Fix nested routes on Windows ([#39](https://github.com/sveltejs/sapper/pull/39))
* Rebundle when routes and main.js change ([#34](https://github.com/sveltejs/sapper/pull/34))
* Add `Link...preload` headers for JavaScript assets ([#2](https://github.com/sveltejs/sapper/issues/2))
* Stream document up to first dynamic content ([#19](https://github.com/sveltejs/sapper/issues/19))
* Error if routes clash ([#33](https://github.com/sveltejs/sapper/issues/33))
## 0.2.4
* Posixify path to HMR client
## 0.2.3
* Posixify import paths, even on Windows ([#31](https://github.com/sveltejs/sapper/pull/31))
* Pass `url` to 404 handler
## 0.2.2
* Create destination directory when building, don't assume it's already there from dev mode
* We have tests now!
## 0.2.1
* Inject HMR logic in dev mode
## 0.2.0
* Separate `sapper build` from prod server ([#21](https://github.com/sveltejs/sapper/issues/21))
## 0.1.3-5
* Fix typo
## 0.1.2
* Use `atime.getTime()` and `mtime.getTime()` instead of `atimeMs` and `mtimeMs` ([#11](https://github.com/sveltejs/sapper/issues/11))
* Make dest dir before anyone tries to write to it ([#18](https://github.com/sveltejs/sapper/pull/18))
## 0.1.1
* Expose resolved pathname to `sapper/runtime/app.js` as `__app__` inside main.js
## 0.1.0
* First public preview

View File

@@ -16,6 +16,7 @@ High-level goals:
* 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
@@ -26,8 +27,6 @@ A Sapper app is just an Express app (conventionally, `server.js`) that uses the
const app = require('express')();
const sapper = require('sapper');
const app = express();
app.use(sapper());
const { PORT = 3000 } = process.env;
@@ -45,13 +44,13 @@ Like Next, routes are defined by the project directory structure, but with some
* 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`.
* 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
// routes/api/post/[id].js
export async function get(req, res) {
try {
const data = await getPostFromDatabase(req.params.id);
@@ -72,13 +71,71 @@ export async function get(req, res) {
Or, if you omit the `res` argument, it can use the return value:
```js
// routes/api/post/%id%.js
export async function get(req, res) {
// routes/api/post/[id].js
export async function get(req) {
return await getPostFromDatabase(req.params.id);
}
```
## Client-side app
Sapper will create (and in development mode, update) a barebones `main.js` file that dynamically imports individual routes and renders them — something like this:
```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
@@ -87,4 +144,7 @@ export async function get(req, res) {
* `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.

17
appveyor.yml Normal file
View File

@@ -0,0 +1,17 @@
version: "{build}"
shallow_clone: true
init:
- git config --global core.autocrlf false
build: off
environment:
matrix:
# node.js
- nodejs_version: stable
install:
- ps: Install-Product node $env:nodejs_version
- npm install

8
cli/index.js Executable file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env node
const cmd = process.argv[2];
if (cmd === 'build') {
process.env.NODE_ENV = 'production';
require('../lib/build.js')();
}

View File

@@ -1,67 +0,0 @@
require('svelte/ssr/register');
const esm = require('@std/esm');
const path = require('path');
const glob = require('glob');
const create_matchers = require('./utils/create_matchers.js');
require = esm(module, {
esm: 'all'
});
module.exports = function connect(opts = {}) {
const routes = path.resolve('routes');
const out = path.resolve('.sapper');
let pages = glob.sync('**/*.html', { cwd: routes });
let page_matchers = create_matchers(pages);
let server_routes = glob.sync('**/*.+(js|mjs)', { cwd: routes });
let server_route_matchers = create_matchers(server_routes);
return async function(req, res, next) {
const url = req.url.replace(/\?.+/, '');
for (let i = 0; i < page_matchers.length; i += 1) {
const matcher = page_matchers[i];
if (matcher.test(url)) {
const params = matcher.exec(url);
const Component = require(`${routes}/${matcher.file}`);
res.end(Component.render({
params,
query: req.query
}));
return;
}
}
for (let i = 0; i < server_route_matchers.length; i += 1) {
const matcher = server_route_matchers[i];
if (matcher.test(url)) {
req.params = matcher.exec(url);
const route = require(`${routes}/${matcher.file}`);
const handler = route[req.method.toLowerCase()];
if (handler) {
if (handler.length === 2) {
handler(req, res);
} else {
const data = await handler(req);
// TODO headers, error handling
if (typeof data === 'string') {
res.end(data);
} else {
res.end(JSON.stringify(data));
}
}
return;
}
}
}
next();
};
};

9
cypress.json Normal file
View File

@@ -0,0 +1,9 @@
{
"baseUrl": "http://localhost:3000",
"videoRecording": false,
"fixturesFolder": "test/cypress/fixtures",
"integrationFolder": "test/cypress/integration",
"pluginsFile": false,
"screenshotsFolder": "test/cypress/screenshots",
"supportFile": "test/cypress/support/index.js"
}

41
lib/build.js Normal file
View File

@@ -0,0 +1,41 @@
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const mkdirp = require('mkdirp');
const { client, server } = require('./utils/compilers.js');
const create_app = require('./utils/create_app.js');
const generate_asset_cache = require('./utils/generate_asset_cache.js');
const { dest } = require('./config.js');
module.exports = () => {
mkdirp(dest);
// 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);
});
});
};

24
lib/config.js Normal file
View File

@@ -0,0 +1,24 @@
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.NOW ? '/tmp' :
process.env.SAPPER_DEST || '.sapper'
);
if (exports.dev) {
mkdirp(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')
};

227
lib/index.js Normal file
View File

@@ -0,0 +1,227 @@
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const rimraf = require('rimraf');
const mkdirp = require('mkdirp');
const webpack = require('webpack');
const route_manager = require('./route_manager.js');
const templates = require('./templates.js');
const create_app = require('./utils/create_app.js');
const create_watcher = require('./utils/create_watcher.js');
const compilers = require('./utils/compilers.js');
const generate_asset_cache = require('./utils/generate_asset_cache.js');
const escape_html = require('escape-html');
const { src, dest, dev } = require('./config.js');
function connect_dev() {
create_app();
const watcher = create_watcher();
let asset_cache;
return 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)
]);
}
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'))
);
return 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)
]);
}
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"`);
let data = { params: req.params, query: req.query };
if (mod.preload) {
const promise = Promise.resolve(mod.preload(data)).then(preloaded => {
Object.assign(data, preloaded);
return mod.render(data);
});
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).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).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'));
}

30
lib/route_manager.js Normal file
View File

@@ -0,0 +1,30 @@
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
});
watcher.on('add', update);
watcher.on('change', update);
watcher.on('unlink', update);
}

89
lib/templates.js Normal file
View File

@@ -0,0 +1,89 @@
const fs = require('fs');
const glob = require('glob');
const chokidar = require('chokidar');
const { dev } = require('./config.js');
let templates;
function create_templates() {
templates = glob.sync('*.html', { cwd: 'templates' })
.map(file => {
const template = fs.readFileSync(`templates/${file}`, 'utf-8');
const status = file.replace('.html', '').toLowerCase();
if (!/^[0-9x]{3}$/.test(status)) {
throw new Error(`Bad template — should be a valid status code like 404.html, or a wildcard like 2xx.html`);
}
const specificity = (
(status[0] === 'x' ? 0 : 4) +
(status[1] === 'x' ? 0 : 2) +
(status[2] === 'x' ? 0 : 1)
);
const pattern = new RegExp(`^${status.split('').map(d => d === 'x' ? '\\d' : d).join('')}$`);
return {
test: status => pattern.test(status),
specificity,
render: data => {
return template.replace(/%sapper\.(\w+)%/g, (match, key) => {
return key in data ? data[key] : '';
});
},
stream: async (res, data) => {
let i = 0;
do {
const start = template.indexOf('%sapper', i);
if (start === -1) {
res.end(template.slice(start));
return;
}
res.write(template.slice(i, start));
const end = template.indexOf('%', start + 1);
if (end === -1) {
throw new Error(`Bad template`); // TODO validate ahead of time
}
const tag = template.slice(start + 1, end);
const match = /sapper\.(\w+)/.exec(tag);
if (!match || !(match[1] in data)) throw new Error(`Bad template`); // TODO ditto
res.write(await data[match[1]]);
i = end + 1;
} while (i < template.length);
}
}
})
.sort((a, b) => b.specificity - a.specificity);
}
create_templates();
if (dev) {
const watcher = chokidar.watch('templates/**.html', {
ignoreInitial: true
});
watcher.on('add', create_templates);
watcher.on('change', create_templates);
watcher.on('unlink', create_templates);
}
exports.render = (status, data) => {
const template = templates.find(template => template.test(status));
if (template) return template.render(data);
return `Missing template for status code ${status}`;
};
exports.stream = (res, status, data) => {
const template = templates.find(template => template.test(status));
if (template) return template.stream(res, data);
return `Missing template for status code ${status}`;
};

11
lib/utils/compilers.js Normal file
View File

@@ -0,0 +1,11 @@
const path = require('path');
const relative = require('require-relative');
const webpack = relative('webpack', process.cwd());
exports.client = webpack(
require(path.resolve('webpack.client.config.js'))
);
exports.server = webpack(
require(path.resolve('webpack.server.config.js'))
);

82
lib/utils/create_app.js Normal file
View File

@@ -0,0 +1,82 @@
const fs = require('fs');
const path = require('path');
const chokidar = require('chokidar');
const route_manager = require('../route_manager.js');
const { src, dest, 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
});
watcher.on('add', create_app);
watcher.on('change', create_app);
watcher.on('unlink', create_app);
}
module.exports = create_app;

View File

@@ -0,0 +1,77 @@
const path = require('path');
module.exports = function create_matchers(files) {
const routes = files
.map(file => {
if (/(^|\/|\\)_/.test(file)) return;
const parts = file.replace(/\.(html|js|mjs)$/, '').split('/'); // glob output is always posix-style
if (parts[parts.length - 1] === 'index') parts.pop();
const id = (
parts.join('_').replace(/[[\]]/g, '$').replace(/^\d/, '_$&').replace(/[^a-zA-Z0-9_$]/g, '_')
) || '_';
const dynamic = parts
.filter(part => part[0] === '[')
.map(part => part.slice(1, -1));
const pattern = new RegExp(
`^\\/${parts.map(p => p[0] === '[' ? '([^/]+)' : p).join('\\/')}$`
);
const test = url => pattern.test(url);
const exec = url => {
const match = pattern.exec(url);
if (!match) return;
const params = {};
dynamic.forEach((param, i) => {
params[param] = match[i + 1];
});
return params;
};
return {
id,
type: path.extname(file) === '.html' ? 'page' : 'route',
file,
pattern,
test,
exec,
parts,
dynamic
};
})
.filter(Boolean)
.sort((a, b) => {
let same = true;
for (let i = 0; true; i += 1) {
const a_part = a.parts[i];
const b_part = b.parts[i];
if (!a_part && !b_part) {
if (same) throw new Error(`The ${a.file} and ${b.file} routes clash`);
return 0;
}
if (!a_part) return 1;
if (!b_part) return -1;
const a_is_dynamic = a_part[0] === '[';
const b_is_dynamic = b_part[0] === '[';
if (a_is_dynamic === b_is_dynamic) {
if (!a_is_dynamic) same = false;
continue;
}
return a_is_dynamic ? 1 : -1;
}
});
return routes;
}

View File

@@ -0,0 +1,71 @@
const fs = require('fs');
const path = require('path');
const chalk = require('chalk');
const compilers = require('./compilers.js');
const generate_asset_cache = require('./generate_asset_cache.js');
const { dest } = require('../config.js');
function deferred() {
const d = {};
d.promise = new Promise((fulfil, reject) => {
d.fulfil = fulfil;
d.reject = reject;
});
return d;
}
module.exports = function create_watcher() {
const deferreds = {
client: deferred(),
server: deferred()
};
const invalidate = () => Promise.all([
deferreds.client.promise,
deferreds.server.promise
]).then(([client_stats, server_stats]) => {
const client_info = client_stats.toJson();
fs.writeFileSync(path.join(dest, 'stats.client.json'), JSON.stringify(client_info, null, ' '));
const server_info = server_stats.toJson();
fs.writeFileSync(path.join(dest, 'stats.server.json'), JSON.stringify(server_info, null, ' '));
return generate_asset_cache(
client_stats.toJson(),
server_stats.toJson()
);
});
watcher = {
ready: invalidate()
};
function watch_compiler(type) {
const compiler = compilers[type];
compiler.plugin('invalid', filename => {
console.log(chalk.red(`${type} bundle invalidated, file changed: ${chalk.bold(filename)}`));
deferreds[type] = deferred();
watcher.ready = invalidate();
});
compiler.plugin('failed', err => {
deferreds[type].reject(err);
});
compiler.watch({}, (err, stats) => {
if (stats.hasErrors()) {
deferreds[type].reject(stats.toJson().errors[0]);
} else {
deferreds[type].fulfil(stats);
}
});
}
watch_compiler('client');
watch_compiler('server');
return watcher;
};

View File

@@ -0,0 +1,77 @@
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const templates = require('../templates.js');
const route_manager = require('../route_manager.js');
const { dest } = require('../config.js');
function ensure_array(thing) {
return Array.isArray(thing) ? thing : [thing]; // omg webpack what the HELL are you doing
}
module.exports = function generate_asset_cache(clientInfo, serverInfo) {
const main_file = `/client/${ensure_array(clientInfo.assetsByChunkName.main)[0]}`;
const chunk_files = clientInfo.assets.map(chunk => `/client/${chunk.name}`);
const service_worker = generate_service_worker(chunk_files);
const index = generate_index(main_file);
fs.writeFileSync(path.join(dest, 'service-worker.js'), service_worker);
fs.writeFileSync(path.join(dest, 'index.html'), index);
return {
client: {
main_file,
chunk_files,
main: read(`${dest}${main_file}`),
chunks: chunk_files.reduce((lookup, file) => {
lookup[file] = read(`${dest}${file}`);
return lookup;
}, {}),
routes: route_manager.routes.reduce((lookup, route) => {
lookup[route.id] = `/client/${ensure_array(clientInfo.assetsByChunkName[route.id])[0]}`;
return lookup;
}, {}),
index,
service_worker
},
server: {
entry: path.resolve(dest, 'server', serverInfo.assetsByChunkName.main)
}
};
};
function generate_service_worker(chunk_files) {
const assets = glob.sync('**', { cwd: 'assets', nodir: true });
const route_code = `[${
route_manager.routes
.filter(route => route.type === 'page')
.map(route => `{ pattern: ${route.pattern} }`)
.join(', ')
}]`;
return read('templates/service-worker.js')
.replace('__timestamp__', Date.now())
.replace('__assets__', JSON.stringify(assets))
.replace('__shell__', JSON.stringify(chunk_files.concat('/index.html')))
.replace('__routes__', route_code);
}
function generate_index(main_file) {
return templates.render(200, {
styles: '',
head: '',
html: '<noscript>Please enable JavaScript!</noscript>',
main: main_file
});
}
function read(file) {
return fs.readFileSync(file, 'utf-8');
}

View File

@@ -1,2 +1,2 @@
--recursive
utils/**/*.test.js
test/unit/**/*.js

5200
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +1,42 @@
{
"name": "sapper",
"version": "0.0.1",
"description": "Combat-ready apps, engineered by Svelte",
"main": "connect.js",
"version": "0.2.6",
"description": "Military-grade apps, engineered by Svelte",
"main": "lib/index.js",
"bin": {
"sapper": "cli/index.js"
},
"directories": {
"test": "test"
},
"dependencies": {
"@std/esm": "^0.18.0"
"chalk": "^2.3.0",
"chokidar": "^1.7.0",
"escape-html": "^1.0.3",
"mkdirp": "^0.5.1",
"relative": "^3.0.2",
"require-relative": "^0.8.7",
"rimraf": "^2.6.2",
"webpack": "^3.10.0",
"webpack-hot-middleware": "^2.21.0"
},
"devDependencies": {
"cypress": "^1.3.0",
"eslint": "^4.13.1",
"mocha": "^4.0.1",
"svelte": "^1.47.0"
"npm-run-all": "^4.1.2",
"wait-on": "^2.0.2"
},
"scripts": {
"test": "mocha --opts mocha.opts"
"cy:open": "cypress open",
"test": "run-s test:unit test:dev test:prod",
"test:unit": "mocha --opts mocha.opts",
"test:dev": "run-p --race test:launch:dev cy:run:dev",
"test:launch:dev": "node test/launch.js --dev",
"cy:run:dev": "wait-on http://localhost:3000 && cypress run -s test/cypress/integration/dev.js",
"test:prod": "run-p --race test:launch:prod cy:run:prod",
"test:launch:prod": "node test/launch.js --prod",
"cy:run:prod": "wait-on http://localhost:3000 && cypress run -s test/cypress/integration/prod.js"
},
"repository": "https://github.com/sveltejs/sapper",
"keywords": [
@@ -29,8 +51,5 @@
"bugs": {
"url": "https://github.com/sveltejs/sapper/issues"
},
"homepage": "https://github.com/sveltejs/sapper#readme",
"@std/esm": {
"esm": "js"
}
"homepage": "https://github.com/sveltejs/sapper#readme"
}

192
runtime/app.js Normal file
View File

@@ -0,0 +1,192 @@
const detach = node => {
node.parentNode.removeChild(node);
};
export let component;
let target;
let routes;
const scroll_history = {};
let uid = 1;
let cid;
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual'
}
function select_route(url) {
if (url.origin !== window.location.origin) return null;
for (const route of routes) {
const match = route.pattern.exec(url.pathname);
if (match) {
const params = route.params(match);
const query = {};
for (const [key, value] of url.searchParams) query[key] = value || true;
return { route, data: { params, query } };
}
}
}
function render(Component, data, scroll) {
Promise.resolve(
Component.preload ? Component.preload(data) : {}
).then(preloaded => {
if (component) {
component.destroy();
} else {
// first load — remove SSR'd <head> contents
const start = document.querySelector('#sapper-head-start');
let end = document.querySelector('#sapper-head-end');
if (start && end) {
while (start.nextSibling !== end) detach(start.nextSibling);
detach(start);
detach(end);
}
// preload additional routes
routes.reduce((promise, route) => promise.then(route.load), Promise.resolve());
}
component = new Component({
target,
data: Object.assign(data, preloaded),
hydrate: !!component
});
if (scroll) {
window.scrollTo(scroll.x, scroll.y);
}
});
}
function navigate(url, id) {
const selected = select_route(url);
if (selected) {
if (id) {
// popstate or initial navigation
cid = id;
} else {
// clicked on a link. preserve scroll state
scroll_history[cid] = scroll_state();
id = cid = ++uid;
scroll_history[cid] = { x: 0, y: 0 };
history.pushState({ id }, '', url.href);
}
selected.route.load().then(mod => {
render(mod.default, selected.data, scroll_history[id]);
});
cid = id;
return true;
}
}
function handle_click(event) {
// Adapted from https://github.com/visionmedia/page.js
// MIT license https://github.com/visionmedia/page.js#license
if (which(event) !== 1) return;
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
if (event.defaultPrevented) return;
const a = findAnchor(event.target);
if (!a) return;
// check if link is inside an svg
// in this case, both href and target are always inside an object
const svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
const href = svg ? a.href.baseVal : a.href;
if (href === window.location.href) {
event.preventDefault();
return;
}
// Ignore if tag has
// 1. 'download' attribute
// 2. rel='external' attribute
if (a.hasAttribute('download') || a.getAttribute('rel') === 'external') return;
// Ignore if <a> has a target
if (svg ? a.target.baseVal : a.target) return;
const url = new URL(href);
// Don't handle hash changes
if (url.pathname === window.location.pathname && url.search === window.location.search) return;
if (navigate(url, null)) {
event.preventDefault();
}
}
function handle_popstate(event) {
scroll_history[cid] = scroll_state();
if (event.state) {
navigate(new URL(window.location), event.state.id);
} else {
// hashchange
cid = ++uid;
history.replaceState({ id: cid }, '', window.location.href);
}
}
function prefetch(event) {
const a = findAnchor(event.target);
if (!a || a.rel !== 'prefetch') return;
const selected = select_route(new URL(a.href));
if (selected) {
selected.route.load().then(mod => {
if (mod.default.preload) mod.default.preload(selected.data);
});
}
}
function findAnchor(node) {
while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG <a> elements have a lowercase name
return node;
}
let inited;
export function init(_target, _routes) {
target = _target;
routes = _routes;
if (!inited) { // this check makes HMR possible
window.addEventListener('click', handle_click);
window.addEventListener('popstate', handle_popstate);
// prefetch
window.addEventListener('touchstart', prefetch);
window.addEventListener('mouseover', prefetch);
inited = true;
}
const scroll = scroll_history[uid] = scroll_state();
history.replaceState({ id: uid }, '', window.location.href);
navigate(new URL(window.location), uid);
}
function which(event) {
event = event || window.event;
return event.which === null ? event.button : event.which;
}
function scroll_state() {
return {
x: window.scrollX,
y: window.scrollY
};
}

5
test/app/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.DS_Store
node_modules
.sapper
yarn.lock
cypress/screenshots

81
test/app/README.md Normal file
View File

@@ -0,0 +1,81 @@
# sapper-template
The default [Sapper](https://github.com/sveltejs/sapper) template. To clone it and get started:
```bash
npx degit sveltejs/sapper-template my-app
cd my-app
npm install # or yarn!
npm run dev
```
Open up [localhost:3000](http://localhost:3000) and start clicking around.
## Structure
Sapper expects to find three directories in the root of your project — `assets`, `routes` and `templates`.
### assets
The [assets](assets) directory contains any static assets that should be available. These are served using [serve-static](https://github.com/expressjs/serve-static).
In your [service-worker.js](templates/service-worker.js) file, Sapper makes these files available as `__assets__` so that you can cache them (though you can choose not to, for example if you don't want to cache very large files).
### routes
This is the heart of your Sapper app. There are two kinds of routes — *pages*, and *server routes*.
**Pages** are Svelte components written in `.html` files. When a user first visits the application, they will be served a server-rendered version of the route in question, plus some JavaScript that 'hydrates' the page and initialises a client-side router. From that point forward, navigating to other pages is handled entirely on the client for a fast, app-like feel. (Sapper will preload and cache the code for these subsequent pages, so that navigation is instantaneous.)
**Server routes** are modules written in `.js` files, that export functions corresponding to HTTP methods. Each function receives Express `request` and `response` objects as arguments, plus a `next` function. This is useful for creating a JSON API, for example.
There are three simple rules for naming the files that define your routes:
* A file called `routes/about.html` corresponds to the `/about` route. A file called `routes/blog/[slug].html` corresponds to the `/blog/:slug` route, in which case `params.slug` is available to the route
* The file `routes/index.html` (or `routes/index.js`) corresponds to the root of your app. `routes/about/index.html` is treated the same as `routes/about.html`.
* Files and directories with a leading underscore do *not* create routes. This allows you to colocate helper modules and components with the routes that depend on them — for example you could have a file called `routes/_helpers/datetime.js` and it would *not* create a `/_helpers/datetime` route
### templates
This directory should contain the following files at a minimum:
* [2xx.html](templates/2xx.html) — a template for the page to serve for valid requests
* [4xx.html](templates/4xx.html) — a template for 4xx-range errors (such as 404 Not Found)
* [5xx.html](templates/5xx.html) — a template for 5xx-range errors (such as 500 Internal Server Error)
* [main.js](templates/main.js) — this module initialises Sapper
* [service-worker.js](templates/service-worker.js) — your app's service worker
Inside the HTML templates, Sapper will inject various values as indicated by `%sapper.xxxx%` tags. Inside JavaScript files, Sapper will replace strings like `__dev__` with the appropriate value.
In lieu of documentation (bear with us), consult the files to see what variables are available and how they're used.
## Webpack config
Sapper uses webpack to provide code-splitting, dynamic imports and hot module reloading, as well as compiling your Svelte components. As long as you don't do anything daft, you can edit the configuration files to add whatever loaders and plugins you'd like.
## Production mode and deployment
To start a production version of your app, run `npm start`. This will disable hot module replacement, and activate the appropriate webpack plugins.
You can deploy your application to any environment that supports Node 8 or above. As an example, to deploy to [Now](https://zeit.co/now), run these commands:
```bash
npm install -g now
now
```
## Bugs and feedback
Sapper is in early development, and may have the odd rough edge here and there. Please be vocal over on the [Sapper issue tracker](https://github.com/sveltejs/sapper/issues).
## License
[LIL](LICENSE)

BIN
test/app/assets/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,45 @@
body {
margin: 0;
font-family: Roboto, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
font-size: 14px;
line-height: 1.5;
color: #333;
}
main {
position: relative;
max-width: 56em;
background-color: white;
padding: 2em;
margin: 0 auto;
box-sizing: border-box;
}
h1, h2, h3, h4, h5, h6 {
margin: 0 0 0.5em 0;
font-weight: 400;
line-height: 1.2;
}
h1 {
font-size: 2em;
}
a {
color: inherit;
}
code {
font-family: menlo, inconsolata, monospace;
font-size: calc(1em - 2px);
color: #555;
background-color: #f0f0f0;
padding: 0.2em 0.4em;
border-radius: 2px;
}
@media (min-width: 400px) {
body {
font-size: 16px;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View File

@@ -0,0 +1,20 @@
{
"background_color": "#ffffff",
"theme_color": "#aa1e1e",
"name": "TODO",
"short_name": "TODO",
"display": "minimal-ui",
"start_url": "/",
"icons": [
{
"src": "svelte-logo-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "svelte-logo-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

4931
test/app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
test/app/package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "TODO",
"description": "TODO",
"version": "0.0.1",
"scripts": {
"dev": "node server.js",
"build": "sapper build",
"start": "cross-env NODE_ENV=production node server.js",
"prestart": "npm run build"
},
"dependencies": {
"compression": "^1.7.1",
"cross-env": "^5.1.1",
"css-loader": "^0.28.7",
"express": "^4.16.2",
"extract-text-webpack-plugin": "^3.0.2",
"glob": "^7.1.2",
"marked": "^0.3.7",
"node-fetch": "^1.7.3",
"npm-run-all": "^4.1.2",
"serve-static": "^1.13.1",
"style-loader": "^0.19.0",
"svelte": "^1.49.1",
"svelte-loader": "^2.2.1",
"uglifyjs-webpack-plugin": "^1.1.2",
"webpack": "^3.10.0"
}
}

View File

@@ -0,0 +1,15 @@
<Nav page={{page}}/>
<main>
<slot></slot>
</main>
<script>
import Nav from './Nav.html';
export default {
components: {
Nav
}
};
</script>

View File

@@ -0,0 +1,56 @@
<nav>
<ul>
<li><a class='{{page === "home" ? "selected" : ""}}' href='/'>home</a></li>
<li><a class='{{page === "about" ? "selected" : ""}}' href='/about'>about</a></li>
<!-- for the blog link, we're using rel=prefetch so that Sapper prefetches
the blog data when we hover over the link or tap it on a touchscreen -->
<li><a rel=prefetch class='{{page === "blog" ? "selected" : ""}}' href='/blog'>blog</a></li>
</ul>
</nav>
<style>
nav {
border-bottom: 1px solid rgba(170,30,30,0.1);
font-weight: 300;
padding: 0 1em;
}
ul {
margin: 0;
padding: 0;
}
/* clearfix */
ul::after {
content: '';
display: block;
clear: both;
}
li {
display: block;
float: left;
}
.selected {
position: relative;
display: inline-block;
}
.selected::after {
position: absolute;
content: '';
width: calc(100% - 1em);
height: 2px;
background-color: rgb(170,30,30);
display: block;
bottom: -1px;
}
a {
text-decoration: none;
padding: 1em 0.5em;
display: block;
}
</style>

View File

@@ -0,0 +1,19 @@
<:Head>
<title>About</title>
</:Head>
<Layout page='about'>
<h1>About this site</h1>
<p>This is the 'about' page. There's not much here.</p>
</Layout>
<script>
import Layout from './_components/Layout.html';
export default {
components: {
Layout
}
};
</script>

View File

@@ -0,0 +1,23 @@
import posts from './_posts.js';
const lookup = {};
posts.forEach(post => {
lookup[post.slug] = JSON.stringify(post);
});
export function get(req, res, next) {
// the `slug` parameter is available because this file
// is called [slug].js
const { slug } = req.params;
if (slug in lookup) {
res.set({
'Content-Type': 'application/json',
'Cache-Control': `max-age=${30 * 60 * 1e3}` // cache for 30 minutes
});
res.end(lookup[slug]);
} else {
next();
}
}

View File

@@ -0,0 +1,92 @@
// Ordinarily, you'd generate this data from markdown files in your
// repo, or fetch them from a database of some kind. But in order to
// avoid unnecessary dependencies in the starter template, and in the
// service of obviousness, we're just going to leave it here.
// This file is called `_posts.js` rather than `posts.js`, because
// we don't want to create an `/api/blog/posts` route — the leading
// underscore tells Sapper not to do that.
const posts = [
{
title: 'What is Sapper?',
slug: 'what-is-sapper',
html: `
<p>First, you have to know what <a href='https://svelte.technology'>Svelte</a> is. Svelte is a UI framework with a bold new idea: rather than providing a library that you write code with (like React or Vue, for example), it's a compiler that turns your components into highly optimized vanilla JavaScript. If you haven't already read the <a href='https://svelte.technology/blog/frameworks-without-the-framework'>introductory blog post</a>, you should!</p>
<p>Sapper is a Next.js-style framework (<a href='/blog/how-is-sapper-different-from-next'>more on that here</a>) built around Svelte. It makes it embarrassingly easy to create extremely high performance web apps. Out of the box, you get:</p>
<ul>
<li>Code-splitting, dynamic imports and hot module replacement, powered by webpack</li>
<li>Server-side rendering (SSR) with client-side hydration</li>
<li>Service worker for offline support, and all the PWA bells and whistles</li>
<li>The nicest development experience you've ever had, or your money back</li>
</ul>
<p>It's implemented as Express middleware. Everything is set up and waiting for you to get started, but you keep complete control over the server, service worker, webpack config and everything else, so it's as flexible as you need it to be.</p>
`
},
{
title: 'How to use Sapper',
slug: 'how-to-use-sapper',
html: `
<h2>Step one</h2>
<p>Create a new project, using <a href='https://github.com/Rich-Harris/degit'>degit</a>:</p>
<pre><code>npx degit sveltejs/sapper-template my-app
cd my-app
npm install # or yarn!
npm run dev
</code></pre>
<h2>Step two</h2>
<p>Go to <a href='http://localhost:3000'>localhost:3000</a>. Open <code>my-app</code> in your editor. Edit the files in the <code>routes</code> directory or add new ones.</p>
<h2>Step three</h2>
<p>...</p>
<h2>Step four</h2>
<p>Resist overdone joke formats.</p>
`
},
{
title: 'Why the name?',
slug: 'why-the-name',
html: `
<p>In war, the soldiers who build bridges, repair roads, clear minefields and conduct demolitions — all under combat conditions — are known as <em>sappers</em>.</p>
<p>For web developers, the stakes are generally lower than those for combat engineers. But we face our own hostile environment: underpowered devices, poor network connections, and the complexity inherent in front-end engineering. Sapper, which is short for <strong>S</strong>velte <strong>app</strong> mak<strong>er</strong>, is your courageous and dutiful ally.</p>
`
},
{
title: 'How is Sapper different from Next.js?',
slug: 'how-is-sapper-different-from-next',
html: `
<p><a href='https://github.com/zeit/next.js/'>Next.js</a> is a React framework from <a href='https://zeit.co'>Zeit</a>, and is the inspiration for Sapper. There are a few notable differences, however:</p>
<ul>
<li>It's powered by <a href='https://svelte.technology'>Svelte</a> instead of React, so it's faster and your apps are smaller</li>
<li>Instead of route masking, we encode route parameters in filenames. For example, the page you're looking at right now is <code>routes/blog/[slug].html</code></li>
<li>As well as pages (Svelte components, which render on server or client), you can create <em>server routes</em> in your <code>routes</code> directory. These are just <code>.js</code> files that export functions corresponding to HTTP methods, and receive Express <code>request</code> and <code>response</code> objects as arguments. This makes it very easy to, for example, add a JSON API such as the one powering this very page (look in <code>routes/api/blog</code>)</li>
<li>Links are just <code>&lt;a&gt;</code> elements, rather than framework-specific <code>&lt;Link&gt;</code> components. That means, for example, that <a href='/blog/how-can-i-get-involved'>this link right here</a>, despite being inside a blob of HTML, works with the router as you'd expect.</li>
</ul>
`
},
{
title: 'How can I get involved?',
slug: 'how-can-i-get-involved',
html: `
<p>We're so glad you asked! Come on over to the <a href='https://github.com/sveltejs/svelte'>Svelte</a> and <a href='https://github.com/sveltejs/sapper'>Sapper</a> repos, and join us in the <a href='https://gitter.im/sveltejs/svelte'>Gitter chatroom</a>. Everyone is welcome, especially you!</p>
`
}
];
posts.forEach(post => {
post.html = post.html.replace(/^\t{3}/gm, '');
});
export default posts;

View File

@@ -0,0 +1,17 @@
import posts from './_posts.js';
const contents = JSON.stringify(posts.map(post => {
return {
title: post.title,
slug: post.slug
};
}));
export function get(req, res) {
res.set({
'Content-Type': 'application/json',
'Cache-Control': `max-age=${30 * 60 * 1e3}` // cache for 30 minutes
});
res.end(contents);
}

View File

@@ -0,0 +1,67 @@
<:Head>
<title>{{post.title}}</title>
</:Head>
<Layout page='blog'>
<h1>{{post.title}}</h1>
<div class='content'>
{{{post.html}}}
</div>
</Layout>
<style>
/*
By default, CSS is locally scoped to the component,
and any unused styles are dead-code-eliminated.
In this page, Svelte can't know which elements are
going to appear inside the {{{post.html}}} block,
so we have to use the :global(...) modifier to target
all elements inside .content
*/
.content :global(h2) {
font-size: 1.4em;
font-weight: 500;
}
.content :global(pre) {
background-color: #f9f9f9;
box-shadow: inset 1px 1px 5px rgba(0,0,0,0.05);
padding: 0.5em;
border-radius: 2px;
overflow-x: auto;
}
.content :global(pre) :global(code) {
background-color: transparent;
padding: 0;
}
.content :global(ul) {
line-height: 1.5;
}
.content :global(li) {
margin: 0 0 0.5em 0;
}
</style>
<script>
import Layout from '../_components/Layout.html';
export default {
components: {
Layout
},
preload({ params, query }) {
// the `slug` parameter is available because this file
// is called [slug].html
const { slug } = params;
return fetch(`/api/blog/${slug}`).then(r => r.json()).then(post => {
return { post };
});
}
};
</script>

View File

@@ -0,0 +1,40 @@
<:Head>
<title>Blog</title>
</:Head>
<Layout page='blog'>
<h1>Recent posts</h1>
<ul>
{{#each posts as post}}
<!-- we're using the non-standard `rel=prefetch` attribute to
tell Sapper to load the data for the page as soon as
the user hovers over the link or taps it, instead of
waiting for the 'click' event -->
<li><a rel='prefetch' href='/blog/{{post.slug}}'>{{post.title}}</a></li>
{{/each}}
</ul>
</Layout>
<style>
ul {
margin: 0 0 1em 0;
line-height: 1.5;
}
</style>
<script>
import Layout from '../_components/Layout.html';
export default {
components: {
Layout
},
preload({ params, query }) {
return fetch(`/api/blog`).then(r => r.json()).then(posts => {
return { posts };
});
}
};
</script>

View File

@@ -0,0 +1,58 @@
<:Head>
<title>Sapper project template</title>
</:Head>
<Layout page='home'>
<h1>Great success!</h1>
<figure>
<img src='/great-success.png'>
<figcaption>HIGH FIVE!</figcaption>
</figure>
<p><strong>Try editing this file (routes/index.html) to test hot module reloading.</strong></p>
</Layout>
<style>
h1, figure, p {
text-align: center;
margin: 0 auto;
}
h1 {
font-size: 2.8em;
text-transform: uppercase;
font-weight: 700;
margin: 0 0 0.5em 0;
}
figure {
margin: 0 0 1em 0;
}
img {
width: 100%;
max-width: 400px;
margin: 0 0 1em 0;
}
p {
margin: 1em auto;
}
@media (min-width: 480px) {
h1 {
font-size: 4em;
}
}
</style>
<script>
import Layout from './_components/Layout.html';
export default {
components: {
Layout
}
};
</script>

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

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

View File

@@ -0,0 +1,37 @@
<!doctype>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width'>
<meta name='theme-color' content='#aa1e1e'>
<link rel='stylesheet' href='/global.css'>
<link rel='manifest' href='/manifest.json'>
<link rel='icon' type='image/png' href='/favicon.png'>
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js');
}
</script>
<!-- Sapper generates a <style> tag containing critical CSS
for the current page. CSS for the rest of the app is
lazily loaded when it precaches secondary pages -->
%sapper.styles%
<!-- This contains the contents of the <:Head> component, if
the current page has one -->
%sapper.head%
</head>
<body>
<!-- The application will be rendered inside this element,
because `templates/main.js` references it -->
<div id='sapper'>%sapper.html%</div>
<!-- Sapper creates a <script> tag containing `templates/main.js`
and anything else it needs to hydrate the app and
initialise the router -->
<script src='%sapper.main%'></script>
</body>
</html>

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
import { init } from '__app__';
// `routes` is an array of route objects injected by Sapper
init(document.querySelector('#sapper'), __routes__);
// if (__dev__) {
// // Enable hot-module reloading
// import('sapper/webpack/hmr');
// if (module.hot) module.hot.accept();
// }

View File

@@ -0,0 +1,77 @@
const ASSETS = `cache__timestamp__`;
// `shell` is an array of all the files generated by webpack,
// `assets` is an array of everything in the `assets` directory
const to_cache = __shell__.concat(__assets__);
const cached = new Set(to_cache);
// `routes` is an array of `{ pattern: RegExp }` objects that
// match the pages in your app
const routes = __routes__;
self.addEventListener('install', event => {
event.waitUntil(
caches
.open(ASSETS)
.then(cache => cache.addAll(to_cache))
.then(() => {
self.skipWaiting();
})
);
});
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(async keys => {
// delete old caches
for (const key of keys) {
if (key !== ASSETS) await caches.delete(key);
}
await self.clients.claim();
})
);
});
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// don't try to handle e.g. data: URIs
if (!url.protocol.startsWith('http')) return;
// always serve assets and webpack-generated files from cache
if (cached.has(url.pathname)) {
event.respondWith(caches.match(event.request));
return;
}
// for pages, you might want to serve a shell `index.html` file,
// which Sapper has generated for you. It's not right for every
// app, but if it's right for yours then uncomment this section
/*
if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) {
event.respondWith(caches.match('/index.html'));
return;
}
*/
// for everything else, try the network first, falling back to
// cache if the user is offline. (If the pages never change, you
// might prefer a cache-first approach to a network-first one.)
event.respondWith(
caches
.open('offline')
.then(async cache => {
try {
const response = await fetch(event.request);
cache.put(event.request, response.clone());
return response;
} catch(err) {
const response = await cache.match(event.request);
if (response) return response;
throw err;
}
})
);
});

View File

@@ -0,0 +1,50 @@
const config = require('../../webpack/config.js');
const webpack = require('webpack');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
entry: config.client.entry(),
output: config.client.output(),
resolve: {
extensions: ['.js', '.html']
},
module: {
rules: [
{
test: /\.html$/,
exclude: /node_modules/,
use: {
loader: 'svelte-loader',
options: {
hydratable: true,
emitCss: !config.dev,
cascade: false,
store: true
}
}
},
config.dev && {
test: /\.css$/,
use: [
{ loader: "style-loader" },
{ loader: "css-loader" }
]
},
!config.dev && {
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [{ loader: 'css-loader', options: { sourceMap: config.dev } }]
})
}
].filter(Boolean)
},
plugins: [
config.dev && new webpack.HotModuleReplacementPlugin(),
!config.dev && new ExtractTextPlugin('main.css'),
!config.dev && new webpack.optimize.ModuleConcatenationPlugin(),
!config.dev && new UglifyJSPlugin()
].filter(Boolean),
devtool: config.dev ? 'inline-source-map' : false
};

View File

@@ -0,0 +1,30 @@
const config = require('../../webpack/config.js');
const webpack = require('webpack');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
entry: config.server.entry(),
output: config.server.output(),
target: 'node',
resolve: {
extensions: ['.js', '.html']
},
module: {
rules: [
{
test: /\.html$/,
exclude: /node_modules/,
use: {
loader: 'svelte-loader',
options: {
css: false,
cascade: false,
store: true,
generate: 'ssr'
}
}
}
]
}
};

View File

@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@@ -0,0 +1,19 @@
describe('dev mode', () => {
beforeEach(() => {
cy.visit('/')
});
it('has the correct <h1>', () => {
cy.contains('h1', 'Great success!')
});
it('navigates to /about', () => {
cy.get('nav a').contains('about').click();
cy.url().should('include', '/about');
});
it('navigates to /blog', () => {
cy.get('nav a').contains('blog').click();
cy.url().should('include', '/blog');
});
});

View File

@@ -0,0 +1,19 @@
describe('prod mode', () => {
beforeEach(() => {
cy.visit('/')
});
it('has the correct <h1>', () => {
cy.contains('h1', 'Great success!')
});
it('navigates to /about', () => {
cy.get('nav a').contains('about').click();
cy.url().should('include', '/about');
});
it('navigates to /blog', () => {
cy.get('nav a').contains('blog').click();
cy.url().should('include', '/blog');
});
});

View File

@@ -0,0 +1,17 @@
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

View File

@@ -0,0 +1,25 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })

View File

@@ -0,0 +1,20 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

75
test/launch.js Normal file
View File

@@ -0,0 +1,75 @@
const fs = require('fs');
const path = require('path');
const rimraf = require('rimraf');
const child_process = require('child_process');
// ensure sapper doesn't exist in app/node_modules
rimraf.sync(
path.join(__dirname, 'app/node_modules/sapper')
);
rimraf.sync(
path.join(__dirname, 'app/node_modules/.bin/sapper')
);
// create symlinks
fs.symlinkSync(
path.join(__dirname, '..'),
path.join(__dirname, 'app/node_modules/sapper')
);
fs.symlinkSync(
path.join(__dirname, '../cli/index.js'),
path.join(__dirname, 'app/node_modules/.bin/sapper')
);
const app_dir = path.join(__dirname, 'app');
function start_server() {
const server = child_process.spawn(process.execPath, ['server.js'], {
cwd: app_dir,
env: {
NODE_ENV: 'development'
},
stdio: 'pipe'
});
server.stdout.on('data', (data) => {
process.stdout.write(data);
});
server.stderr.on('data', (data) => {
process.stderr.write(data);
});
}
function launch() {
if (process.argv[2] === '--dev') {
start_server();
} else {
child_process.exec(`npm run build`, {
cwd: app_dir
}, (err, stdout, stderr) => {
if (err) throw err;
process.stdout.write(stdout);
process.stderr.write(stderr);
start_server();
});
}
}
// this is a terrible hack
if (process.env.APPVEYOR) {
child_process.exec(`npm install`, {
cwd: app_dir
}, (err, stdout, stderr) => {
if (err) throw err;
process.stdout.write(stdout);
process.stderr.write(stderr);
launch();
});
} else {
launch();
}

View File

@@ -0,0 +1,74 @@
const path = require('path');
const assert = require('assert');
const create_routes = require('../../lib/utils/create_routes.js');
describe('create_routes', () => {
it('sorts routes correctly', () => {
const routes = create_routes(['index.html', 'about.html', '[wildcard].html', 'post/foo.html', 'post/[id].html', 'post/bar.html']);
assert.deepEqual(
routes.map(r => r.file),
[
'post/foo.html',
'post/bar.html',
'post/[id].html',
'about.html',
'[wildcard].html',
'index.html'
]
);
});
it('generates params', () => {
const routes = create_routes(['index.html', 'about.html', '[wildcard].html', 'post/[id].html']);
let file;
let params;
for (let i = 0; i < routes.length; i += 1) {
const route = routes[i];
if (params = route.exec('/post/123')) {
file = route.file;
break;
}
}
assert.equal(file, 'post/[id].html');
assert.deepEqual(params, {
id: '123'
});
});
it('ignores files and directories with leading underscores', () => {
const routes = create_routes(['index.html', '_foo.html', 'a/_b/c/d.html', 'e/f/g/h.html', 'i/_j.html']);
assert.deepEqual(
routes.map(r => r.file),
[
'e/f/g/h.html',
'index.html'
]
);
});
it('matches /foo/:bar before /:baz/qux', () => {
const a = create_routes(['foo/[bar].html', '[baz]/qux.html']);
const b = create_routes(['[baz]/qux.html', 'foo/[bar].html']);
assert.deepEqual(
a.map(r => r.file),
['foo/[bar].html', '[baz]/qux.html']
);
assert.deepEqual(
b.map(r => r.file),
['foo/[bar].html', '[baz]/qux.html']
);
});
it('fails if routes are indistinguishable', () => {
assert.throws(() => {
create_routes(['[foo].html', '[bar]/index.html']);
}, /The \[foo\].html and \[bar\]\/index.html routes clash/);
});
});

View File

@@ -1,52 +0,0 @@
const path = require('path');
module.exports = function create_matchers(files) {
return files
.map(file => {
const parts = file.replace(/\.(html|js|mjs)$/, '').split(path.sep);
if (parts[parts.length - 1] === 'index') parts.pop();
const dynamic = parts
.filter(part => part[0] === '%')
.map(part => part.slice(1, -1));
const pattern = new RegExp(
`^\\/${parts.map(p => p[0] === '%' ? '([^/]+)' : p).join('\\/')}$`
);
const test = url => pattern.test(url);
const exec = url => {
const match = pattern.exec(url);
if (!match) return;
const params = {};
dynamic.forEach((param, i) => {
params[param] = match[i + 1];
});
return params;
};
return {
file,
test,
exec,
parts,
dynamic
};
})
.sort((a, b) => {
return (
(a.dynamic.length - b.dynamic.length) || // match static paths first
(b.parts.length - a.parts.length) // match longer paths first
);
})
.map(matcher => {
return {
file: matcher.file,
test: matcher.test,
exec: matcher.exec
}
});
}

View File

@@ -1,39 +0,0 @@
const path = require('path');
const assert = require('assert');
const create_matchers = require('./create_matchers.js');
describe('create_matchers', () => {
it('sorts routes correctly', () => {
const matchers = create_matchers(['index.html', 'about.html', '%wildcard%.html', 'post/%id%.html']);
assert.deepEqual(
matchers.map(m => m.file),
[
'about.html',
'index.html',
'post/%id%.html',
'%wildcard%.html'
]
);
});
it('generates params', () => {
const matchers = create_matchers(['index.html', 'about.html', '%wildcard%.html', 'post/%id%.html']);
let file;
let params;
for (let i = 0; i < matchers.length; i += 1) {
const matcher = matchers[i];
if (params = matcher.exec('/post/123')) {
file = matcher.file;
break;
}
}
assert.equal(file, 'post/%id%.html');
assert.deepEqual(params, {
id: '123'
});
});
});

40
webpack/config.js Normal file
View File

@@ -0,0 +1,40 @@
const path = require('path');
const { src, dest, dev, entry } = require('../lib/config.js');
module.exports = {
dev,
client: {
entry: () => {
return {
main: entry.client
};
},
output: () => {
return {
path: `${dest}/client`,
filename: '[name].[hash].js',
chunkFilename: '[name].[id].[hash].js',
publicPath: '/client/'
};
}
},
server: {
entry: () => {
return {
main: entry.server
}
},
output: () => {
return {
path: `${dest}/server`,
filename: '[name].[hash].js',
chunkFilename: '[name].[id].[hash].js',
libraryTarget: 'commonjs2'
};
}
}
};

1
webpack/hmr.js Normal file
View File

@@ -0,0 +1 @@
import 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000';

3370
yarn.lock Normal file

File diff suppressed because it is too large Load Diff