Compare commits

..

26 Commits

Author SHA1 Message Date
Richard Harris
c2ed73f103 -> v0.27.0 2019-05-09 09:30:45 -04:00
Richard Harris
133ac07ed2 remove some unused imports 2019-05-09 09:26:49 -04:00
Richard Harris
7417de101e Merge branch 'master' of github.com:sveltejs/sapper 2019-05-09 09:26:13 -04:00
Rich Harris
2792b7c5d1 Merge pull request #676 from mrkishi/server-indexes
Consolidate index handling for page and server routes
2019-05-09 09:18:26 -04:00
Rich Harris
cf8d5ee717 Merge pull request #670 from cristianl/pr-rollup-output-naming
Build rollup config: Don't add hashes to files in dist
2019-05-09 09:15:04 -04:00
Rich Harris
8e4517a1ad Merge pull request #673 from mrkishi/testing-issue
Harden tests (...a bit)
2019-05-09 08:57:18 -04:00
Rich Harris
91894722ee Merge pull request #652 from sveltejs/relicense
switch license to MIT
2019-05-09 08:44:32 -04:00
Rich Harris
933b3b76a6 Merge pull request #665 from mrkishi/node-stable
Switch to node stable
2019-05-09 08:42:07 -04:00
Richard Harris
3ed4d1d887 update deps 2019-05-09 08:39:58 -04:00
Richard Harris
dda936e53b fix theme color 2019-05-09 08:34:41 -04:00
Rich Harris
ac4eb84f3d Merge pull request #662 from thollander/feat/fix-mobile-viewport
Fix mobile viewport KO
2019-05-09 08:32:57 -04:00
Rich Harris
9e70e68c0c Merge pull request #664 from mrkishi/refactor
Tidy a few things up
2019-05-09 08:28:57 -04:00
Rich Harris
22389eab99 Merge pull request #678 from mrkishi/generated-app-quoted-attributes
Add attribute quotes to generated `App.svelte`
2019-05-09 08:24:02 -04:00
mrkishi
fe6b7976ef Add attribute quotes to generated App.svelte 2019-05-08 22:44:07 -03:00
mrkishi
54e92c3b99 Regularize page and server routes
Treats both page and server routes similarly in regards to indexes.

It's a (somewhat) breaking change: server routes will accept trailing
slashes to match page routes behavior, but only if there's no extension.

We still need a stronger support for dealing with different clean urls
rules.
2019-05-08 12:24:45 -03:00
mrkishi
e6c1a54164 Harden tests (...a bit) 2019-05-08 00:57:59 -03:00
Cristian Lorsson
629b5601c8 Don't add hashes to dist output
Default is '[name].[hash].js'
2019-05-06 20:46:32 -03:00
Conduitry
8bdd363a19 site: remove chokidar 2019-05-06 12:50:31 -04:00
mrkishi
8fcc27d44f Update svelte to released v3 2019-05-05 23:19:00 -03:00
mrkishi
f6e72a0432 Update dependencies 2019-05-05 21:25:00 -03:00
mrkishi
f886a12bd7 Group route filters 2019-05-05 14:51:23 -03:00
mrkishi
6c03cfd46a Extract layout file search 2019-05-05 14:51:22 -03:00
mrkishi
78480fe5e8 Fix typo 2019-05-05 14:51:22 -03:00
mrkishi
5cba40b7e0 Refactor compose_handlers and ignore 2019-05-05 14:51:22 -03:00
thollander
5e5a8c4c69 Fix mobile viewport KO
- Changed `;` to `,` which causes the bug
2019-05-05 12:17:41 +02:00
Richard Harris
a2204a9d2e switch license to MIT 2019-05-02 08:32:40 -04:00
56 changed files with 1942 additions and 2008 deletions

4
.gitignore vendored
View File

@@ -3,11 +3,7 @@ yarn.lock
yarn-error.log
node_modules
cypress/screenshots
test/app/.sapper
test/app/src/manifest
__sapper__
test/app/export
test/app/build
sapper
runtime.js
dist

View File

@@ -3,7 +3,7 @@ sudo: false
language: node_js
node_js:
- "11"
- "stable"
env:
global:

View File

@@ -1,5 +1,10 @@
# sapper changelog
## 0.27.0
* Change license from LIL to MIT ([#652](https://github.com/sveltejs/sapper/pull/652))
* Fix index server route mapping ([#624](https://github.com/sveltejs/sapper/issues/624))
## 0.26.1
* Handle skipped segments ([#663](https://github.com/sveltejs/sapper/pull/663))

10
LICENSE
View File

@@ -1,9 +1,7 @@
Copyright (c) 2017 [these people](https://github.com/sveltejs/sapper/graphs/contributors).
Copyright (c) 2016-19 [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:
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 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.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
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.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

1457
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "sapper",
"version": "0.26.1",
"version": "0.27.0",
"description": "Military-grade apps, engineered by Svelte",
"bin": {
"sapper": "./sapper"
@@ -18,50 +18,50 @@
"test": "test"
},
"dependencies": {
"html-minifier": "^3.5.21",
"html-minifier": "^4.0.0",
"http-link-header": "^1.0.2",
"shimport": "^1.0.0",
"sourcemap-codec": "^1.4.4",
"string-hash": "^1.1.3"
},
"devDependencies": {
"@types/mocha": "^5.2.5",
"@types/node": "^10.12.21",
"@types/puppeteer": "^1.11.3",
"@types/mocha": "^5.2.6",
"@types/node": "^10.14.6",
"@types/puppeteer": "^1.12.4",
"agadoo": "^1.0.1",
"cheap-watch": "^1.0.2",
"cookie": "^0.3.1",
"devalue": "^1.1.0",
"eslint": "^5.12.1",
"eslint-plugin-import": "^2.16.0",
"kleur": "^3.0.1",
"mocha": "^5.2.0",
"node-fetch": "^2.3.0",
"eslint": "^5.16.0",
"eslint-plugin-import": "^2.17.2",
"kleur": "^3.0.3",
"mocha": "^6.1.4",
"node-fetch": "^2.5.0",
"npm-run-all": "^4.1.5",
"polka": "^0.5.1",
"polka": "^0.5.2",
"port-authority": "^1.0.5",
"pretty-bytes": "^5.1.0",
"puppeteer": "^1.12.0",
"pretty-bytes": "^5.2.0",
"puppeteer": "^1.15.0",
"require-relative": "^0.8.7",
"rollup": "^1.1.2",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-json": "^3.1.0",
"rollup-plugin-node-resolve": "^4.0.0",
"rollup-plugin-replace": "^2.1.0",
"rollup-plugin-string": "^2.0.2",
"rollup": "^1.11.3",
"rollup-plugin-commonjs": "^9.3.4",
"rollup-plugin-json": "^4.0.0",
"rollup-plugin-node-resolve": "^4.2.3",
"rollup-plugin-replace": "^2.2.0",
"rollup-plugin-string": "^3.0.0",
"rollup-plugin-sucrase": "^2.1.0",
"rollup-plugin-svelte": "^5.0.3",
"sade": "^1.4.2",
"sirv": "^0.2.2",
"sucrase": "^3.9.5",
"svelte": "^3.0.0-beta.11",
"sirv": "^0.4.0",
"sucrase": "^3.10.1",
"svelte": "^3.2.0",
"svelte-loader": "^2.13.3",
"webpack": "^4.29.0",
"webpack": "^4.30.0",
"webpack-format-messages": "^2.0.5",
"yootils": "0.0.15"
},
"peerDependencies": {
"svelte": "^3.0.0"
"svelte": "^3.2.0"
},
"scripts": {
"test": "mocha --opts mocha.opts",
@@ -81,7 +81,7 @@
"express"
],
"author": "Rich Harris",
"license": "LIL",
"license": "MIT",
"bugs": {
"url": "https://github.com/sveltejs/sapper/issues"
},

View File

@@ -1,5 +1,5 @@
import sucrase from 'rollup-plugin-sucrase';
import string from 'rollup-plugin-string';
import { string } from 'rollup-plugin-string';
import json from 'rollup-plugin-json';
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
@@ -52,7 +52,8 @@ export default [
output: {
dir: 'dist',
format: 'cjs',
sourcemap: true
sourcemap: true,
chunkFileNames: '[name].js'
},
external,
plugins: [

View File

@@ -101,7 +101,11 @@ export function select_target(url: URL): Target {
if (url.origin !== location.origin) return null;
if (!url.pathname.startsWith(initial_data.baseUrl)) return null;
const path = url.pathname.slice(initial_data.baseUrl.length);
let path = url.pathname.slice(initial_data.baseUrl.length);
if (path === '') {
path = '/';
}
// avoid accidental clashes between server routes and page routes
if (ignore.some(pattern => pattern.test(path))) return;

View File

@@ -1 +0,0 @@
export const IGNORE = '__SAPPER__IGNORE__';

View File

@@ -5,10 +5,8 @@ import cookie from 'cookie';
import devalue from 'devalue';
import fetch from 'node-fetch';
import URL from 'url';
import { IGNORE } from '../constants';
import { Manifest, Page, Props, Req, Res } from './types';
import { Manifest, Page, Req, Res } from './types';
import { build_dir, dev, src_dir } from '@sapper/internal/manifest-server';
import { stores } from '@sapper/internal/shared';
import App from '@sapper/internal/App.svelte';
export function get_page_handler(
@@ -328,8 +326,6 @@ export function get_page_handler(
}
return function find_route(req: Req, res: Res, next: () => void) {
if (req[IGNORE]) return next();
if (req.path === '/service-worker-index.html') {
const homePage = pages.find(page => page.pattern.test('/'));
handle_page(homePage, req, res);

View File

@@ -1,4 +1,3 @@
import { IGNORE } from '../constants';
import { Req, Res, ServerRoute } from './types';
export function get_server_route_handler(routes: ServerRoute[]) {
@@ -64,8 +63,6 @@ export function get_server_route_handler(routes: ServerRoute[]) {
}
return function find_route(req: Req, res: Res, next: () => void) {
if (req[IGNORE]) return next();
for (const route of routes) {
if (route.pattern.test(req.path)) {
handle_route(route, req, res, next);
@@ -75,4 +72,4 @@ export function get_server_route_handler(routes: ServerRoute[]) {
next();
};
}
}

View File

@@ -5,7 +5,6 @@ import { Handler, Req, Res } from './types';
import { get_server_route_handler } from './get_server_route_handler';
import { get_page_handler } from './get_page_handler';
import { lookup } from './mime';
import { IGNORE } from '../constants';
export default function middleware(opts: {
session?: (req: Req, res: Res) => any,
@@ -15,15 +14,8 @@ export default function middleware(opts: {
let emitted_basepath = false;
return compose_handlers([
ignore && ((req: Req, res: Res, next: () => void) => {
req[IGNORE] = should_ignore(req.path, ignore);
next();
}),
return compose_handlers(ignore, [
(req: Req, res: Res, next: () => void) => {
if (req[IGNORE]) return next();
if (req.baseUrl === undefined) {
let { originalUrl } = req;
if (req.url === '/' && originalUrl[originalUrl.length - 1] !== '/') {
@@ -73,24 +65,26 @@ export default function middleware(opts: {
].filter(Boolean));
}
export function compose_handlers(handlers: Handler[]) {
return (req: Req, res: Res, next: () => void) => {
let i = 0;
function go() {
const handler = handlers[i];
export function compose_handlers(ignore: any, handlers: Handler[]): Handler {
const total = handlers.length;
if (handler) {
handler(req, res, () => {
i += 1;
go();
});
} else {
next();
}
function nth_handler(n: number, req: Req, res: Res, next: () => void) {
if (n >= total) {
return next();
}
go();
};
handlers[n](req, res, () => nth_handler(n+1, req, res, next));
}
return !ignore
? (req, res, next) => nth_handler(0, req, res, next)
: (req, res, next) => {
if (should_ignore(req.path, ignore)) {
next();
} else {
nth_handler(0, req, res, next);
}
};
}
export function should_ignore(uri: string, val: any) {
@@ -116,8 +110,6 @@ export function serve({ prefix, pathname, cache_control }: {
: (file: string) => (cache.has(file) ? cache : cache.set(file, fs.readFileSync(path.resolve(build_dir, file)))).get(file)
return (req: Req, res: Res, next: () => void) => {
if (req[IGNORE]) return next();
if (filter(req)) {
const type = lookup(req.path);

737
site/package-lock.json generated
View File

@@ -846,9 +846,9 @@
}
},
"@sveltejs/site-kit": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@sveltejs/site-kit/-/site-kit-1.0.1.tgz",
"integrity": "sha512-S5mZG2PkWGDXGZaIJQh5JtF7RLG2uIO36f9juUkUHhjqlz6FaBl2D4zyofmvGergSd6m9NgQ+iXLiFIK9WNmBg==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@sveltejs/site-kit/-/site-kit-1.0.4.tgz",
"integrity": "sha512-BaQhIL1iPhCF+iDXfy9psDvRdFzfyMPkWnoZHfVz+INpHsU2aJmRZOPl9rykXmPyiPo+AwTTNK5vjIvmtwHLPQ==",
"dev": true,
"requires": {
"@sindresorhus/slugify": "^0.9.1",
@@ -862,9 +862,9 @@
"dev": true
},
"@types/node": {
"version": "11.13.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-11.13.8.tgz",
"integrity": "sha512-szA3x/3miL90ZJxUCzx9haNbK5/zmPieGraZEe4WI+3srN0eGLiT22NXeMHmyhNEopn+IrxqMc7wdVwvPl8meg==",
"version": "11.13.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-11.13.10.tgz",
"integrity": "sha512-leUNzbFTMX94TWaIKz8N15Chu55F9QSH+INKayQr5xpkasBQBRF3qQXfo3/dOnMU/dEIit+Y/SU8HyOjq++GwA==",
"dev": true
},
"@types/resolve": {
@@ -900,27 +900,6 @@
"color-convert": "^1.9.0"
}
},
"anymatch": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
"integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
"dev": true,
"requires": {
"micromatch": "^3.1.4",
"normalize-path": "^2.1.1"
},
"dependencies": {
"normalize-path": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
"integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
"dev": true,
"requires": {
"remove-trailing-separator": "^1.0.1"
}
}
}
},
"arr-diff": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
@@ -969,12 +948,6 @@
"integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
"dev": true
},
"async-each": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz",
"integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==",
"dev": true
},
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@@ -1042,12 +1015,6 @@
}
}
},
"binary-extensions": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz",
"integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==",
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -1088,14 +1055,14 @@
}
},
"browserslist": {
"version": "4.5.5",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.5.5.tgz",
"integrity": "sha512-0QFO1r/2c792Ohkit5XI8Cm8pDtZxgNl2H6HU4mHrpYz7314pEYcsAVVatM0l/YmxPnEzh9VygXouj4gkFUTKA==",
"version": "4.5.6",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.5.6.tgz",
"integrity": "sha512-o/hPOtbU9oX507lIqon+UvPYqpx3mHc8cV3QemSBTXwkG8gSQSK6UKvXcE/DcleU3+A59XTUHyCvZ5qGy8xVAg==",
"dev": true,
"requires": {
"caniuse-lite": "^1.0.30000960",
"electron-to-chromium": "^1.3.124",
"node-releases": "^1.1.14"
"caniuse-lite": "^1.0.30000963",
"electron-to-chromium": "^1.3.127",
"node-releases": "^1.1.17"
}
},
"buffer-from": {
@@ -1143,9 +1110,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30000963",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000963.tgz",
"integrity": "sha512-n4HUiullc7Lw0LyzpeLa2ffP8KxFBGdxqD/8G3bSL6oB758hZ2UE2CVK+tQN958tJIi0/tfpjAc67aAtoHgnrQ==",
"version": "1.0.30000967",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000967.tgz",
"integrity": "sha512-rUBIbap+VJfxTzrM4akJ00lkvVb5/n5v3EGXfWzSH5zT8aJmGzjA8HWhJ4U6kCpzxozUSnB+yvAYDRPY6mRpgQ==",
"dev": true
},
"chalk": {
@@ -1159,26 +1126,6 @@
"supports-color": "^5.3.0"
}
},
"chokidar": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.5.tgz",
"integrity": "sha512-i0TprVWp+Kj4WRPtInjexJ8Q+BqTE909VpH8xVhXrJkoc5QC8VO9TryGOqTr+2hljzc1sC62t22h5tZePodM/A==",
"dev": true,
"requires": {
"anymatch": "^2.0.0",
"async-each": "^1.0.1",
"braces": "^2.3.2",
"fsevents": "^1.2.7",
"glob-parent": "^3.1.0",
"inherits": "^2.0.3",
"is-binary-path": "^1.0.0",
"is-glob": "^4.0.0",
"normalize-path": "^3.0.0",
"path-is-absolute": "^1.0.0",
"readdirp": "^2.2.1",
"upath": "^1.1.1"
}
},
"class-utils": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
@@ -1432,9 +1379,9 @@
"optional": true
},
"electron-to-chromium": {
"version": "1.3.127",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.127.tgz",
"integrity": "sha512-1o25iFRf/dbgauTWalEzmD1EmRN3a2CzP/K7UVpYLEBduk96LF0FyUdCcf4Ry2mAWJ1VxyblFjC93q6qlLwA2A==",
"version": "1.3.133",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.133.tgz",
"integrity": "sha512-lyoC8aoqbbDqsprb6aPdt9n3DpOZZzdz/T4IZKsR0/dkZIxnJVUjjcpOSwA66jPRIOyDAamCTAUqweU05kKNSg==",
"dev": true
},
"error-ex": {
@@ -1648,535 +1595,6 @@
"map-cache": "^0.2.2"
}
},
"fsevents": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz",
"integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==",
"dev": true,
"optional": true,
"requires": {
"nan": "^2.12.1",
"node-pre-gyp": "^0.12.0"
},
"dependencies": {
"abbrev": {
"version": "1.1.1",
"bundled": true,
"dev": true,
"optional": true
},
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true
},
"aproba": {
"version": "1.2.0",
"bundled": true,
"dev": true,
"optional": true
},
"are-we-there-yet": {
"version": "1.1.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"delegates": "^1.0.0",
"readable-stream": "^2.0.6"
}
},
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"chownr": {
"version": "1.1.1",
"bundled": true,
"dev": true,
"optional": true
},
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true
},
"core-util-is": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
},
"debug": {
"version": "4.1.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ms": "^2.1.1"
}
},
"deep-extend": {
"version": "0.6.0",
"bundled": true,
"dev": true,
"optional": true
},
"delegates": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
},
"detect-libc": {
"version": "1.0.3",
"bundled": true,
"dev": true,
"optional": true
},
"fs-minipass": {
"version": "1.2.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minipass": "^2.2.1"
}
},
"fs.realpath": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
},
"gauge": {
"version": "2.7.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"aproba": "^1.0.3",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.0",
"object-assign": "^4.1.0",
"signal-exit": "^3.0.0",
"string-width": "^1.0.1",
"strip-ansi": "^3.0.1",
"wide-align": "^1.1.0"
}
},
"glob": {
"version": "7.1.3",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"has-unicode": {
"version": "2.0.1",
"bundled": true,
"dev": true,
"optional": true
},
"iconv-lite": {
"version": "0.4.24",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"ignore-walk": {
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimatch": "^3.0.4"
}
},
"inflight": {
"version": "1.0.6",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true
},
"ini": {
"version": "1.3.5",
"bundled": true,
"dev": true,
"optional": true
},
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"requires": {
"number-is-nan": "^1.0.0"
}
},
"isarray": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
},
"minimatch": {
"version": "3.0.4",
"bundled": true,
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"dev": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
}
},
"minizlib": {
"version": "1.2.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minipass": "^2.2.1"
}
},
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"dev": true,
"requires": {
"minimist": "0.0.8"
}
},
"ms": {
"version": "2.1.1",
"bundled": true,
"dev": true,
"optional": true
},
"needle": {
"version": "2.3.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"debug": "^4.1.0",
"iconv-lite": "^0.4.4",
"sax": "^1.2.4"
}
},
"node-pre-gyp": {
"version": "0.12.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"detect-libc": "^1.0.2",
"mkdirp": "^0.5.1",
"needle": "^2.2.1",
"nopt": "^4.0.1",
"npm-packlist": "^1.1.6",
"npmlog": "^4.0.2",
"rc": "^1.2.7",
"rimraf": "^2.6.1",
"semver": "^5.3.0",
"tar": "^4"
}
},
"nopt": {
"version": "4.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"abbrev": "1",
"osenv": "^0.1.4"
}
},
"npm-bundled": {
"version": "1.0.6",
"bundled": true,
"dev": true,
"optional": true
},
"npm-packlist": {
"version": "1.4.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ignore-walk": "^3.0.1",
"npm-bundled": "^1.0.1"
}
},
"npmlog": {
"version": "4.1.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"are-we-there-yet": "~1.1.2",
"console-control-strings": "~1.1.0",
"gauge": "~2.7.3",
"set-blocking": "~2.0.0"
}
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true
},
"object-assign": {
"version": "4.1.1",
"bundled": true,
"dev": true,
"optional": true
},
"once": {
"version": "1.4.0",
"bundled": true,
"dev": true,
"requires": {
"wrappy": "1"
}
},
"os-homedir": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
},
"os-tmpdir": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
},
"osenv": {
"version": "0.1.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"os-homedir": "^1.0.0",
"os-tmpdir": "^1.0.0"
}
},
"path-is-absolute": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
},
"process-nextick-args": {
"version": "2.0.0",
"bundled": true,
"dev": true,
"optional": true
},
"rc": {
"version": "1.2.8",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"dependencies": {
"minimist": {
"version": "1.2.0",
"bundled": true,
"dev": true,
"optional": true
}
}
},
"readable-stream": {
"version": "2.3.6",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"rimraf": {
"version": "2.6.3",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"glob": "^7.1.3"
}
},
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
"bundled": true,
"dev": true,
"optional": true
},
"sax": {
"version": "1.2.4",
"bundled": true,
"dev": true,
"optional": true
},
"semver": {
"version": "5.7.0",
"bundled": true,
"dev": true,
"optional": true
},
"set-blocking": {
"version": "2.0.0",
"bundled": true,
"dev": true,
"optional": true
},
"signal-exit": {
"version": "3.0.2",
"bundled": true,
"dev": true,
"optional": true
},
"string-width": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
"strip-ansi": "^3.0.0"
}
},
"string_decoder": {
"version": "1.1.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "~5.1.0"
}
},
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
"dev": true,
"requires": {
"ansi-regex": "^2.0.0"
}
},
"strip-json-comments": {
"version": "2.0.1",
"bundled": true,
"dev": true,
"optional": true
},
"tar": {
"version": "4.4.8",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"chownr": "^1.1.1",
"fs-minipass": "^1.2.5",
"minipass": "^2.3.4",
"minizlib": "^1.1.1",
"mkdirp": "^0.5.0",
"safe-buffer": "^5.1.2",
"yallist": "^3.0.2"
}
},
"util-deprecate": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
},
"wide-align": {
"version": "1.1.3",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"string-width": "^1.0.2 || 2"
}
},
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
"dev": true
}
}
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@@ -2189,27 +1607,6 @@
"integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=",
"dev": true
},
"glob-parent": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
"integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
"dev": true,
"requires": {
"is-glob": "^3.1.0",
"path-dirname": "^1.0.0"
},
"dependencies": {
"is-glob": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
"integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
"dev": true,
"requires": {
"is-extglob": "^2.1.0"
}
}
}
},
"globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
@@ -2377,15 +1774,6 @@
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
"dev": true
},
"is-binary-path": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
"integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
"dev": true,
"requires": {
"binary-extensions": "^1.0.0"
}
},
"is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
@@ -2449,21 +1837,6 @@
"integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=",
"dev": true
},
"is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
"dev": true
},
"is-glob": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
"integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
"dev": true,
"requires": {
"is-extglob": "^2.1.1"
}
},
"is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
@@ -2778,13 +2151,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"nan": {
"version": "2.13.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz",
"integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==",
"dev": true,
"optional": true
},
"nanomatch": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@@ -2825,9 +2191,9 @@
}
},
"node-releases": {
"version": "1.1.17",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.17.tgz",
"integrity": "sha512-/SCjetyta1m7YXLgtACZGDYJdCSIBAWorDWkGCGZlydP2Ll7J48l7j/JxNYZ+xsgSPbWfdulVS/aY+GdjUsQ7Q==",
"version": "1.1.18",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.18.tgz",
"integrity": "sha512-/mnVgm6u/8OwlIsoyRXtTI0RfQcxZoAZbdwyXap0EeWwcOpDDymyCHM2/aR9XKmHXrvizHoPAOs0pcbiJ6RUaA==",
"dev": true,
"requires": {
"semver": "^5.3.0"
@@ -2845,12 +2211,6 @@
"validate-npm-package-license": "^3.0.1"
}
},
"normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true
},
"npm-run-all": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz",
@@ -2953,18 +2313,6 @@
"integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=",
"dev": true
},
"path-dirname": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
"integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=",
"dev": true
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true
},
"path-key": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
@@ -3059,17 +2407,6 @@
"util-deprecate": "~1.0.1"
}
},
"readdirp": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz",
"integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.11",
"micromatch": "^3.1.10",
"readable-stream": "^2.0.2"
}
},
"regenerate": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
@@ -3159,12 +2496,6 @@
"integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=",
"dev": true
},
"remove-trailing-separator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
"integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
"dev": true
},
"repeat-element": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz",
@@ -3205,13 +2536,13 @@
"dev": true
},
"rollup": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-1.10.1.tgz",
"integrity": "sha512-pW353tmBE7QP622ITkGxtqF0d5gSRCVPD9xqM+fcPjudeZfoXMFW2sCzsTe2TU/zU1xamIjiS9xuFCPVT9fESw==",
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-1.11.3.tgz",
"integrity": "sha512-81MR7alHcFKxgWzGfG7jSdv+JQxSOIOD/Fa3iNUmpzbd7p+V19e1l9uffqT8/7YAHgGOzmoPGN3Fx3L2ptOf5g==",
"dev": true,
"requires": {
"@types/estree": "0.0.39",
"@types/node": "^11.13.5",
"@types/node": "^11.13.9",
"acorn": "^6.1.1"
}
},
@@ -3394,9 +2725,9 @@
"dev": true
},
"sirv": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-0.2.5.tgz",
"integrity": "sha512-q7F1VElkj/WPrXwWsdHK9gqi2rd94oaMQc3VtN8N1TNHrKxlsd7hGsPFiOIodRxre8eHCV1XK1iqC1BDaZ+IKA==",
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-0.4.2.tgz",
"integrity": "sha512-dQbZnsMaIiTQPZmbGmktz+c74zt/hyrJEB4tdp2Jj0RNv9J6B/OWR5RyrZEvIn9fyh9Zlg2OlE2XzKz6wMKGAw==",
"requires": {
"@polka/url": "^0.5.0",
"mime": "^2.3.1"
@@ -3662,9 +2993,9 @@
}
},
"svelte": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.1.0.tgz",
"integrity": "sha512-b5TyzV7Dx1ijN4QPNarhKq5rX98QHDmi18nF0G8KV3d5KX3Jj98Yu4+tzM97ktnXcfoVJmvONvPaX1ZI0mr8Dw==",
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.2.2.tgz",
"integrity": "sha512-B6ePpPodCQodVFDwv/uByaWs0KvXojaXZiB/db1x7WXbyxgLJ8xZF3j1lk5M83Ox0AWlX0fQnCTAaZmfT6xqdw==",
"dev": true
},
"terser": {
@@ -3881,12 +3212,6 @@
}
}
},
"upath": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz",
"integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==",
"dev": true
},
"upper-case": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz",

View File

@@ -15,30 +15,29 @@
},
"dependencies": {
"@polka/send": "^0.4.0",
"compression": "^1.7.1",
"compression": "^1.7.4",
"highlight.js": "^9.15.6",
"marked": "^0.6.2",
"polka": "^0.5.2",
"prismjs": "^1.16.0",
"sirv": "^0.2.0"
"sirv": "^0.4.2"
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-transform-runtime": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/runtime": "^7.0.0",
"@sveltejs/site-kit": "^1.0.1",
"chokidar": "^2.0.4",
"@babel/core": "^7.4.4",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-runtime": "^7.4.4",
"@babel/preset-env": "^7.4.4",
"@babel/runtime": "^7.4.4",
"@sveltejs/site-kit": "^1.0.4",
"npm-run-all": "^4.1.5",
"rollup": "^1.0.0",
"rollup-plugin-babel": "^4.0.2",
"rollup-plugin-commonjs": "^9.1.6",
"rollup-plugin-node-resolve": "^4.0.0",
"rollup-plugin-replace": "^2.0.0",
"rollup-plugin-svelte": "^5.0.1",
"rollup": "^1.11.3",
"rollup-plugin-babel": "^4.3.2",
"rollup-plugin-commonjs": "^9.3.4",
"rollup-plugin-node-resolve": "^4.2.3",
"rollup-plugin-replace": "^2.2.0",
"rollup-plugin-svelte": "^5.0.3",
"rollup-plugin-terser": "^4.0.4",
"sapper": "alpha",
"svelte": "^3.1.0"
"svelte": "^3.2.2"
}
}

View File

@@ -2,8 +2,8 @@
<html lang='en' class="theme-default typo-default">
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width;initial-scale=1.0'>
<meta name='theme-color' content='#ff3e00'>
<meta name='viewport' content='width=device-width,initial-scale=1.0'>
<meta name='theme-color' content='#159794'>
%sapper.base%

View File

@@ -1,6 +1,6 @@
{
"background_color": "#ffffff",
"theme_color": "#ff3e00",
"theme_color": "#159794",
"name": "Sapper",
"short_name": "Sapper",
"display": "minimal-ui",

View File

@@ -251,11 +251,11 @@ function generate_app(manifest_data: ManifestData, path_to_routes: string) {
let l = max_depth;
let pyramid = `<svelte:component this={level${l}.component} {...level${l}.props}/>`;
let pyramid = `<svelte:component this="{level${l}.component}" {...level${l}.props}/>`;
while (l-- > 1) {
pyramid = `
<svelte:component this={level${l}.component} segment={segments[${l}]} {...level${l}.props}>
<svelte:component this="{level${l}.component}" segment="{segments[${l}]}" {...level${l}.props}>
{#if level${l + 1}}
${pyramid.replace(/\n/g, '\n\t\t\t\t\t')}
{/if}
@@ -281,7 +281,7 @@ function generate_app(manifest_data: ManifestData, path_to_routes: string) {
setContext(CONTEXT_KEY, stores);
</script>
<Layout segment={segments[0]} {...level0.props}>
<Layout segment="{segments[0]}" {...level0.props}>
{#if error}
<Error {error} {status}/>
{:else}

View File

@@ -25,6 +25,19 @@ export default function create_manifest_data(cwd: string): ManifestData {
return false;
}
function find_layout(file_name: string, component_name: string, dir: string = '') {
const ext = component_extensions.find((ext) => fs.existsSync(path.join(cwd, dir, `${file_name}${ext}`)));
const file = posixify(path.join(dir, `${file_name}${ext}`))
return ext
? {
name: component_name,
file: file,
has_preload: has_preload(file)
}
: null;
}
const components: PageComponent[] = [];
const pages: Page[] = [];
const server_routes: ServerRoute[] = [];
@@ -61,15 +74,19 @@ export default function create_manifest_data(cwd: string): ManifestData {
const is_dir = fs.statSync(resolved).isDirectory();
const ext = path.extname(basename);
if (basename[0] === '_') return null;
if (basename[0] === '.' && basename !== '.well-known') return null;
if (!is_dir && !/^\.[a-z]+$/i.test(ext)) return null; // filter out tmp files etc
const segment = is_dir
? basename
: basename.slice(0, -path.extname(basename).length);
: basename.slice(0, -ext.length);
const parts = get_parts(segment);
const is_index = is_dir ? false : basename.startsWith('index.');
const is_page = component_extensions.indexOf(ext) !== -1;
const route_suffix = basename.slice(basename.indexOf('.'), -ext.length);
parts.forEach(part => {
if (/\]\[/.test(part.content)) {
@@ -88,39 +105,35 @@ export default function create_manifest_data(cwd: string): ManifestData {
file: posixify(file),
is_dir,
is_index,
is_page
is_page,
route_suffix
};
})
.filter(Boolean)
.sort(comparator);
items.forEach(item => {
if (item.basename[0] === '_') return;
if (item.basename[0] === '.') {
if (item.file !== '.well-known') return;
}
const segments = parent_segments.slice();
if (item.is_index && segments.length > 0) {
const last_segment = segments[segments.length - 1].slice();
const suffix = item.basename
.slice(0, -path.extname(item.basename).length).
replace('index', '');
if (item.is_index) {
if (item.route_suffix) {
if (segments.length > 0) {
const last_segment = segments[segments.length - 1].slice();
const last_part = last_segment[last_segment.length - 1];
if (suffix) {
const last_part = last_segment[last_segment.length - 1];
if (last_part.dynamic) {
last_segment.push({ dynamic: false, content: suffix });
if (last_part.dynamic) {
last_segment.push({ dynamic: false, content: item.route_suffix });
} else {
last_segment[last_segment.length - 1] = {
dynamic: false,
content: `${last_part.content}${item.route_suffix}`
};
}
segments[segments.length - 1] = last_segment;
} else {
last_segment[last_segment.length - 1] = {
dynamic: false,
content: `${last_part.content}${suffix}`
};
segments.push(item.parts);
}
segments[segments.length - 1] = last_segment;
}
} else {
segments.push(item.parts);
@@ -130,16 +143,7 @@ export default function create_manifest_data(cwd: string): ManifestData {
params.push(...item.parts.filter(p => p.dynamic).map(p => p.content));
if (item.is_dir) {
const ext = component_extensions.find((ext: string) => {
const index = path.join(dir, item.basename, `_layout${ext}`);
return fs.existsSync(index);
});
const component = ext && {
name: `${get_slug(item.file)}__layout`,
file: `${item.file}/_layout${ext}`,
has_preload: has_preload(`${item.file}/_layout${ext}`)
};
const component = find_layout('_layout', `${get_slug(item.file)}__layout`, item.file);
if (component) components.push(component);
@@ -154,8 +158,6 @@ export default function create_manifest_data(cwd: string): ManifestData {
}
else if (item.is_page) {
const is_index = item.basename === `index${item.ext}`;
const component = {
name: get_slug(item.file),
file: item.file,
@@ -164,22 +166,20 @@ export default function create_manifest_data(cwd: string): ManifestData {
components.push(component);
const parts = (is_index && stack[stack.length - 1] === null)
const parts = (item.is_index && stack[stack.length - 1] === null)
? stack.slice(0, -1).concat({ component, params })
: stack.concat({ component, params })
const page = {
pattern: get_pattern(is_index ? parent_segments : segments, true),
pages.push({
pattern: get_pattern(segments, true),
parts
};
pages.push(page);
});
}
else {
server_routes.push({
name: `route_${get_slug(item.file)}`,
pattern: get_pattern(segments, false),
pattern: get_pattern(segments, !item.route_suffix),
file: item.file,
params: params
});
@@ -187,23 +187,8 @@ export default function create_manifest_data(cwd: string): ManifestData {
});
}
const root_ext = component_extensions.find(ext => fs.existsSync(path.join(cwd, `_layout${ext}`)));
const root = root_ext
? {
name: 'main',
file: `_layout${root_ext}`,
has_preload: has_preload(`_layout${root_ext}`)
}
: default_layout;
const error_ext = component_extensions.find(ext => fs.existsSync(path.join(cwd, `_error${ext}`)));
const error = error_ext
? {
name: 'error',
file: `_error${error_ext}`,
has_preload: has_preload(`_error${error_ext}`)
}
: default_error;
const root = find_layout('_layout', 'main') || default_layout;
const error = find_layout('_error', 'error') || default_error;
walk(cwd, [], [], []);
@@ -249,7 +234,7 @@ type Part = {
spread?: boolean;
};
function is_spead(path: string) {
function is_spread(path: string) {
const spread_pattern = /\[\.{3}/g;
return spread_pattern.test(path)
}
@@ -259,9 +244,9 @@ function comparator(
b: { basename: string, parts: Part[], file: string, is_index: boolean }
) {
if (a.is_index !== b.is_index) {
if (a.is_index) return is_spead(a.file) ? 1 : -1;
if (a.is_index) return is_spread(a.file) ? 1 : -1;
return is_spead(b.file) ? -1 : 1;
return is_spread(b.file) ? -1 : 1;
}
const max = Math.max(a.parts.length, b.parts.length);
@@ -334,7 +319,6 @@ function get_parts(part: string): Part[] {
function get_slug(file: string) {
let name = file
.replace(/[\\\/]index/, '')
.replace(/_default([\/\\index])?\.html$/, 'index')
.replace(/[\/\\]/g, '_')
.replace(/\.\w+$/, '')
.replace(/\[([^(]+)(?:\([^(]+\))?\]/, '$$$1')
@@ -347,19 +331,19 @@ function get_slug(file: string) {
}
function get_pattern(segments: Part[][], add_trailing_slash: boolean) {
return new RegExp(
`^` +
segments.map(segment => {
return '\\/' + segment.map(part => {
return part.dynamic
? part.qualifier || part.spread ? '(.+)' : '([^\\/]+?)'
: encodeURI(part.content.normalize())
.replace(/\?/g, '%3F')
.replace(/#/g, '%23')
.replace(/%5B/g, '[')
.replace(/%5D/g, ']');
}).join('');
}).join('') +
(add_trailing_slash ? '\\\/?$' : '$')
);
const path = segments.map(segment => {
return segment.map(part => {
return part.dynamic
? part.qualifier || part.spread ? '(.+)' : '([^\\/]+?)'
: encodeURI(part.content.normalize())
.replace(/\?/g, '%3F')
.replace(/#/g, '%23')
.replace(/%5B/g, '[')
.replace(/%5D/g, ']');
}).join('');
}).join('\\/');
const trailing = add_trailing_slash && segments.length ? '\\/?$' : '$';
return new RegExp(`^\\/${path}${trailing}`);
}

View File

@@ -1,49 +1,77 @@
import * as path from 'path';
import puppeteer from 'puppeteer';
import * as ports from 'port-authority';
import { fork, ChildProcess } from 'child_process';
import { AddressInfo } from 'net';
import { wait } from '../utils'
const DEFAULT_ENTRY = '__sapper__/build/server/server.js';
const DELAY = parseInt(process.env.SAPPER_TEST_DELAY) || 50;
declare const start: () => Promise<void>;
declare const prefetchRoutes: () => Promise<void>;
declare const prefetch: (href: string) => Promise<void>;
declare const goto: (href: string) => Promise<void>;
type StartOpts = {
requestInterceptor?: (interceptedRequst: puppeteer.Request) => any
};
export class AppRunner {
cwd: string;
entry: string;
port: number;
proc: ChildProcess;
exiting: boolean;
terminate: Promise<any>;
server: ChildProcess;
address: AddressInfo;
base: string;
messages: any[];
errors: Error[];
browser: puppeteer.Browser;
page: puppeteer.Page;
constructor(cwd: string, entry: string) {
this.cwd = cwd;
this.entry = path.join(cwd, entry);
sapper = {
start: () => this.page.evaluate(() => start()).then(() => void 0),
prefetchRoutes: () => this.page.evaluate(() => prefetchRoutes()).then(() => void 0),
prefetch: (href: string) => this.page.evaluate((href: string) => prefetch(href), href).then(() => void 0),
goto: (href: string) => this.page.evaluate((href: string) => goto(href), href).then(() => void 0)
};
constructor() {
this.messages = [];
this.errors = [];
}
async start({ requestInterceptor }: StartOpts = {}) {
this.port = await ports.find(3000);
async start(cwd: string, entry: string = DEFAULT_ENTRY) {
const server_listening = deferred();
const server_closed = deferred();
const browser_closed = deferred();
this.proc = fork(this.entry, [], {
cwd: this.cwd,
env: {
PORT: String(this.port)
this.terminate = Promise.all([server_closed, browser_closed]);
this.server = fork(path.join(cwd, entry), [], { cwd });
this.server.on('exit', () => {
server_listening.reject();
server_closed.settle(this.exiting);
});
this.server.on('message', message => {
if (!message.__sapper__) return;
switch (message.event) {
case 'listening':
this.address = message.address;
this.base = `http://localhost:${this.address.port}`;
server_listening.resolve();
break;
case 'error':
this.errors.push(Object.assign(new Error(), message.error));
break;
default:
this.messages.push(message);
}
});
this.proc.on('message', message => {
if (!message.__sapper__) return;
this.messages.push(message);
});
this.browser = await puppeteer.launch({ args: ['--no-sandbox'] });
this.browser.on('disconnected', () => browser_closed.settle(this.exiting));
this.page = await this.browser.newPage();
this.page.on('console', msg => {
@@ -54,25 +82,28 @@ export class AppRunner {
}
});
if (requestInterceptor) {
await this.page.setRequestInterception(true);
this.page.on('request', requestInterceptor);
}
await server_listening;
return {
page: this.page,
base: `http://localhost:${this.port}`,
// helpers
start: () => this.page.evaluate(() => start()).then(() => void 0),
prefetchRoutes: () => this.page.evaluate(() => prefetchRoutes()).then(() => void 0),
prefetch: (href: string) => this.page.evaluate((href: string) => prefetch(href), href).then(() => void 0),
goto: (href: string) => this.page.evaluate((href: string) => goto(href), href).then(() => void 0),
title: () => this.page.$eval('h1', node => node.textContent).then(serializable => String(serializable))
};
return this;
}
capture(fn: () => any): Promise<string[]> {
load(url: string) {
if (url[0] === '/') {
url = `${this.base}${url}`;
}
return this.page.goto(url);
}
text(selector: string) {
return this.page.$eval(selector, node => node.textContent);
}
wait(extra_ms: number = 0) {
return wait(DELAY + extra_ms);
}
capture_requests(fn: () => any): Promise<string[]> {
return new Promise((fulfil, reject) => {
const requests: string[] = [];
const pending: Set<string> = new Set();
@@ -120,13 +151,55 @@ export class AppRunner {
});
}
end() {
return Promise.all([
this.browser.close(),
new Promise(fulfil => {
this.proc.once('exit', fulfil);
this.proc.kill();
})
]);
async intercept_requests(interceptor: (request: puppeteer.Request) => void, fn: () => any): Promise<void> {
const unique_interceptor = request => interceptor(request);
this.page.prependListener('request', unique_interceptor);
await this.page.setRequestInterception(true);
const result = await Promise.resolve(fn());
await this.page.setRequestInterception(false);
this.page.removeListener('request', unique_interceptor);
return result;
}
}
end() {
this.exiting = true;
this.server.kill();
this.browser.close();
return this.terminate;
}
}
interface Deferred<T> extends Promise<T> {
resolve: (value?: T | PromiseLike<T>) => void;
reject: (reason?: any) => void;
settle: (result?: boolean) => void;
}
function settle<T>(this: Deferred<T>, result: boolean) {
if (result) {
this.resolve();
} else {
this.reject();
}
}
function deferred<T>() {
let resolve, reject;
const deferred = new Promise((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
}) as Deferred<T>;
deferred.resolve = resolve;
deferred.reject = reject;
deferred.settle = settle;
return deferred;
}

View File

@@ -1,8 +1,9 @@
import polka from 'polka';
import * as sapper from '@sapper/server';
const { PORT } = process.env;
import { start } from '../../common.js';
polka()
const app = polka()
.use(sapper.middleware())
.listen(PORT);
start(app);

View File

@@ -1,23 +1,23 @@
import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import * as http from 'http';
import { build } from '../../../api';
import { AppRunner } from '../AppRunner';
import { wait } from '../../utils';
declare let deleted: { id: number };
declare let el: any;
function get(url: string, opts?: any): Promise<{ headers: Record<string, string>, body: string }> {
type Response = { headers: http.IncomingHttpHeaders, body: string };
function get(url: string, opts: http.RequestOptions = {}): Promise<Response> {
return new Promise((fulfil, reject) => {
const req = http.get(url, opts || {}, res => {
const req = http.get(url, opts, res => {
res.on('error', reject);
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
fulfil({
headers: res.headers as Record<string, string>,
headers: res.headers,
body
});
});
@@ -30,114 +30,104 @@ function get(url: string, opts?: any): Promise<{ headers: Record<string, string>
describe('basics', function() {
this.timeout(10000);
let runner: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
let prefetchRoutes: () => Promise<void>;
let prefetch: (href: string) => Promise<void>;
let goto: (href: string) => Promise<void>;
let title: () => Promise<string>;
let r: AppRunner;
// hooks
before(async () => {
await build({ cwd: __dirname });
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes, prefetch, goto, title } = await runner.start());
before('build app', () => build({ cwd: __dirname }));
before('start runner', async () => {
r = await new AppRunner().start(__dirname);
});
after(() => runner.end());
after(() => r && r.end());
// tests
it('serves /', async () => {
await page.goto(base);
await r.load('/');
assert.equal(
await title(),
await r.text('h1'),
'Great success!'
);
});
it('serves /?', async () => {
await page.goto(`${base}?`);
await r.load('/?');
assert.equal(
await title(),
await r.text('h1'),
'Great success!'
);
});
it('serves static route', async () => {
await page.goto(`${base}/a`);
await r.load('/a');
assert.equal(
await title(),
await r.text('h1'),
'a'
);
});
it('serves static route from dir/index.html file', async () => {
await page.goto(`${base}/b`);
await r.load('/b');
assert.equal(
await title(),
await r.text('h1'),
'b'
);
});
it('serves dynamic route', async () => {
await page.goto(`${base}/test-slug`);
await r.load('/test-slug');
assert.equal(
await title(),
await r.text('h1'),
'TEST-SLUG'
);
});
it('navigates to a new page without reloading', async () => {
await page.goto(base);
await start();
await prefetchRoutes();
await r.load('/');
await r.sapper.start();
await r.sapper.prefetchRoutes();
const requests: string[] = await runner.capture(async () => {
await page.click('a[href="a"]');
const requests: string[] = await r.capture_requests(async () => {
await r.page.click('a[href="a"]');
await r.wait();
});
assert.deepEqual(requests, []);
assert.equal(
await title(),
await r.text('h1'),
'a'
);
});
it('navigates programmatically', async () => {
await page.goto(`${base}/a`);
await start();
await goto('b');
await r.load('/a');
await r.sapper.start();
await r.sapper.goto('b');
assert.equal(
await title(),
await r.text('h1'),
'b'
);
});
it('prefetches programmatically', async () => {
await page.goto(`${base}/a`);
await start();
await r.load(`/a`);
await r.sapper.start();
const requests = await runner.capture(() => prefetch('b'));
const requests = await r.capture_requests(() => r.sapper.prefetch('b'));
assert.equal(requests.length, 2);
assert.equal(requests[1], `${base}/b.json`);
assert.equal(requests[1], `${r.base}/b.json`);
});
// TODO equivalent test for a webpack app
it('sets Content-Type, Link...modulepreload, and Cache-Control headers', async () => {
const { headers } = await get(base);
const { headers } = await get(r.base);
assert.equal(
headers['content-type'],
@@ -157,162 +147,163 @@ describe('basics', function() {
});
it('calls a delete handler', async () => {
await page.goto(`${base}/delete-test`);
await start();
await r.load('/delete-test');
await r.sapper.start();
await page.click('.del');
await page.waitForFunction(() => deleted);
await r.page.click('.del');
await r.page.waitForFunction(() => deleted);
assert.equal(await page.evaluate(() => deleted.id), 42);
assert.equal(await r.page.evaluate(() => deleted.id), 42);
});
it('hydrates initial route', async () => {
await page.goto(base);
await r.load('/');
await page.evaluate(() => {
await r.page.evaluate(() => {
el = document.querySelector('.hydrate-test');
});
await start();
await r.sapper.start();
assert.ok(await page.evaluate(() => {
assert.ok(await r.page.evaluate(() => {
return document.querySelector('.hydrate-test') === el;
}));
});
it('does not attempt client-side navigation to server routes', async () => {
await page.goto(base);
await start();
await prefetchRoutes();
await r.load('/');
await r.sapper.start();
await r.sapper.prefetchRoutes();
await page.click(`[href="ambiguous/ok.json"]`);
await wait(50);
await r.page.click('[href="ambiguous/ok.json"]');
await r.wait();
assert.equal(
await page.evaluate(() => document.body.textContent),
await r.text('body'),
'ok'
);
});
it('allows reserved words as route names', async () => {
await page.goto(`${base}/const`);
await start();
await r.load('/const');
await r.sapper.start();
assert.equal(
await title(),
await r.text('h1'),
'reserved words are okay as routes'
);
});
it('accepts value-less query string parameter on server', async () => {
await page.goto(`${base}/echo-query?message`);
await r.load('/echo-query?message');
assert.equal(
await title(),
await r.text('h1'),
'{"message":""}'
);
});
it('accepts value-less query string parameter on client', async () => {
await page.goto(base);
await start();
await prefetchRoutes();
await r.load('/');
await r.sapper.start();
await r.sapper.prefetchRoutes();
await page.click('a[href="echo-query?message"]')
await r.page.click('a[href="echo-query?message"]');
await r.wait();
assert.equal(
await title(),
await r.text('h1'),
'{"message":""}'
);
});
it('accepts duplicated query string parameter on server', async () => {
await page.goto(`${base}/echo-query?p=one&p=two`);
await r.load('/echo-query?p=one&p=two');
assert.equal(
await title(),
await r.text('h1'),
'{"p":["one","two"]}'
);
});
it('accepts duplicated query string parameter on client', async () => {
await page.goto(base);
await start();
await prefetchRoutes();
await r.load('/');
await r.sapper.start();
await r.sapper.prefetchRoutes();
await page.click('a[href="echo-query?p=one&p=two"]')
await r.page.click('a[href="echo-query?p=one&p=two"]')
assert.equal(
await title(),
await r.text('h1'),
'{"p":["one","two"]}'
);
});
// skipped because Nightmare doesn't seem to focus the <a> correctly
it('resets the active element after navigation', async () => {
await page.goto(base);
await start();
await prefetchRoutes();
await r.load('/');
await r.sapper.start();
await r.sapper.prefetchRoutes();
await page.click('[href="a"]');
await wait(50);
await r.page.click('[href="a"]');
await r.wait();
assert.equal(
await page.evaluate(() => document.activeElement.nodeName),
await r.page.evaluate(() => document.activeElement.nodeName),
'BODY'
);
});
it('replaces %sapper.xxx% tags safely', async () => {
await page.goto(`${base}/unsafe-replacement`);
await start();
await r.load('/unsafe-replacement');
await r.sapper.start();
const html = String(await page.evaluate(() => document.body.innerHTML));
const html = String(await r.page.evaluate(() => document.body.innerHTML));
assert.equal(html.indexOf('%sapper'), -1);
});
it('navigates between routes with empty parts', async () => {
await page.goto(`${base}/dirs/foo`);
await start();
await r.load('/dirs/foo');
await r.sapper.start();
assert.equal(await title(), 'foo');
assert.equal(await r.text('h1'), 'foo');
await page.click('[href="dirs/bar"]');
await wait(50);
assert.equal(await title(), 'bar');
await r.page.click('[href="dirs/bar"]');
await r.wait();
assert.equal(await r.text('h1'), 'bar');
});
it('navigates to ...rest', async () => {
await page.goto(`${base}/abc/xyz`);
await start();
await r.load('/abc/xyz');
await r.sapper.start();
assert.equal(await title(), 'abc,xyz');
assert.equal(await r.text('h1'), 'abc,xyz');
await page.click('[href="xyz/abc/deep"]');
await wait(50);
assert.equal(await title(), 'xyz,abc');
await r.page.click('[href="xyz/abc/deep"]');
await r.wait();
assert.equal(await r.text('h1'), 'xyz,abc');
await page.click(`[href="xyz/abc/qwe/deep.json"]`);
await wait(50);
await r.page.click('[href="xyz/abc/qwe/deep.json"]');
await r.wait();
assert.equal(
await page.evaluate(() => document.body.textContent),
await r.text('body'),
'xyz,abc,qwe'
);
});
it('navigates between dynamic routes with same segments', async () => {
await page.goto(`${base}/dirs/bar/xyz`);
await start();
await r.load('/dirs/bar/xyz');
await r.sapper.start();
assert.equal(await title(), 'A page');
assert.equal(await r.text('h1'), 'A page');
await page.click('[href="dirs/foo/xyz"]');
await wait(50);
assert.equal(await title(), 'B page');
await r.page.click('[href="dirs/foo/xyz"]');
await r.wait();
assert.equal(await r.text('h1'), 'B page');
});
it('runs server route handlers before page handlers, if they match', async () => {
const json = await get(`${base}/middleware`, {
const json = await get(`${r.base}/middleware`, {
headers: {
'Accept': 'application/json'
}
@@ -320,22 +311,26 @@ describe('basics', function() {
assert.equal(json.body, '{"json":true}');
const html = await get(`${base}/middleware`);
const html = await get(`${r.base}/middleware`);
assert.ok(html.body.indexOf('<h1>HTML</h1>') !== -1);
});
it('invalidates page when a segment is skipped', async () => {
await page.goto(`${base}/skipped/x/1`);
await start();
await prefetchRoutes();
await r.load('/skipped/x/1');
await r.sapper.start();
await r.sapper.prefetchRoutes();
await page.click('a[href="skipped/y/1"]');
await wait(50);
await r.page.click('a[href="skipped/y/1"]');
await r.wait();
assert.equal(
await title(),
await r.text('h1'),
'y:1'
);
});
it('survives the tests with no server errors', () => {
assert.deepEqual(r.errors, []);
});
});

42
test/apps/common.js Normal file
View File

@@ -0,0 +1,42 @@
const { NODE_ENV, PORT } = process.env;
export const dev = NODE_ENV === 'development';
export function start(app) {
const port = parseInt(PORT) || 0;
app.listen(port, () => {
const address = app.server.address();
process.env.PORT = address.port;
send({
__sapper__: true,
event: 'listening',
address
});
});
}
const properties = ['name', 'message', 'stack', 'code', 'lineNumber', 'fileName'];
function send(message) {
process.send && process.send(message);
}
function send_error(error) {
send({
__sapper__: true,
event: 'error',
error: properties.reduce((object, key) => ({...object, [key]: error[key]}), {})
})
}
process.on('unhandledRejection', (reason, p) => {
send_error(reason);
});
process.on('uncaughtException', err => {
send_error(err);
process.exitCode = 1;
});

View File

@@ -1,13 +1,14 @@
import polka from 'polka';
import * as sapper from '@sapper/server';
const { PORT } = process.env;
import { start } from '../../common.js';
polka()
const app = polka()
.use((req, res, next) => {
// set test cookie
res.setHeader('Set-Cookie', ['a=1; Max-Age=3600', 'b=2; Max-Age=3600']);
next();
})
.use(sapper.middleware())
.listen(PORT);
start(app);

View File

@@ -1,59 +1,54 @@
import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api';
import { wait } from '../../utils';
import { AppRunner } from '../AppRunner';
describe('credentials', function() {
this.timeout(10000);
let runner: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
let prefetchRoutes: () => Promise<void>;
let r: AppRunner;
// hooks
before(async () => {
await build({ cwd: __dirname });
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes } = await runner.start());
before('build app', () => build({ cwd: __dirname }));
before('start runner', async () => {
r = await new AppRunner().start(__dirname);
});
after(() => runner.end());
after(() => r && r.end());
// tests
it('sends cookies when using this.fetch with credentials: "include"', async () => {
await page.goto(`${base}/credentials?creds=include`);
await r.load('/credentials?creds=include');
assert.equal(
await page.$eval('h1', node => node.textContent),
await r.text('h1'),
'a: 1, b: 2, max-age: undefined'
);
});
it('does not send cookies when using this.fetch without credentials', async () => {
await page.goto(`${base}/credentials`);
await r.load('/credentials');
assert.equal(
await page.$eval('h1', node => node.textContent),
await r.text('h1'),
'unauthorized'
);
});
it('delegates to fetch on the client', async () => {
await page.goto(base)
await start();
await prefetchRoutes();
await r.load('/')
await r.sapper.start();
await r.sapper.prefetchRoutes();
await page.click('[href="credentials?creds=include"]');
await wait(50);
await r.page.click('[href="credentials?creds=include"]');
await r.wait();
assert.equal(
await page.$eval('h1', node => node.textContent),
await r.text('h1'),
'a: 1, b: 2, max-age: undefined'
);
});
});
it('survives the tests with no server errors', () => {
assert.deepEqual(r.errors, []);
});
});

View File

@@ -1,8 +1,9 @@
import polka from 'polka';
import * as sapper from '@sapper/server';
const { PORT } = process.env;
import { start } from '../../common.js';
polka()
.use(sapper.middleware())
.listen(PORT);
const app = polka()
.use(sapper.middleware());
start(app);

View File

@@ -1,78 +1,61 @@
import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api';
import { AppRunner } from '../AppRunner';
import { wait } from '../../utils';
describe('css', function() {
this.timeout(10000);
let runner: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
let prefetchRoutes: () => Promise<void>;
let prefetch: (href: string) => Promise<void>;
let goto: (href: string) => Promise<void>;
let title: () => Promise<string>;
let r: AppRunner;
// hooks
before(async () => {
await build({ cwd: __dirname });
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes, prefetch, goto, title } = await runner.start());
before('build app', () => build({ cwd: __dirname }));
before('start runner', async () => {
r = await new AppRunner().start(__dirname);
});
after(() => runner.end());
after(() => r && r.end());
// tests
it('includes critical CSS with server render', async () => {
await page.goto(base);
await r.load('/');
assert.equal(
await page.evaluate(() => {
const h1 = document.querySelector('h1');
return getComputedStyle(h1).color;
}),
await r.page.$eval('h1', node => getComputedStyle(node).color),
'rgb(255, 0, 0)'
);
});
it('loads CSS when navigating client-side', async () => {
await page.goto(base);
await r.load('/');
await start();
await prefetchRoutes();
await r.sapper.start();
await r.sapper.prefetchRoutes();
await page.click(`[href="foo"]`);
await wait(50);
await r.page.click(`[href="foo"]`);
await r.wait();
assert.equal(
await page.evaluate(() => {
const h1 = document.querySelector('h1');
return getComputedStyle(h1).color;
}),
await r.page.$eval('h1', node => getComputedStyle(node).color),
'rgb(0, 0, 255)'
);
});
it('loads CSS for a lazily-rendered component', async () => {
await page.goto(base);
await r.load('/');
await start();
await prefetchRoutes();
await r.sapper.start();
await r.sapper.prefetchRoutes();
await page.click(`[href="bar"]`);
await wait(50);
await r.page.click(`[href="bar"]`);
await r.wait();
assert.equal(
await page.evaluate(() => {
const h1 = document.querySelector('h1');
return getComputedStyle(h1).color;
}),
await r.page.$eval('h1', node => getComputedStyle(node).color),
'rgb(0, 128, 0)'
);
});
});
it('survives the tests with no server errors', () => {
assert.deepEqual(r.errors, []);
});
});

View File

@@ -1,8 +1,9 @@
import polka from 'polka';
import * as sapper from '@sapper/server';
const { PORT } = process.env;
import { start } from '../../common.js';
polka()
.use(sapper.middleware())
.listen(PORT);
const app = polka()
.use(sapper.middleware());
start(app);

View File

@@ -1,68 +1,63 @@
import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api';
import { AppRunner } from '../AppRunner';
import { wait } from '../../utils';
describe('encoding', function() {
this.timeout(10000);
let runner: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
let prefetchRoutes: () => Promise<void>;
let r: AppRunner;
// hooks
before(async () => {
await build({ cwd: __dirname });
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes } = await runner.start());
before('build app', () => build({ cwd: __dirname }));
before('start runner', async () => {
r = await new AppRunner().start(__dirname);
});
after(() => runner.end());
after(() => r && r.end());
// tests
it('encodes routes', async () => {
await page.goto(`${base}/fünke`);
await r.load('/fünke');
assert.equal(
await page.$eval('h1', node => node.textContent),
await r.text('h1'),
`I'm afraid I just blue myself`
);
});
it('encodes req.params and req.query for server-rendered pages', async () => {
await page.goto(`${base}/echo/page/encöded?message=hëllö+wörld&föo=bar&=baz&tel=%2B123456789`);
await r.load('/echo/page/encöded?message=hëllö+wörld&föo=bar&=baz&tel=%2B123456789');
assert.equal(
await page.$eval('h1', node => node.textContent),
await r.text('h1'),
'encöded {"message":"hëllö wörld","föo":"bar","":"baz","tel":"+123456789"}'
);
});
it('encodes req.params and req.query for client-rendered pages', async () => {
await page.goto(base);
await start();
await prefetchRoutes();
await r.load('/');
await r.sapper.start();
await r.sapper.prefetchRoutes();
await page.click('a');
await wait(50);
await r.page.click('a');
await r.wait();
assert.equal(
await page.$eval('h1', node => node.textContent),
await r.text('h1'),
'encöded {"message":"hëllö wörld","föo":"bar","":"baz","tel":"+123456789"}'
);
});
it('encodes req.params for server routes', async () => {
await page.goto(`${base}/echo/server-route/encöded`);
await r.load('/echo/server-route/encöded');
assert.equal(
await page.$eval('h1', node => node.textContent),
await r.text('h1'),
'encöded'
);
});
});
it('survives the tests with no server errors', () => {
assert.deepEqual(r.errors, []);
});
});

View File

@@ -1,3 +1,7 @@
<h1>{error ? error.message : 'No error here'}</h1>
<script>
export let error;
</script>
<h1>{error ? error.message : 'No error here'}</h1>
<a href="enhance-your-calm">Enhance your calm</a>

View File

@@ -1,8 +1,9 @@
import polka from 'polka';
import * as sapper from '@sapper/server';
const { PORT } = process.env;
import { start } from '../../common.js';
polka()
.use(sapper.middleware())
.listen(PORT);
const app = polka()
.use(sapper.middleware());
start(app);

View File

@@ -1,5 +1,4 @@
import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api';
import { AppRunner } from '../AppRunner';
import { wait } from '../../utils';
@@ -7,142 +6,137 @@ import { wait } from '../../utils';
describe('errors', function() {
this.timeout(10000);
let runner: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
let prefetchRoutes: () => Promise<void>;
let title: () => Promise<string>;
let r: AppRunner;
// hooks
before(async () => {
await build({ cwd: __dirname });
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes, title } = await runner.start());
before('build app', () => build({ cwd: __dirname }));
before('start runner', async () => {
r = await new AppRunner().start(__dirname);
});
after(() => runner.end());
after(() => r && r.end());
// tests
it('handles missing route on server', async () => {
await page.goto(`${base}/nope`);
await r.load('/nope');
assert.equal(
await page.$eval('h1', node => node.textContent),
await r.text('h1'),
'404'
);
});
it('handles missing route on client', async () => {
await page.goto(base);
await start();
await r.load('/');
await r.sapper.start();
await page.click('[href="nope"]');
await wait(50);
await r.page.click('[href="nope"]');
await r.wait();
assert.equal(
await page.$eval('h1', node => node.textContent),
await r.text('h1'),
'404'
);
});
it('handles explicit 4xx on server', async () => {
await page.goto(`${base}/blog/nope`);
await r.load('/blog/nope');
assert.equal(
await page.$eval('h1', node => node.textContent),
await r.text('h1'),
'404'
);
});
it('handles explicit 4xx on client', async () => {
await page.goto(base);
await start();
await prefetchRoutes();
await r.load('/');
await r.sapper.start();
await r.sapper.prefetchRoutes();
await page.click('[href="blog/nope"]');
await wait(50);
await r.page.click('[href="blog/nope"]');
await r.wait();
assert.equal(
await page.$eval('h1', node => node.textContent),
await r.text('h1'),
'404'
);
});
it('handles error on server', async () => {
await page.goto(`${base}/throw`);
await r.load('/throw');
assert.equal(
await page.$eval('h1', node => node.textContent),
await r.text('h1'),
'500'
);
});
it('handles error on client', async () => {
await page.goto(base);
await start();
await prefetchRoutes();
await r.load('/');
await r.sapper.start();
await r.sapper.prefetchRoutes();
await page.click('[href="throw"]');
await wait(50);
await r.page.click('[href="throw"]');
await r.wait();
assert.equal(
await page.$eval('h1', node => node.textContent),
await r.text('h1'),
'500'
);
});
it('does not serve error page for explicit non-page errors', async () => {
await page.goto(`${base}/nope.json`);
await r.load('/nope.json');
assert.equal(
await page.evaluate(() => document.body.textContent),
await r.text('body'),
'nope'
);
});
it('does not serve error page for thrown non-page errors', async () => {
await page.goto(`${base}/throw.json`);
await r.load('/throw.json');
assert.equal(
await page.evaluate(() => document.body.textContent),
await r.text('body'),
'oops'
);
});
it('execute error page hooks', async () => {
await page.goto(`${base}/some-throw-page`);
await start();
await wait(50);
await r.load('/some-throw-page');
await r.sapper.start();
assert.equal(
await page.$eval('h2', node => node.textContent),
await r.text('h2'),
'success'
);
})
it('does not serve error page for async non-page error', async () => {
await page.goto(`${base}/async-throw.json`);
await r.load('/async-throw.json');
assert.equal(
await page.evaluate(() => document.body.textContent),
await r.text('body'),
'oops'
);
});
it('clears props.error on successful render', async () => {
await page.goto(`${base}/no-error`);
await start();
await prefetchRoutes();
await r.load('/no-error');
await r.sapper.start();
await r.sapper.prefetchRoutes();
await page.click('[href="enhance-your-calm"]');
await wait(50);
assert.equal(await title(), '420');
await r.page.click('[href="enhance-your-calm"]');
await r.wait();
assert.equal(await r.text('h1'), '420');
await page.goBack();
await wait(50);
assert.equal(await title(), 'No error here');
await r.page.goBack();
await r.wait();
assert.equal(await r.text('h1'), 'No error here');
});
it('survives the tests with no server errors', () => {
assert.deepEqual(r.errors, []);
});
});

View File

@@ -2,14 +2,12 @@ import sirv from 'sirv';
import polka from 'polka';
import * as sapper from '@sapper/server';
const { PORT, NODE_ENV } = process.env;
const dev = NODE_ENV === 'development';
import { start, dev } from '../../common.js';
polka()
const app = polka()
.use(
sirv('static', { dev }),
sapper.middleware()
)
.listen(PORT, err => {
if (err) console.log('error', err);
});
);
start(app);

View File

@@ -6,14 +6,12 @@ describe('export-webpack', function() {
this.timeout(10000);
// hooks
before(async () => {
await api.build({ cwd: __dirname, bundler: 'webpack' });
await api.export({ cwd: __dirname, bundler: 'webpack' });
});
before('build app', () => api.build({ cwd: __dirname, bundler: 'webpack' }));
before('export app', () => api.export({ cwd: __dirname }));
// tests
it('injects <link rel=preload> tags', () => {
const index = fs.readFileSync(`${__dirname}/__sapper__/export/index.html`, 'utf8');
assert.ok(/rel=preload/.test(index));
});
});

View File

@@ -2,14 +2,12 @@ import sirv from 'sirv';
import polka from 'polka';
import * as sapper from '@sapper/server';
const { PORT, NODE_ENV } = process.env;
const dev = NODE_ENV === 'development';
import { start, dev } from '../../common.js';
polka()
const app = polka()
.use(
sirv('static', { dev }),
sapper.middleware()
)
.listen(PORT, err => {
if (err) console.log('error', err);
});
);
start(app);

View File

@@ -6,11 +6,10 @@ describe('export', function() {
this.timeout(10000);
// hooks
before(async () => {
await api.build({ cwd: __dirname });
await api.export({ cwd: __dirname });
});
before('build app', () => api.build({ cwd: __dirname }));
before('export app', () => api.export({ cwd: __dirname }));
// tests
it('crawls a site', () => {
const files = walk(`${__dirname}/__sapper__/export`);
@@ -45,4 +44,4 @@ describe('export', function() {
});
// TODO test timeout, basepath
});
});

View File

@@ -1,3 +1,8 @@
<h1>{status}</h1>
<script>
export let status;
export let error;
</script>
<h1>{status}</h1>
<p>{error.message}</p>

View File

@@ -1,7 +1,7 @@
import polka from 'polka';
import * as sapper from '@sapper/server';
const { PORT } = process.env;
import { start } from '../../common.js';
const app = polka().use(sapper.middleware({
ignore: [
@@ -16,4 +16,4 @@ const app = polka().use(sapper.middleware({
app.get('/'+uri, (req, res) => res.end(uri));
});
app.listen(PORT);
start(app);

View File

@@ -1,58 +1,58 @@
import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api';
import { AppRunner } from '../AppRunner';
describe('ignore', function() {
this.timeout(10000);
let runner: AppRunner;
let page: puppeteer.Page;
let base: string;
let r: AppRunner;
// hooks
before(async () => {
await build({ cwd: __dirname });
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page } = await runner.start());
before('build app', () => build({ cwd: __dirname }));
before('start runner', async () => {
r = await new AppRunner().start(__dirname);
});
after(() => runner.end());
after(() => r && r.end());
// tests
it('respects `options.ignore` values (RegExp)', async () => {
await page.goto(`${base}/foobar`);
await r.load('/foobar');
assert.equal(
await page.evaluate(() => document.documentElement.textContent),
await r.text('body'),
'foobar'
);
});
it('respects `options.ignore` values (String #1)', async () => {
await page.goto(`${base}/buzz`);
await r.load('/buzz');
assert.equal(
await page.evaluate(() => document.documentElement.textContent),
await r.text('body'),
'buzz'
);
});
it('respects `options.ignore` values (String #2)', async () => {
await page.goto(`${base}/fizzer`);
await r.load('/fizzer');
assert.equal(
await page.evaluate(() => document.documentElement.textContent),
await r.text('body'),
'fizzer'
);
});
it('respects `options.ignore` values (Function)', async () => {
await page.goto(`${base}/hello`);
await r.load('/hello');
assert.equal(
await page.evaluate(() => document.documentElement.textContent),
await r.text('body'),
'hello'
);
});
});
it('survives the tests with no server errors', () => {
assert.deepEqual(r.errors, []);
});
});

View File

@@ -1,8 +1,9 @@
import polka from 'polka';
import * as sapper from '@sapper/server';
const { PORT } = process.env;
import { start } from '../../common.js';
polka()
.use(sapper.middleware())
.listen(PORT);
const app = polka()
.use(sapper.middleware());
start(app);

View File

@@ -1,33 +1,25 @@
import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api';
import { AppRunner } from '../AppRunner';
import { wait } from '../../utils';
describe('layout', function() {
this.timeout(10000);
let runner: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
let r: AppRunner;
// hooks
before(async () => {
await build({ cwd: __dirname });
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start } = await runner.start());
before('build app', () => build({ cwd: __dirname }));
before('start runner', async () => {
r = await new AppRunner().start(__dirname);
});
after(() => runner.end());
after(() => r && r.end());
// tests
it('only recreates components when necessary', async () => {
await page.goto(`${base}/foo/bar/baz`);
await r.load('/foo/bar/baz');
const text1 = String(await page.evaluate(() => document.querySelector('#sapper').textContent));
const text1 = await r.text('#sapper');
assert.deepEqual(text1.split('\n').map(str => str.trim()).filter(Boolean), [
'y: bar 1',
'z: baz 1',
@@ -35,8 +27,8 @@ describe('layout', function() {
'child segment: baz'
]);
await start();
const text2 = String(await page.evaluate(() => document.querySelector('#sapper').textContent));
await r.sapper.start();
const text2 = await r.text('#sapper');
assert.deepEqual(text2.split('\n').map(str => str.trim()).filter(Boolean), [
'y: bar 1',
'z: baz 1',
@@ -44,10 +36,10 @@ describe('layout', function() {
'child segment: baz'
]);
await page.click('[href="foo/bar/qux"]');
await wait(50);
await r.page.click('[href="foo/bar/qux"]');
await r.wait();
const text3 = String(await page.evaluate(() => document.querySelector('#sapper').textContent));
const text3 = await r.text('#sapper');
assert.deepEqual(text3.split('\n').map(str => str.trim()).filter(Boolean), [
'y: bar 1',
'z: qux 2',
@@ -55,4 +47,8 @@ describe('layout', function() {
'child segment: qux'
]);
});
});
it('survives the tests with no server errors', () => {
assert.deepEqual(r.errors, []);
});
});

View File

@@ -1,8 +1,9 @@
import polka from 'polka';
import * as sapper from '@sapper/server';
const { PORT } = process.env;
import { start } from '../../common.js';
polka()
.use(sapper.middleware())
.listen(PORT);
const app = polka()
.use(sapper.middleware());
start(app);

View File

@@ -1,7 +1,5 @@
import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api';
import { wait } from '../../utils';
import { AppRunner } from '../AppRunner';
declare const fulfil: () => Promise<void>;
@@ -9,112 +7,106 @@ declare const fulfil: () => Promise<void>;
describe('preloading', function() {
this.timeout(10000);
let runner: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
let prefetchRoutes: () => Promise<void>;
let title: () => Promise<string>;
let r: AppRunner;
// hooks
before(async () => {
await build({ cwd: __dirname });
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes, title } = await runner.start());
before('build app', () => build({ cwd: __dirname }));
before('start runner', async () => {
r = await new AppRunner().start(__dirname);
});
after(() => runner.end());
after(() => r && r.end());
// tests
it('serializes Set objects returned from preload', async () => {
await page.goto(`${base}/preload-values/set`);
await r.load('/preload-values/set');
assert.equal(await title(), 'true');
assert.equal(await r.text('h1'), 'true');
await start();
assert.equal(await title(), 'true');
await r.sapper.start();
assert.equal(await r.text('h1'), 'true');
});
it('prevent crash if preload return nothing', async () => {
await page.goto(`${base}/preload-nothing`);
await r.load('/preload-nothing');
await start();
await wait(50);
await r.sapper.start();
assert.equal(await title(), 'Page loaded');
assert.equal(await r.text('h1'), 'Page loaded');
});
it('bails on custom classes returned from preload', async () => {
await page.goto(`${base}/preload-values/custom-class`);
await r.load('/preload-values/custom-class');
assert.equal(await title(), '42');
assert.equal(await r.text('h1'), '42');
await start();
assert.equal(await title(), '42');
await r.sapper.start();
assert.equal(await r.text('h1'), '42');
});
it('sets preloading true when appropriate', async () => {
await page.goto(base);
await start();
await prefetchRoutes();
await r.load('/');
await r.sapper.start();
await r.sapper.prefetchRoutes();
await page.click('a[href="slow-preload"]');
await r.page.click('a[href="slow-preload"]');
assert.ok(await page.evaluate(() => !!document.querySelector('progress')));
assert.ok(await r.page.evaluate(() => !!document.querySelector('progress')));
await page.evaluate(() => fulfil());
assert.ok(await page.evaluate(() => !document.querySelector('progress')));
await r.page.evaluate(() => fulfil());
assert.ok(await r.page.evaluate(() => !document.querySelector('progress')));
});
it('runs preload in root component', async () => {
await page.goto(`${base}/preload-root`);
assert.equal(await title(), 'root preload function ran: true');
await r.load('/preload-root');
assert.equal(await r.text('h1'), 'root preload function ran: true');
});
it('cancels navigation if subsequent navigation occurs during preload', async () => {
await page.goto(base);
await start();
await prefetchRoutes();
await r.load('/');
await r.sapper.start();
await r.sapper.prefetchRoutes();
await page.click('a[href="slow-preload"]');
await wait(100);
await page.click('a[href="foo"]');
await r.page.click('a[href="slow-preload"]');
await r.wait();
await r.page.click('a[href="foo"]');
assert.equal(page.url(), `${base}/foo`);
assert.equal(await title(), 'foo');
assert.equal(r.page.url(), `${r.base}/foo`);
assert.equal(await r.text('h1'), 'foo');
await page.evaluate(() => fulfil());
await wait(100);
assert.equal(page.url(), `${base}/foo`);
assert.equal(await title(), 'foo');
await r.page.evaluate(() => fulfil());
await r.wait();
assert.equal(r.page.url(), `${r.base}/foo`);
assert.equal(await r.text('h1'), 'foo');
});
it('navigates to prefetched urls', async () => {
await page.goto(base);
await start();
await prefetchRoutes();
await r.load('/');
await r.sapper.start();
await r.sapper.prefetchRoutes();
await page.hover('a[href="prefetch/qwe"]');
await wait(100);
await page.hover('a[href="prefetch/xyz"]');
await wait(100);
await r.page.hover('a[href="prefetch/qwe"]');
await r.wait(50);
await r.page.hover('a[href="prefetch/xyz"]');
await r.wait(50);
await page.click('a[href="prefetch/qwe"]');
await wait(50);
await r.page.click('a[href="prefetch/qwe"]');
await r.wait();
assert.equal(
await title(),
await r.text('h1'),
'qwe'
);
await page.goto(`${base}/prefetch`);
await wait(50);
await r.load('/prefetch');
assert.equal(
await title(),
await r.text('h1'),
'prefetch'
);
});
it('survives the tests with no server errors', () => {
assert.deepEqual(r.errors, []);
});
});

View File

@@ -1,8 +1,9 @@
import polka from 'polka';
import * as sapper from '@sapper/server';
const { PORT } = process.env;
import { start } from '../../common.js';
polka()
.use(sapper.middleware())
.listen(PORT);
const app = polka()
.use(sapper.middleware());
start(app);

View File

@@ -2,138 +2,137 @@ import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api';
import { AppRunner } from '../AppRunner';
import { wait } from '../../utils';
describe('redirects', function() {
this.timeout(10000);
let runner: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
let prefetchRoutes: () => Promise<void>;
let title: () => Promise<string>;
let r: AppRunner;
// hooks
before(async () => {
await build({ cwd: __dirname });
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes, title } = await runner.start({
requestInterceptor: (interceptedRequest) => {
if (/example\.com/.test(interceptedRequest.url())) {
interceptedRequest.respond({
status: 200,
contentType: 'text/html',
body: `<h1>external</h1>`
});
} else {
interceptedRequest.continue();
}
}
}));
before('build app', () => build({ cwd: __dirname }));
before('start runner', async () => {
r = await new AppRunner().start(__dirname);
});
after(() => runner.end());
after(() => r && r.end());
// tests
it('redirects on server', async () => {
await page.goto(`${base}/redirect-from`);
await r.load('/redirect-from');
assert.equal(
page.url(),
`${base}/redirect-to`
r.page.url(),
`${r.base}/redirect-to`
);
assert.equal(
await title(),
await r.text('h1'),
'redirected'
);
});
it('redirects in client', async () => {
await page.goto(base);
await start();
await prefetchRoutes();
await r.load('/');
await r.sapper.start();
await r.sapper.prefetchRoutes();
await page.click('[href="redirect-from"]');
await wait(50);
await r.page.click('[href="redirect-from"]');
await r.wait();
assert.equal(
page.url(),
`${base}/redirect-to`
r.page.url(),
`${r.base}/redirect-to`
);
assert.equal(
await title(),
await r.text('h1'),
'redirected'
);
});
it('redirects to root on server', async () => {
await page.goto(`${base}/redirect-to-root`);
await r.load('/redirect-to-root');
assert.equal(
page.url(),
`${base}/`
r.page.url(),
`${r.base}/`
);
assert.equal(
await title(),
await r.text('h1'),
'root'
);
});
it('redirects to root in client', async () => {
await page.goto(base);
await start();
await prefetchRoutes();
await r.load('/');
await r.sapper.start();
await r.sapper.prefetchRoutes();
await page.click('[href="redirect-to-root"]');
await wait(50);
await r.page.click('[href="redirect-to-root"]');
await r.wait();
assert.equal(
page.url(),
`${base}/`
r.page.url(),
`${r.base}/`
);
assert.equal(
await title(),
await r.text('h1'),
'root'
);
});
const interceptor = (request: puppeteer.Request) => {
if (/example\.com/.test(request.url())) {
request.respond({
status: 200,
contentType: 'text/html',
body: `<h1>external</h1>`
});
} else {
request.continue();
}
};
it('redirects to external URL on server', async () => {
await page.goto(`${base}/redirect-to-external`);
await r.intercept_requests(interceptor, async () => {
await r.load('/redirect-to-external');
});
assert.equal(
page.url(),
r.page.url(),
`https://example.com/`
);
assert.equal(
await title(),
await r.text('h1'),
'external'
);
});
it('redirects to external URL in client', async () => {
await page.goto(base);
await start();
await prefetchRoutes();
await r.load('/');
await r.sapper.start();
await r.sapper.prefetchRoutes();
await page.click('[href="redirect-to-external"]');
await wait(50);
await r.intercept_requests(interceptor, async () => {
await r.page.click('[href="redirect-to-external"]');
await r.wait();
});
assert.equal(
page.url(),
r.page.url(),
`https://example.com/`
);
assert.equal(
await title(),
await r.text('h1'),
'external'
);
});
});
it('survives the tests with no server errors', () => {
assert.deepEqual(r.errors, []);
});
});

View File

@@ -1,8 +1,9 @@
import polka from 'polka';
import * as sapper from '@sapper/server';
const { PORT } = process.env;
import { start } from '../../common.js';
polka()
.use(sapper.middleware())
.listen(PORT);
const app = polka()
.use(sapper.middleware());
start(app);

View File

@@ -1,93 +1,87 @@
import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api';
import { AppRunner } from '../AppRunner';
import { wait } from '../../utils';
describe('scroll', function() {
this.timeout(10000);
let runner: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
let prefetchRoutes: () => Promise<void>;
let title: () => Promise<string>;
let r: AppRunner;
// hooks
before(async () => {
await build({ cwd: __dirname });
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes, title } = await runner.start());
before('build app', () => build({ cwd: __dirname }));
before('start runner', async () => {
r = await new AppRunner().start(__dirname);
});
after(() => runner.end());
after(() => r && r.end());
// tests
it('scrolls to active deeplink', async () => {
await page.goto(`${base}/tall-page#foo`);
await start();
await r.load('/tall-page#foo');
await r.sapper.start();
const scrollY = await page.evaluate(() => window.scrollY);
const scrollY = await r.page.evaluate(() => window.scrollY);
assert.ok(scrollY > 0, String(scrollY));
});
it('scrolls to any deeplink if it was already active', async () => {
await page.goto(`${base}/tall-page#foo`);
await start();
await r.load('/tall-page#foo');
await r.sapper.start();
let scrollY = await page.evaluate(() => window.scrollY);
let scrollY = await r.page.evaluate(() => window.scrollY);
assert.ok(scrollY > 0, String(scrollY));
scrollY = await page.evaluate(() => {
scrollY = await r.page.evaluate(() => {
window.scrollTo(0, 0)
return window.scrollY
});
assert.ok(scrollY === 0, String(scrollY));
await page.click('[href="tall-page#foo"]');
scrollY = await page.evaluate(() => window.scrollY);
await r.page.click('[href="tall-page#foo"]');
scrollY = await r.page.evaluate(() => window.scrollY);
assert.ok(scrollY > 0, String(scrollY));
});
it('resets scroll when a link is clicked', async () => {
await page.goto(`${base}/tall-page#foo`);
await start();
await prefetchRoutes();
await r.load('/tall-page#foo');
await r.sapper.start();
await r.sapper.prefetchRoutes();
await page.click('[href="another-tall-page"]');
await wait(50);
await r.page.click('[href="another-tall-page"]');
await r.wait();
assert.equal(
await page.evaluate(() => window.scrollY),
await r.page.evaluate(() => window.scrollY),
0
);
});
it('preserves scroll when a link with sapper-noscroll is clicked', async () => {
await page.goto(`${base}/tall-page#foo`);
await start();
await prefetchRoutes();
await r.load('/tall-page#foo');
await r.sapper.start();
await r.sapper.prefetchRoutes();
await page.click('[href="another-tall-page"][sapper-noscroll]');
await wait(50);
await r.page.click('[href="another-tall-page"][sapper-noscroll]');
await r.wait();
const scrollY = await page.evaluate(() => window.scrollY);
const scrollY = await r.page.evaluate(() => window.scrollY);
assert.ok(scrollY > 0);
});
it('scrolls into a deeplink on a new page', async () => {
await page.goto(`${base}/tall-page#foo`);
await start();
await prefetchRoutes();
await r.load('/tall-page#foo');
await r.sapper.start();
await r.sapper.prefetchRoutes();
await page.click('[href="another-tall-page#bar"]');
await wait(50);
assert.equal(await title(), 'Another tall page');
const scrollY = await page.evaluate(() => window.scrollY);
await r.page.click('[href="another-tall-page#bar"]');
await r.wait();
assert.equal(await r.text('h1'), 'Another tall page');
const scrollY = await r.page.evaluate(() => window.scrollY);
assert.ok(scrollY > 0);
});
});
it('survives the tests with no server errors', () => {
assert.deepEqual(r.errors, []);
});
});

View File

@@ -1,9 +1,9 @@
import polka from 'polka';
import * as sapper from '@sapper/server';
const { PORT } = process.env;
import { start } from '../../common.js';
polka()
const app = polka()
.use((req, res, next) => {
req.hello = 'hello';
res.locals = { name: 'world' };
@@ -15,5 +15,6 @@ polka()
title: `${req.hello} ${res.locals.name}`
})
})
)
.listen(PORT);
);
start(app);

View File

@@ -1,50 +1,46 @@
import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api';
import { AppRunner } from '../AppRunner';
describe('session', function() {
this.timeout(10000);
let runner: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
let title: () => Promise<string>;
let r: AppRunner;
// hooks
before(async () => {
await build({ cwd: __dirname });
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, title } = await runner.start());
before('build app', () => build({ cwd: __dirname }));
before('start runner', async () => {
r = await new AppRunner().start(__dirname);
});
after(() => runner.end());
after(() => r && r.end());
// tests
it('renders session props', async () => {
await page.goto(`${base}/session`);
await r.load('/session');
assert.equal(await title(), 'hello world');
assert.equal(await r.text('h1'), 'hello world');
await start();
assert.equal(await title(), 'hello world');
await r.sapper.start();
assert.equal(await r.text('h1'), 'hello world');
await page.click('button');
assert.equal(await title(), 'changed');
await r.page.click('button');
assert.equal(await r.text('h1'), 'changed');
});
it('preloads session props', async () => {
await page.goto(`${base}/preloaded`);
await r.load('/preloaded');
assert.equal(await title(), 'hello world');
assert.equal(await r.text('h1'), 'hello world');
await start();
assert.equal(await title(), 'hello world');
await r.sapper.start();
assert.equal(await r.text('h1'), 'hello world');
await page.click('button');
assert.equal(await title(), 'changed');
await r.page.click('button');
assert.equal(await r.text('h1'), 'changed');
});
});
it('survives the tests with no server errors', () => {
assert.deepEqual(r.errors, []);
});
});

View File

@@ -2,15 +2,14 @@ import sirv from 'sirv';
import polka from 'polka';
import * as sapper from '@sapper/server';
const { PORT, NODE_ENV } = process.env;
const dev = NODE_ENV === 'development';
import { start, dev } from '../../common.js';
polka()
const app = polka()
.use(
'custom-basepath',
sirv('static', { dev }),
sapper.middleware()
)
.listen(PORT, err => {
if (err) console.log('error', err);
});
);
start(app);

View File

@@ -1,51 +1,36 @@
import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import * as api from '../../../api';
import { walk } from '../../utils';
import { AppRunner } from '../AppRunner';
import { wait } from '../../utils';
describe('with-basepath', function() {
this.timeout(10000);
let runner: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
let prefetchRoutes: () => Promise<void>;
let title: () => Promise<string>;
let r: AppRunner;
// hooks
before(async () => {
await api.build({ cwd: __dirname });
await api.export({
cwd: __dirname,
basepath: '/custom-basepath'
});
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, start, page, prefetchRoutes, title } = await runner.start());
before('build app', () => api.build({ cwd: __dirname }));
before('export app', () => api.export({ cwd: __dirname, basepath: '/custom-basepath' }));
before('start runner', async () => {
r = await new AppRunner().start(__dirname);
});
after(() => runner.end());
after(() => r && r.end());
// tests
it('serves /custom-basepath', async () => {
await page.goto(`${base}/custom-basepath`);
await r.load('/custom-basepath');
assert.equal(
await page.$eval('h1', node => node.textContent),
await r.text('h1'),
'Great success!'
);
});
it('emits a basepath message', async () => {
await page.goto(`${base}/custom-basepath`);
await r.load('/custom-basepath');
assert.deepEqual(runner.messages, [{
assert.deepEqual(r.messages, [{
__sapper__: true,
event: 'basepath',
basepath: '/custom-basepath'
@@ -71,35 +56,39 @@ describe('with-basepath', function() {
});
it('redirects on server', async () => {
await page.goto(`${base}/custom-basepath/redirect-from`);
await r.load('/custom-basepath/redirect-from');
assert.equal(
page.url(),
`${base}/custom-basepath/redirect-to`
r.page.url(),
`${r.base}/custom-basepath/redirect-to`
);
assert.equal(
await title(),
await r.text('h1'),
'redirected'
);
});
it('redirects in client', async () => {
await page.goto(`${base}/custom-basepath`);
await start();
await prefetchRoutes();
await r.load('/custom-basepath');
await r.sapper.start();
await r.sapper.prefetchRoutes();
await page.click('[href="redirect-from"]');
await wait(50);
await r.page.click('[href="redirect-from"]');
await r.wait();
assert.equal(
page.url(),
`${base}/custom-basepath/redirect-to`
r.page.url(),
`${r.base}/custom-basepath/redirect-to`
);
assert.equal(
await title(),
await r.text('h1'),
'redirected'
);
});
});
it('survives the tests with no server errors', () => {
assert.deepEqual(r.errors, []);
});
});

View File

@@ -1,8 +1,9 @@
import polka from 'polka';
import * as sapper from '@sapper/server';
const { PORT } = process.env;
import { start } from '../../common.js';
polka()
.use(sapper.middleware())
.listen(PORT);
const app = polka()
.use(sapper.middleware());
start(app);

View File

@@ -7,10 +7,9 @@ describe('with-sourcemaps-webpack', function() {
this.timeout(10000);
// hooks
before(async () => {
await build({ cwd: __dirname, bundler: 'webpack' });
});
before('build app', () => build({ cwd: __dirname, bundler: 'webpack' }));
// tests
it('does not put sourcemap files in service worker shell', async () => {
const service_worker_source = fs.readFileSync(`${__dirname}/src/node_modules/@sapper/service-worker.js`, 'utf-8');
const shell_source = /shell = (\[[\s\S]+?\])/.exec(service_worker_source)[1];
@@ -23,4 +22,4 @@ describe('with-sourcemaps-webpack', function() {
const sourcemapFiles = fs.readdirSync(clientShellDir).filter(_ => _.endsWith('.map'));
assert.ok(sourcemapFiles.length > 0, 'sourcemap files exist');
});
});
});

View File

@@ -1,8 +1,9 @@
import polka from 'polka';
import * as sapper from '@sapper/server';
const { PORT } = process.env;
import { start } from '../../common.js';
polka()
.use(sapper.middleware())
.listen(PORT);
const app = polka()
.use(sapper.middleware());
start(app);

View File

@@ -7,10 +7,9 @@ describe('with-sourcemaps', function() {
this.timeout(10000);
// hooks
before(async () => {
await build({ cwd: __dirname });
});
before('build app', () => build({ cwd: __dirname }));
// tests
it('does not put sourcemap files in service worker shell', async () => {
const service_worker_source = fs.readFileSync(`${__dirname}/src/node_modules/@sapper/service-worker.js`, 'utf-8');
const shell_source = /shell = (\[[\s\S]+?\])/.exec(service_worker_source)[1];
@@ -23,4 +22,4 @@ describe('with-sourcemaps', function() {
const sourcemapFiles = fs.readdirSync(clientShellDir).filter(_ => _.endsWith('.map'));
assert.ok(sourcemapFiles.length > 0, 'sourcemap files exist');
});
});
});

View File

@@ -20,7 +20,7 @@ describe('manifest_data', () => {
assert.deepEqual(pages, [
{
pattern: /^\/?$/,
pattern: /^\/$/,
parts: [
{ component: index, params: [] }
]
@@ -50,6 +50,13 @@ describe('manifest_data', () => {
]);
assert.deepEqual(server_routes, [
{
name: 'route_index',
pattern: /^\/$/,
file: 'index.js',
params: []
},
{
name: 'route_blog_json',
pattern: /^\/blog.json$/,
@@ -167,7 +174,7 @@ describe('manifest_data', () => {
file: 'foo.js',
name: 'route_foo',
params: [],
pattern: /^\/foo$/
pattern: /^\/foo\/?$/
}]);
});
});