mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-15 20:34:44 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03c5f5b446 | ||
|
|
6655b1b49d | ||
|
|
eebf076f23 | ||
|
|
198be28f4b | ||
|
|
4f720446b2 | ||
|
|
e69cb3639a | ||
|
|
1b1a86764f | ||
|
|
f50f83c4a4 | ||
|
|
eadefd996b | ||
|
|
ab52aabd1d | ||
|
|
c5a80543b3 | ||
|
|
cfd95ac024 | ||
|
|
73ff95c677 | ||
|
|
382fe6b7b9 | ||
|
|
3b714c0de3 | ||
|
|
28186227a9 | ||
|
|
2ac0f2bf3d | ||
|
|
4991f3b359 | ||
|
|
65128118c7 | ||
|
|
3eced6fa4d | ||
|
|
c4aee66c32 | ||
|
|
410c52df41 | ||
|
|
3fe7b55955 | ||
|
|
464924ed67 |
15
CHANGELOG.md
15
CHANGELOG.md
@@ -1,5 +1,20 @@
|
|||||||
# sapper changelog
|
# sapper changelog
|
||||||
|
|
||||||
|
## 0.24.0
|
||||||
|
|
||||||
|
* Handle external URLs in `this.redirect` ([#490](https://github.com/sveltejs/sapper/issues/490))
|
||||||
|
* Strip leading `/` from basepath ([#495](https://github.com/sveltejs/sapper/issues/495))
|
||||||
|
* Treat duplicate query string parameters as arrays ([#497](https://github.com/sveltejs/sapper/issues/497))
|
||||||
|
* Don't buffer `stdout` and `stderr` ([#305](https://github.com/sveltejs/sapper/issues/305))
|
||||||
|
* Posixify `build_dir` ([#498](https://github.com/sveltejs/sapper/pull/498))
|
||||||
|
* Use `page[XY]Offset` instead of `scroll[XY]` ([#480](https://github.com/sveltejs/sapper/issues/480))
|
||||||
|
|
||||||
|
## 0.23.5
|
||||||
|
|
||||||
|
* Include lazily-imported CSS in main CSS chunk ([#492](https://github.com/sveltejs/sapper/pull/492))
|
||||||
|
* Make search param decoding spec-compliant ([#493](https://github.com/sveltejs/sapper/pull/493))
|
||||||
|
* Handle async route errors ([#488](https://github.com/sveltejs/sapper/pull/488))
|
||||||
|
|
||||||
## 0.23.4
|
## 0.23.4
|
||||||
|
|
||||||
* Ignore empty anchors when exporting ([#491](https://github.com/sveltejs/sapper/pull/491))
|
* Ignore empty anchors when exporting ([#491](https://github.com/sveltejs/sapper/pull/491))
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sapper",
|
"name": "sapper",
|
||||||
"version": "0.23.4",
|
"version": "0.24.0",
|
||||||
"description": "Military-grade apps, engineered by Svelte",
|
"description": "Military-grade apps, engineered by Svelte",
|
||||||
"bin": {
|
"bin": {
|
||||||
"sapper": "./sapper"
|
"sapper": "./sapper"
|
||||||
|
|||||||
@@ -214,12 +214,9 @@ class Watcher extends EventEmitter {
|
|||||||
// TODO watch the configs themselves?
|
// TODO watch the configs themselves?
|
||||||
const compilers: Compilers = await create_compilers(this.bundler, cwd, src, dest, false);
|
const compilers: Compilers = await create_compilers(this.bundler, cwd, src, dest, false);
|
||||||
|
|
||||||
let log = '';
|
|
||||||
|
|
||||||
const emitFatal = () => {
|
const emitFatal = () => {
|
||||||
this.emit('fatal', <FatalEvent>{
|
this.emit('fatal', <FatalEvent>{
|
||||||
message: `Server crashed`,
|
message: `Server crashed`
|
||||||
log
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.crashed = true;
|
this.crashed = true;
|
||||||
@@ -236,7 +233,6 @@ class Watcher extends EventEmitter {
|
|||||||
handle_result: (result: CompileResult) => {
|
handle_result: (result: CompileResult) => {
|
||||||
deferred.promise.then(() => {
|
deferred.promise.then(() => {
|
||||||
const restart = () => {
|
const restart = () => {
|
||||||
log = '';
|
|
||||||
this.crashed = false;
|
this.crashed = false;
|
||||||
|
|
||||||
ports.wait(this.port)
|
ports.wait(this.port)
|
||||||
@@ -260,8 +256,7 @@ class Watcher extends EventEmitter {
|
|||||||
if (this.crashed) return;
|
if (this.crashed) return;
|
||||||
|
|
||||||
this.emit('fatal', <FatalEvent>{
|
this.emit('fatal', <FatalEvent>{
|
||||||
message: `Server is not listening on port ${this.port}`,
|
message: `Server is not listening on port ${this.port}`
|
||||||
log
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -292,12 +287,10 @@ class Watcher extends EventEmitter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.proc.stdout.on('data', chunk => {
|
this.proc.stdout.on('data', chunk => {
|
||||||
log += chunk;
|
|
||||||
this.emit('stdout', chunk);
|
this.emit('stdout', chunk);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.proc.stderr.on('data', chunk => {
|
this.proc.stderr.on('data', chunk => {
|
||||||
log += chunk;
|
|
||||||
this.emit('stderr', chunk);
|
this.emit('stderr', chunk);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ async function _export({
|
|||||||
oninfo = noop,
|
oninfo = noop,
|
||||||
onfile = noop
|
onfile = noop
|
||||||
}: Opts = {}) {
|
}: Opts = {}) {
|
||||||
|
basepath = basepath.replace(/^\//, '')
|
||||||
|
|
||||||
cwd = path.resolve(cwd);
|
cwd = path.resolve(cwd);
|
||||||
static_files = path.resolve(cwd, static_files);
|
static_files = path.resolve(cwd, static_files);
|
||||||
build_dir = path.resolve(cwd, build_dir);
|
build_dir = path.resolve(cwd, build_dir);
|
||||||
|
|||||||
18
src/cli.ts
18
src/cli.ts
@@ -64,6 +64,14 @@ prog.command('dev')
|
|||||||
|
|
||||||
let first = true;
|
let first = true;
|
||||||
|
|
||||||
|
watcher.on('stdout', data => {
|
||||||
|
process.stdout.write(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher.on('stderr', data => {
|
||||||
|
process.stderr.write(data);
|
||||||
|
});
|
||||||
|
|
||||||
watcher.on('ready', async (event: ReadyEvent) => {
|
watcher.on('ready', async (event: ReadyEvent) => {
|
||||||
if (first) {
|
if (first) {
|
||||||
console.log(colors.bold.cyan(`> Listening on http://localhost:${event.port}`));
|
console.log(colors.bold.cyan(`> Listening on http://localhost:${event.port}`));
|
||||||
@@ -73,16 +81,6 @@ prog.command('dev')
|
|||||||
}
|
}
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO clear screen?
|
|
||||||
|
|
||||||
event.process.stdout.on('data', data => {
|
|
||||||
process.stdout.write(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
event.process.stderr.on('data', data => {
|
|
||||||
process.stderr.write(data);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
watcher.on('invalid', (event: InvalidEvent) => {
|
watcher.on('invalid', (event: InvalidEvent) => {
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ export default function extract_css(client_result: CompileResult, components: Pa
|
|||||||
});
|
});
|
||||||
|
|
||||||
unclaimed.forEach(file => {
|
unclaimed.forEach(file => {
|
||||||
entry_css_modules.push(css_map.get(file));
|
entry_css_modules.push(file);
|
||||||
});
|
});
|
||||||
|
|
||||||
const leftover = get_css_from_modules(entry_css_modules, css_map, dirs);
|
const leftover = get_css_from_modules(entry_css_modules, css_map, dirs);
|
||||||
|
|||||||
@@ -226,8 +226,8 @@ function generate_server(
|
|||||||
error
|
error
|
||||||
};`.replace(/^\t\t/gm, '').trim();
|
};`.replace(/^\t\t/gm, '').trim();
|
||||||
|
|
||||||
const build_dir = path.relative(cwd, dest);
|
const build_dir = posixify(path.relative(cwd, dest));
|
||||||
const src_dir = path.relative(cwd, src);
|
const src_dir = posixify(path.relative(cwd, src));
|
||||||
|
|
||||||
return `// This file is generated by Sapper — do not edit it!\n` + template
|
return `// This file is generated by Sapper — do not edit it!\n` + template
|
||||||
.replace('__BUILD__DIR__', JSON.stringify(build_dir))
|
.replace('__BUILD__DIR__', JSON.stringify(build_dir))
|
||||||
@@ -242,4 +242,4 @@ function get_file(path_to_routes: string, component: PageComponent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return posixify(`${path_to_routes}/${component.file}`);
|
return posixify(`${path_to_routes}/${component.file}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ export type ErrorEvent = {
|
|||||||
|
|
||||||
export type FatalEvent = {
|
export type FatalEvent = {
|
||||||
message: string;
|
message: string;
|
||||||
log?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InvalidEvent = {
|
export type InvalidEvent = {
|
||||||
|
|||||||
@@ -87,11 +87,14 @@ export function select_route(url: URL): Target {
|
|||||||
|
|
||||||
const match = page.pattern.exec(path);
|
const match = page.pattern.exec(path);
|
||||||
if (match) {
|
if (match) {
|
||||||
const query: Record<string, string | true> = {};
|
const query: Record<string, string | string[]> = Object.create(null);
|
||||||
if (url.search.length > 0) {
|
if (url.search.length > 0) {
|
||||||
url.search.slice(1).split('&').forEach(searchParam => {
|
url.search.slice(1).split('&').forEach(searchParam => {
|
||||||
const [, key, value] = /([^=]+)(?:=(.*))?/.exec(searchParam);
|
let [, key, value] = /([^=]*)(?:=(.*))?/.exec(decodeURIComponent(searchParam));
|
||||||
query[key] = decodeURIComponent((value || '').replace(/\+/g, ' '));
|
value = (value || '').replace(/\+/g, ' ');
|
||||||
|
if (typeof query[key] === 'string') query[key] = [<string>query[key]];
|
||||||
|
if (typeof query[key] === 'object') query[key].push(value);
|
||||||
|
else query[key] = value;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return { url, path, page, match, query };
|
return { url, path, page, match, query };
|
||||||
@@ -101,8 +104,8 @@ export function select_route(url: URL): Target {
|
|||||||
|
|
||||||
export function scroll_state() {
|
export function scroll_state() {
|
||||||
return {
|
return {
|
||||||
x: scrollX,
|
x: pageXOffset,
|
||||||
y: scrollY
|
y: pageYOffset
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export type Target = {
|
|||||||
path: string;
|
path: string;
|
||||||
page: Page;
|
page: Page;
|
||||||
match: RegExpExecArray;
|
match: RegExpExecArray;
|
||||||
query: Record<string, string | true>;
|
query: Record<string, string | string[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Redirect = {
|
export type Redirect = {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import * as path from 'path';
|
|||||||
import cookie from 'cookie';
|
import cookie from 'cookie';
|
||||||
import devalue from 'devalue';
|
import devalue from 'devalue';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import { URL } from 'url';
|
import { URL, resolve } from 'url';
|
||||||
import { build_dir, dev, src_dir, IGNORE } from '../placeholders';
|
import { build_dir, dev, src_dir, IGNORE } from '../placeholders';
|
||||||
import { Manifest, Page, Props, Req, Res, Store } from './types';
|
import { Manifest, Page, Props, Req, Res, Store } from './types';
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ export function get_page_handler(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
const location = `${req.baseUrl}/${redirect.location}`;
|
const location = resolve(req.baseUrl || '/', redirect.location);
|
||||||
|
|
||||||
res.statusCode = redirect.statusCode;
|
res.statusCode = redirect.statusCode;
|
||||||
res.setHeader('Location', location);
|
res.setHeader('Location', location);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { IGNORE } from '../placeholders';
|
|||||||
import { Req, Res, ServerRoute } from './types';
|
import { Req, Res, ServerRoute } from './types';
|
||||||
|
|
||||||
export function get_server_route_handler(routes: ServerRoute[]) {
|
export function get_server_route_handler(routes: ServerRoute[]) {
|
||||||
function handle_route(route: ServerRoute, req: Req, res: Res, next: () => void) {
|
async function handle_route(route: ServerRoute, req: Req, res: Res, next: () => void) {
|
||||||
req.params = route.params(route.pattern.exec(req.path));
|
req.params = route.params(route.pattern.exec(req.path));
|
||||||
|
|
||||||
const method = req.method.toLowerCase();
|
const method = req.method.toLowerCase();
|
||||||
@@ -53,7 +53,7 @@ export function get_server_route_handler(routes: ServerRoute[]) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
handle_method(req, res, handle_next);
|
await handle_method(req, res, handle_next);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handle_next(err);
|
handle_next(err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,20 @@ export class AppRunner {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.page.setRequestInterception(true);
|
||||||
|
|
||||||
|
this.page.on('request', interceptedRequest => {
|
||||||
|
if (/example\.com/.test(interceptedRequest.url())) {
|
||||||
|
interceptedRequest.respond({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'text/html',
|
||||||
|
body: `<h1>external</h1>`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
interceptedRequest.continue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
page: this.page,
|
page: this.page,
|
||||||
base: `http://localhost:${this.port}`,
|
base: `http://localhost:${this.port}`,
|
||||||
|
|||||||
@@ -1,9 +1 @@
|
|||||||
<h1>message: "{message}"</h1>
|
<h1>{JSON.stringify(query)}</h1>
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
preload({ query }) {
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
<a href="a">a</a>
|
<a href="a">a</a>
|
||||||
<a href="ambiguous/ok.json">ok</a>
|
<a href="ambiguous/ok.json">ok</a>
|
||||||
<a href="echo-query?message">ok</a>
|
<a href="echo-query?message">ok</a>
|
||||||
|
<a href="echo-query?p=one&p=two">ok</a>
|
||||||
|
|
||||||
<div class='hydrate-test'></div>
|
<div class='hydrate-test'></div>
|
||||||
@@ -204,7 +204,7 @@ describe('basics', function() {
|
|||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await title(),
|
await title(),
|
||||||
'message: ""'
|
'{"message":""}'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -217,7 +217,29 @@ describe('basics', function() {
|
|||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await title(),
|
await title(),
|
||||||
'message: ""'
|
'{"message":""}'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts duplicated query string parameter on server', async () => {
|
||||||
|
await page.goto(`${base}/echo-query?p=one&p=two`);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await title(),
|
||||||
|
'{"p":["one","two"]}'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts duplicated query string parameter on client', async () => {
|
||||||
|
await page.goto(base);
|
||||||
|
await start();
|
||||||
|
await prefetchRoutes();
|
||||||
|
|
||||||
|
await page.click('a[href="echo-query?p=one&p=two"]')
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await title(),
|
||||||
|
'{"p":["one","two"]}'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
64
test/apps/css/rollup.config.js
Normal file
64
test/apps/css/rollup.config.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import resolve from 'rollup-plugin-node-resolve';
|
||||||
|
import replace from 'rollup-plugin-replace';
|
||||||
|
import svelte from 'rollup-plugin-svelte';
|
||||||
|
|
||||||
|
const mode = process.env.NODE_ENV;
|
||||||
|
const dev = mode === 'development';
|
||||||
|
|
||||||
|
const config = require('../../../config/rollup.js');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
client: {
|
||||||
|
input: config.client.input(),
|
||||||
|
output: config.client.output(),
|
||||||
|
plugins: [
|
||||||
|
replace({
|
||||||
|
'process.browser': true,
|
||||||
|
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||||
|
}),
|
||||||
|
svelte({
|
||||||
|
dev,
|
||||||
|
hydratable: true,
|
||||||
|
emitCss: true
|
||||||
|
}),
|
||||||
|
resolve()
|
||||||
|
],
|
||||||
|
|
||||||
|
// temporary, pending Rollup 1.0
|
||||||
|
experimentalCodeSplitting: true
|
||||||
|
},
|
||||||
|
|
||||||
|
server: {
|
||||||
|
input: config.server.input(),
|
||||||
|
output: config.server.output(),
|
||||||
|
plugins: [
|
||||||
|
replace({
|
||||||
|
'process.browser': false,
|
||||||
|
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||||
|
}),
|
||||||
|
svelte({
|
||||||
|
generate: 'ssr',
|
||||||
|
dev
|
||||||
|
}),
|
||||||
|
resolve({
|
||||||
|
preferBuiltins: true
|
||||||
|
})
|
||||||
|
],
|
||||||
|
external: ['sirv', 'polka'],
|
||||||
|
|
||||||
|
// temporary, pending Rollup 1.0
|
||||||
|
experimentalCodeSplitting: true
|
||||||
|
},
|
||||||
|
|
||||||
|
serviceworker: {
|
||||||
|
input: config.serviceworker.input(),
|
||||||
|
output: config.serviceworker.output(),
|
||||||
|
plugins: [
|
||||||
|
resolve(),
|
||||||
|
replace({
|
||||||
|
'process.browser': true,
|
||||||
|
'process.env.NODE_ENV': JSON.stringify(mode)
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
9
test/apps/css/src/client.js
Normal file
9
test/apps/css/src/client.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import * as sapper from '../__sapper__/client.js';
|
||||||
|
|
||||||
|
window.start = () => sapper.start({
|
||||||
|
target: document.querySelector('#sapper')
|
||||||
|
});
|
||||||
|
|
||||||
|
window.prefetchRoutes = () => sapper.prefetchRoutes();
|
||||||
|
window.prefetch = href => sapper.prefetch(href);
|
||||||
|
window.goto = href => sapper.goto(href);
|
||||||
7
test/apps/css/src/routes/_components/Title.html
Normal file
7
test/apps/css/src/routes/_components/Title.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<h1>Title</h1>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
h1 {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
3
test/apps/css/src/routes/_error.html
Normal file
3
test/apps/css/src/routes/_error.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<h1>{status}</h1>
|
||||||
|
|
||||||
|
<p>{error.message}</p>
|
||||||
11
test/apps/css/src/routes/bar.html
Normal file
11
test/apps/css/src/routes/bar.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<svelte:component this={Title}/>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
oncreate() {
|
||||||
|
import('./_components/Title.html').then(({ default: Title }) => {
|
||||||
|
this.set({ Title });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
7
test/apps/css/src/routes/foo.html
Normal file
7
test/apps/css/src/routes/foo.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<h1>Foo</h1>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
h1 {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
10
test/apps/css/src/routes/index.html
Normal file
10
test/apps/css/src/routes/index.html
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<h1>Great success!</h1>
|
||||||
|
|
||||||
|
<a href="foo">foo</a>
|
||||||
|
<a href="bar">bar</a>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
h1 {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
8
test/apps/css/src/server.js
Normal file
8
test/apps/css/src/server.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import polka from 'polka';
|
||||||
|
import * as sapper from '../__sapper__/server.js';
|
||||||
|
|
||||||
|
const { PORT } = process.env;
|
||||||
|
|
||||||
|
polka()
|
||||||
|
.use(sapper.middleware())
|
||||||
|
.listen(PORT);
|
||||||
82
test/apps/css/src/service-worker.js
Normal file
82
test/apps/css/src/service-worker.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { timestamp, files, shell, routes } from '../__sapper__/service-worker.js';
|
||||||
|
|
||||||
|
const ASSETS = `cache${timestamp}`;
|
||||||
|
|
||||||
|
// `shell` is an array of all the files generated by webpack,
|
||||||
|
// `files` is an array of everything in the `static` directory
|
||||||
|
const to_cache = shell.concat(ASSETS);
|
||||||
|
const cached = new Set(to_cache);
|
||||||
|
|
||||||
|
self.addEventListener('install', event => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches
|
||||||
|
.open(ASSETS)
|
||||||
|
.then(cache => cache.addAll(to_cache))
|
||||||
|
.then(() => {
|
||||||
|
self.skipWaiting();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', event => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then(async keys => {
|
||||||
|
// delete old caches
|
||||||
|
for (const key of keys) {
|
||||||
|
if (key !== ASSETS) await caches.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.clients.claim();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', event => {
|
||||||
|
if (event.request.method !== 'GET') return;
|
||||||
|
|
||||||
|
const url = new URL(event.request.url);
|
||||||
|
|
||||||
|
// don't try to handle e.g. data: URIs
|
||||||
|
if (!url.protocol.startsWith('http')) return;
|
||||||
|
|
||||||
|
// ignore dev server requests
|
||||||
|
if (url.hostname === self.location.hostname && url.port !== self.location.port) return;
|
||||||
|
|
||||||
|
// always serve assets and webpack-generated files from cache
|
||||||
|
if (url.host === self.location.host && cached.has(url.pathname)) {
|
||||||
|
event.respondWith(caches.match(event.request));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// for pages, you might want to serve a shell `index.html` file,
|
||||||
|
// which Sapper has generated for you. It's not right for every
|
||||||
|
// app, but if it's right for yours then uncomment this section
|
||||||
|
/*
|
||||||
|
if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) {
|
||||||
|
event.respondWith(caches.match('/index.html'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (event.request.cache === 'only-if-cached') return;
|
||||||
|
|
||||||
|
// for everything else, try the network first, falling back to
|
||||||
|
// cache if the user is offline. (If the pages never change, you
|
||||||
|
// might prefer a cache-first approach to a network-first one.)
|
||||||
|
event.respondWith(
|
||||||
|
caches
|
||||||
|
.open(`offline${timestamp}`)
|
||||||
|
.then(async cache => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(event.request);
|
||||||
|
cache.put(event.request, response.clone());
|
||||||
|
return response;
|
||||||
|
} catch(err) {
|
||||||
|
const response = await cache.match(event.request);
|
||||||
|
if (response) return response;
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
14
test/apps/css/src/template.html
Normal file
14
test/apps/css/src/template.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset='utf-8'>
|
||||||
|
|
||||||
|
%sapper.base%
|
||||||
|
%sapper.styles%
|
||||||
|
%sapper.head%
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id='sapper'>%sapper.html%</div>
|
||||||
|
%sapper.scripts%
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
78
test/apps/css/test.ts
Normal file
78
test/apps/css/test.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
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>;
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => runner.end());
|
||||||
|
|
||||||
|
it('includes critical CSS with server render', async () => {
|
||||||
|
await page.goto(base);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const h1 = document.querySelector('h1');
|
||||||
|
return getComputedStyle(h1).color;
|
||||||
|
}),
|
||||||
|
'rgb(255, 0, 0)'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads CSS when navigating client-side', async () => {
|
||||||
|
await page.goto(base);
|
||||||
|
|
||||||
|
await start();
|
||||||
|
await prefetchRoutes();
|
||||||
|
|
||||||
|
await page.click(`[href="foo"]`);
|
||||||
|
await wait(50);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const h1 = document.querySelector('h1');
|
||||||
|
return getComputedStyle(h1).color;
|
||||||
|
}),
|
||||||
|
'rgb(0, 0, 255)'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads CSS for a lazily-rendered component', async () => {
|
||||||
|
await page.goto(base);
|
||||||
|
|
||||||
|
await start();
|
||||||
|
await prefetchRoutes();
|
||||||
|
|
||||||
|
await page.click(`[href="bar"]`);
|
||||||
|
await wait(50);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const h1 = document.querySelector('h1');
|
||||||
|
return getComputedStyle(h1).color;
|
||||||
|
}),
|
||||||
|
'rgb(0, 128, 0)'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
<h1>{slug} ({message})</h1>
|
<h1>{slug} {JSON.stringify(query)}</h1>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
preload({ params, query }) {
|
preload({ params }) {
|
||||||
return {
|
return {
|
||||||
slug: params.slug,
|
slug: params.slug
|
||||||
message: query.message
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<h1>Great success!</h1>
|
<h1>Great success!</h1>
|
||||||
|
|
||||||
<a href="echo/page/encöded?message=hëllö+wörld">link</a>
|
<a href="echo/page/encöded?message=hëllö+wörld&föo=bar&=baz">link</a>
|
||||||
@@ -35,11 +35,11 @@ describe('encoding', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('encodes req.params and req.query for server-rendered pages', async () => {
|
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`);
|
await page.goto(`${base}/echo/page/encöded?message=hëllö+wörld&föo=bar&=baz`);
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await page.$eval('h1', node => node.textContent),
|
await page.$eval('h1', node => node.textContent),
|
||||||
'encöded (hëllö wörld)'
|
'encöded {"message":"hëllö wörld","föo":"bar","":"baz"}'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,12 +48,12 @@ describe('encoding', function() {
|
|||||||
await start();
|
await start();
|
||||||
await prefetchRoutes();
|
await prefetchRoutes();
|
||||||
|
|
||||||
await page.click('a[href="echo/page/encöded?message=hëllö+wörld"]');
|
await page.click('a[href="echo/page/encöded?message=hëllö+wörld&föo=bar&=baz"]');
|
||||||
await wait(50);
|
await wait(50);
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await page.$eval('h1', node => node.textContent),
|
await page.$eval('h1', node => node.textContent),
|
||||||
'encöded (hëllö wörld)'
|
'encöded {"message":"hëllö wörld","föo":"bar","":"baz"}'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
3
test/apps/errors/src/routes/async-throw.json.js
Normal file
3
test/apps/errors/src/routes/async-throw.json.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function get(req, res) {
|
||||||
|
return Promise.reject(new Error('oops'));
|
||||||
|
}
|
||||||
@@ -112,15 +112,26 @@ describe('errors', function() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not serve error page for async non-page error', async () => {
|
||||||
|
await page.goto(`${base}/async-throw.json`);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await page.evaluate(() => document.body.textContent),
|
||||||
|
'oops'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('clears props.error on successful render', async () => {
|
it('clears props.error on successful render', async () => {
|
||||||
await page.goto(`${base}/no-error`);
|
await page.goto(`${base}/no-error`);
|
||||||
await start();
|
await start();
|
||||||
await prefetchRoutes();
|
await prefetchRoutes();
|
||||||
|
|
||||||
await page.click('[href="enhance-your-calm"]');
|
await page.click('[href="enhance-your-calm"]');
|
||||||
|
await wait(50);
|
||||||
assert.equal(await title(), '420');
|
assert.equal(await title(), '420');
|
||||||
|
|
||||||
await page.goBack();
|
await page.goBack();
|
||||||
|
await wait(50);
|
||||||
assert.equal(await title(), 'No error here');
|
assert.equal(await title(), 'No error here');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<h1>root</h1>
|
<h1>root</h1>
|
||||||
|
|
||||||
<a href="redirect-from">redirect from</a>
|
<a href="redirect-from">redirect from</a>
|
||||||
<a href="redirect-to-root">redirect to root</a>
|
<a href="redirect-to-root">redirect to root</a>
|
||||||
|
<a href="redirect-to-external">redirect to external</a>
|
||||||
9
test/apps/redirects/src/routes/redirect-to-external.html
Normal file
9
test/apps/redirects/src/routes/redirect-to-external.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<h1>unredirected</h1>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
preload() {
|
||||||
|
this.redirect(301, 'https://example.com');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -14,13 +14,14 @@ describe('redirects', function() {
|
|||||||
// helpers
|
// helpers
|
||||||
let start: () => Promise<void>;
|
let start: () => Promise<void>;
|
||||||
let prefetchRoutes: () => Promise<void>;
|
let prefetchRoutes: () => Promise<void>;
|
||||||
|
let title: () => Promise<string>;
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
before(async () => {
|
before(async () => {
|
||||||
await build({ cwd: __dirname });
|
await build({ cwd: __dirname });
|
||||||
|
|
||||||
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
|
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
|
||||||
({ base, page, start, prefetchRoutes } = await runner.start());
|
({ base, page, start, prefetchRoutes, title } = await runner.start());
|
||||||
});
|
});
|
||||||
|
|
||||||
after(() => runner.end());
|
after(() => runner.end());
|
||||||
@@ -34,7 +35,7 @@ describe('redirects', function() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await page.$eval('h1', node => node.textContent),
|
await title(),
|
||||||
'redirected'
|
'redirected'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -53,7 +54,7 @@ describe('redirects', function() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await page.$eval('h1', node => node.textContent),
|
await title(),
|
||||||
'redirected'
|
'redirected'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -67,7 +68,7 @@ describe('redirects', function() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await page.$eval('h1', node => node.textContent),
|
await title(),
|
||||||
'root'
|
'root'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -86,8 +87,41 @@ describe('redirects', function() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await page.$eval('h1', node => node.textContent),
|
await title(),
|
||||||
'root'
|
'root'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('redirects to external URL on server', async () => {
|
||||||
|
await page.goto(`${base}/redirect-to-external`);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
page.url(),
|
||||||
|
`https://example.com/`
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await title(),
|
||||||
|
'external'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects to external URL in client', async () => {
|
||||||
|
await page.goto(base);
|
||||||
|
await start();
|
||||||
|
await prefetchRoutes();
|
||||||
|
|
||||||
|
await page.click('[href="redirect-to-external"]');
|
||||||
|
await wait(50);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
page.url(),
|
||||||
|
`https://example.com/`
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await title(),
|
||||||
|
'external'
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -17,7 +17,7 @@ describe('with-basepath', function() {
|
|||||||
|
|
||||||
await api.export({
|
await api.export({
|
||||||
cwd: __dirname,
|
cwd: __dirname,
|
||||||
basepath: 'custom-basepath'
|
basepath: '/custom-basepath'
|
||||||
});
|
});
|
||||||
|
|
||||||
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
|
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
|
||||||
|
|||||||
Reference in New Issue
Block a user