merge master -> svelte-3, fix various typescript and webpack oddities

This commit is contained in:
Richard Harris
2019-02-01 08:21:45 -05:00
36 changed files with 794 additions and 200 deletions

View File

@@ -64,11 +64,11 @@ export class AppRunner {
base: `http://localhost:${this.port}`,
// helpers
start: () => this.page.evaluate(() => start()),
prefetchRoutes: () => this.page.evaluate(() => prefetchRoutes()),
prefetch: (href: string) => this.page.evaluate((href: string) => prefetch(href), href),
goto: (href: string) => this.page.evaluate((href: string) => goto(href), href),
title: () => this.page.$eval('h1', node => node.textContent)
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))
};
}

View File

@@ -261,7 +261,7 @@ describe('basics', function() {
await page.goto(`${base}/unsafe-replacement`);
await start();
const html = await page.evaluate(() => document.body.innerHTML);
const html = String(await page.evaluate(() => document.body.innerHTML));
assert.equal(html.indexOf('%sapper'), -1);
});
});

View File

@@ -30,6 +30,7 @@ describe('export', function() {
'blog/index.html',
'global.css',
'index.html',
'service-worker-index.html',
'service-worker.js'
]);
});

View File

@@ -28,7 +28,7 @@ describe('layout', function() {
await page.goto(`${base}/foo/bar/baz`);
await start();
const text1 = await page.evaluate(() => document.querySelector('#sapper').textContent);
const text1 = String(await page.evaluate(() => document.querySelector('#sapper').textContent));
assert.deepEqual(text1.split('\n').filter(Boolean), [
'y: bar 1',
'z: baz 1',
@@ -39,7 +39,7 @@ describe('layout', function() {
await page.click('[href="foo/bar/qux"]');
await wait(50);
const text2 = await page.evaluate(() => document.querySelector('#sapper').textContent);
const text2 = String(await page.evaluate(() => document.querySelector('#sapper').textContent));
assert.deepEqual(text2.split('\n').filter(Boolean), [
'y: bar 1',
'z: qux 2',

View File

@@ -1,4 +1,6 @@
<h1>Great success!</h1>
<a href="slow-preload">slow preload</a>
<a href="foo">foo</a>
<a href="foo">foo</a>
<a href="prefetch/qwe" rel=prefetch>prefetch qwe</a>
<a href="prefetch/xyz" rel=prefetch>prefetch xyz</a>

View File

@@ -0,0 +1 @@
<h1>{params.slug}</h1>

View File

@@ -0,0 +1 @@
<svelte:component this={child.component} {...child.props}/>

View File

@@ -0,0 +1 @@
<h1>prefetch</h1>

View File

@@ -81,4 +81,31 @@ describe('preloading', function() {
assert.equal(page.url(), `${base}/foo`);
assert.equal(await title(), 'foo');
});
});
it('navigates to prefetched urls', async () => {
await page.goto(base);
await start();
await prefetchRoutes();
await page.hover('a[href="prefetch/qwe"]');
await wait(100);
await page.hover('a[href="prefetch/xyz"]');
await wait(100);
await page.click('a[href="prefetch/qwe"]');
await wait(50);
assert.equal(
await title(),
'qwe'
);
await page.goto(`${base}/prefetch`);
await wait(50);
assert.equal(
await title(),
'prefetch'
);
});
});

View File

@@ -31,7 +31,7 @@ describe('scroll', function() {
await start();
const scrollY = await page.evaluate(() => window.scrollY);
assert.ok(scrollY > 0, scrollY);
assert.ok(scrollY > 0, String(scrollY));
});
it('scrolls to any deeplink if it was already active', async () => {
@@ -39,17 +39,17 @@ describe('scroll', function() {
await start();
let scrollY = await page.evaluate(() => window.scrollY);
assert.ok(scrollY > 0, scrollY);
assert.ok(scrollY > 0, String(scrollY));
scrollY = await page.evaluate(() => {
window.scrollTo(0, 0)
return window.scrollY
});
assert.ok(scrollY === 0, scrollY);
assert.ok(scrollY === 0, String(scrollY));
await page.click('[href="tall-page#foo"]');
scrollY = await page.evaluate(() => window.scrollY);
assert.ok(scrollY > 0, scrollY);
assert.ok(scrollY > 0, String(scrollY));
});
it('resets scroll when a link is clicked', async () => {

View File

@@ -56,6 +56,7 @@ describe('with-basepath', function() {
assert.deepEqual(non_client_assets, [
'custom-basepath/global.css',
'custom-basepath/index.html',
'custom-basepath/service-worker-index.html',
'custom-basepath/service-worker.js'
]);
});

View File

@@ -0,0 +1,9 @@
import * as sapper from '@sapper/app';
window.start = () => sapper.start({
target: document.querySelector('#sapper')
});
window.prefetchRoutes = () => sapper.prefetchRoutes();
window.prefetch = href => sapper.prefetch(href);
window.goto = href => sapper.goto(href);

View File

@@ -0,0 +1,3 @@
<h1>{status}</h1>
<p>{error.message}</p>

View File

@@ -0,0 +1,3 @@
<h1>Great success!</h1>
<p>Woot!</p>

View File

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

View File

@@ -0,0 +1,82 @@
import { timestamp, files, shell, routes } from '@sapper/service-worker';
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;
}
})
);
});

View 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>

View File

@@ -0,0 +1,43 @@
import * as puppeteer from 'puppeteer';
import { build } from '../../../api';
import * as assert from "assert";
import { AppRunner } from '../AppRunner';
import * as fs from "fs";
import * as path from "path";
describe('with-sourcemaps-webpack', 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>;
// hooks
before(async () => {
await build({ cwd: __dirname, bundler: 'webpack' });
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes, prefetch, goto } = await runner.start());
});
it('does not put sourcemap files in service worker shell', async () => {
const serviceWorker = await import(`${__dirname}/__sapper__/service-worker.js`);
const shell: string[] = serviceWorker.shell;
assert.equal(shell.filter(_ => _.endsWith('.map')).length, 0,
'sourcemap files are not cached in SW');
const clientShellDir = path.resolve(`${__dirname}/__sapper__/build`, path.dirname(shell[0]));
const sourcemapFiles = fs.readdirSync(clientShellDir).filter(_ => _.endsWith('.map'));
assert.ok(sourcemapFiles.length > 0, 'sourcemap files exist');
});
after(() => runner.end());
});

View File

@@ -0,0 +1,73 @@
const webpack = require('webpack');
const config = require('../../../config/webpack.js');
const mode = process.env.NODE_ENV;
const dev = mode === 'development';
module.exports = {
client: {
entry: config.client.entry(),
output: config.client.output(),
resolve: {
extensions: ['.mjs', '.js', '.json', '.html'],
mainFields: ['svelte', 'module', 'browser', 'main']
},
module: {
rules: [
{
test: /\.html$/,
use: {
loader: 'svelte-loader',
options: {
dev,
hydratable: true,
hotReload: true
}
}
}
]
},
mode,
plugins: [
dev && new webpack.HotModuleReplacementPlugin(),
new webpack.DefinePlugin({
'process.browser': true,
'process.env.NODE_ENV': JSON.stringify(mode)
}),
].filter(Boolean),
devtool: dev ? 'inline-source-map' : 'source-map'
},
server: {
entry: config.server.entry(),
output: config.server.output(),
target: 'node',
resolve: {
extensions: ['.mjs', '.js', '.json', '.html'],
mainFields: ['svelte', 'module', 'browser', 'main']
},
module: {
rules: [
{
test: /\.html$/,
use: {
loader: 'svelte-loader',
options: {
css: false,
generate: 'ssr',
dev
}
}
}
]
},
mode: process.env.NODE_ENV
},
serviceworker: {
entry: config.serviceworker.entry(),
output: config.serviceworker.output(),
mode: process.env.NODE_ENV,
devtool: 'sourcemap'
}
};

View 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: Object.assign({}, config.client.output(), { sourcemap: true }),
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)
})
]
}
};

View 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);

View File

@@ -0,0 +1,3 @@
<h1>{status}</h1>
<p>{error.message}</p>

View File

@@ -0,0 +1,3 @@
<h1>Great success!</h1>
<p>Woot!</p>

View 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);

View 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;
}
})
);
});

View 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>

View File

@@ -0,0 +1,43 @@
import * as puppeteer from 'puppeteer';
import { build } from '../../../api';
import * as assert from "assert";
import { AppRunner } from '../AppRunner';
import * as fs from 'fs';
import * as path from "path";
describe('with-sourcemaps', 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>;
// hooks
before(async () => {
await build({ cwd: __dirname });
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes, prefetch, goto } = await runner.start());
});
it('does not put sourcemap files in service worker shell', async () => {
const serviceWorker = await import(`${__dirname}/__sapper__/service-worker.js`);
const shell: string[] = serviceWorker.shell;
assert.equal(shell.filter(_ => _.endsWith('.map')).length, 0,
'sourcemap files are not cached in SW');
const clientShellDir = path.resolve(`${__dirname}/__sapper__/build`, path.dirname(shell[0]));
const sourcemapFiles = fs.readdirSync(clientShellDir).filter(_ => _.endsWith('.map'));
assert.ok(sourcemapFiles.length > 0, 'sourcemap files exist');
});
after(() => runner.end());
});