mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-11 19:04:30 +00:00
fix: add link rel=preload for exported sites
This commit is contained in:
48
package-lock.json
generated
48
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sapper",
|
||||
"version": "0.26.0-alpha.5",
|
||||
"version": "0.26.0-alpha.6",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -2366,7 +2366,8 @@
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
@@ -2387,12 +2388,14 @@
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -2407,17 +2410,20 @@
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
@@ -2534,7 +2540,8 @@
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
@@ -2546,6 +2553,7 @@
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@@ -2560,6 +2568,7 @@
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@@ -2567,12 +2576,14 @@
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.5",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
@@ -2591,6 +2602,7 @@
|
||||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
@@ -2671,7 +2683,8 @@
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@@ -2683,6 +2696,7 @@
|
||||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@@ -2768,7 +2782,8 @@
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
@@ -2804,6 +2819,7 @@
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@@ -2823,6 +2839,7 @@
|
||||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
@@ -2866,12 +2883,14 @@
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3093,6 +3112,11 @@
|
||||
"uglify-js": "3.4.x"
|
||||
}
|
||||
},
|
||||
"http-link-header": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/http-link-header/-/http-link-header-1.0.2.tgz",
|
||||
"integrity": "sha512-z6YOZ8ZEnejkcCWlGZzYXNa6i+ZaTfiTg3WhlV/YvnNya3W/RbX1bMVUMTuCrg/DrtTCQxaFCkXCz4FtLpcebg=="
|
||||
},
|
||||
"https-browserify": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"html-minifier": "^3.5.21",
|
||||
"http-link-header": "^1.0.2",
|
||||
"shimport": "0.0.14",
|
||||
"source-map-support": "^0.5.10",
|
||||
"sourcemap-codec": "^1.4.4",
|
||||
|
||||
@@ -9,6 +9,7 @@ import clean_html from './utils/clean_html';
|
||||
import minify_html from './utils/minify_html';
|
||||
import Deferred from './utils/Deferred';
|
||||
import { noop } from './utils/noop';
|
||||
import { parse as parseLinkHeader } from 'http-link-header';
|
||||
|
||||
type Opts = {
|
||||
build_dir?: string,
|
||||
@@ -21,6 +22,12 @@ type Opts = {
|
||||
onfile?: ({ file, size, status }: { file: string, size: number, status: number }) => void;
|
||||
};
|
||||
|
||||
type Ref = {
|
||||
uri: string,
|
||||
rel: string,
|
||||
as: string
|
||||
};
|
||||
|
||||
function resolve(from: string, to: string) {
|
||||
return url.parse(url.resolve(from, to));
|
||||
}
|
||||
@@ -139,38 +146,49 @@ async function _export({
|
||||
clearTimeout(the_timeout); // prevent it hanging at the end
|
||||
|
||||
let type = r.headers.get('Content-Type');
|
||||
|
||||
let body = await r.text();
|
||||
|
||||
const range = ~~(r.status / 100);
|
||||
|
||||
if (range === 2) {
|
||||
if (type === 'text/html' && pathname !== '/service-worker-index.html') {
|
||||
const cleaned = clean_html(body);
|
||||
if (type === 'text/html') {
|
||||
// parse link rel=preload headers and embed them in the HTML
|
||||
let link = parseLinkHeader(r.headers.get('Link') || '');
|
||||
link.refs.forEach((ref: Ref) => {
|
||||
if (ref.rel === 'preload') {
|
||||
body = body.replace('</head>',
|
||||
`<link rel="preload" as=${JSON.stringify(ref.as)} href=${JSON.stringify(ref.uri)}></head>`)
|
||||
}
|
||||
});
|
||||
if (pathname !== '/service-worker-index.html') {
|
||||
const cleaned = clean_html(body);
|
||||
|
||||
const q = yootils.queue(8);
|
||||
let promise;
|
||||
const q = yootils.queue(8);
|
||||
let promise;
|
||||
|
||||
const base_match = /<base ([\s\S]+?)>/m.exec(cleaned);
|
||||
const base_href = base_match && get_href(base_match[1]);
|
||||
const base = resolve(url.href, base_href);
|
||||
const base_match = /<base ([\s\S]+?)>/m.exec(cleaned);
|
||||
const base_href = base_match && get_href(base_match[1]);
|
||||
const base = resolve(url.href, base_href);
|
||||
|
||||
let match;
|
||||
let pattern = /<a ([\s\S]+?)>/gm;
|
||||
let match;
|
||||
let pattern = /<a ([\s\S]+?)>/gm;
|
||||
|
||||
while (match = pattern.exec(cleaned)) {
|
||||
const attrs = match[1];
|
||||
const href = get_href(attrs);
|
||||
while (match = pattern.exec(cleaned)) {
|
||||
const attrs = match[1];
|
||||
const href = get_href(attrs);
|
||||
|
||||
if (href) {
|
||||
const url = resolve(base.href, href);
|
||||
if (href) {
|
||||
const url = resolve(base.href, href);
|
||||
|
||||
if (url.protocol === protocol && url.host === host) {
|
||||
promise = q.add(() => handle(url));
|
||||
if (url.protocol === protocol && url.host === host) {
|
||||
promise = q.add(() => handle(url));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await promise;
|
||||
await promise;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
9
test/apps/export-webpack/src/client.js
Normal file
9
test/apps/export-webpack/src/client.js
Normal 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);
|
||||
3
test/apps/export-webpack/src/routes/_error.svelte
Normal file
3
test/apps/export-webpack/src/routes/_error.svelte
Normal file
@@ -0,0 +1,3 @@
|
||||
<h1>{status}</h1>
|
||||
|
||||
<p>{error.message}</p>
|
||||
13
test/apps/export-webpack/src/routes/blog/[slug].html
Normal file
13
test/apps/export-webpack/src/routes/blog/[slug].html
Normal file
@@ -0,0 +1,13 @@
|
||||
<script context="module">
|
||||
export function preload({ params }) {
|
||||
return this.fetch(`blog/${params.slug}.json`).then(r => r.json()).then(post => {
|
||||
return { post };
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export let post;
|
||||
</script>
|
||||
|
||||
<h1>{post.title}</h1>
|
||||
19
test/apps/export-webpack/src/routes/blog/[slug].json.js
Normal file
19
test/apps/export-webpack/src/routes/blog/[slug].json.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import posts from './_posts.js';
|
||||
|
||||
export function get(req, res) {
|
||||
const post = posts.find(post => post.slug === req.params.slug);
|
||||
|
||||
if (post) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
|
||||
res.end(JSON.stringify(post));
|
||||
} else {
|
||||
res.writeHead(404, {
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
|
||||
res.end(JSON.stringify({ message: 'not found' }));
|
||||
}
|
||||
}
|
||||
5
test/apps/export-webpack/src/routes/blog/_posts.js
Normal file
5
test/apps/export-webpack/src/routes/blog/_posts.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export default [
|
||||
{ slug: 'foo', title: 'once upon a foo' },
|
||||
{ slug: 'bar', title: 'a bar is born' },
|
||||
{ slug: 'baz', title: 'bazzily ever after' }
|
||||
];
|
||||
17
test/apps/export-webpack/src/routes/blog/index.html
Normal file
17
test/apps/export-webpack/src/routes/blog/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<script context="module">
|
||||
export function preload() {
|
||||
return this.fetch('blog.json').then(r => r.json()).then(posts => {
|
||||
return { posts };
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export let posts;
|
||||
</script>
|
||||
|
||||
<h1>blog</h1>
|
||||
|
||||
{#each posts as post}
|
||||
<p><a href="blog/{post.slug}">{post.title}</a></p>
|
||||
{/each}
|
||||
9
test/apps/export-webpack/src/routes/blog/index.json.js
Normal file
9
test/apps/export-webpack/src/routes/blog/index.json.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import posts from './_posts.js';
|
||||
|
||||
export function get(req, res) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
|
||||
res.end(JSON.stringify(posts));
|
||||
}
|
||||
4
test/apps/export-webpack/src/routes/index.svelte
Normal file
4
test/apps/export-webpack/src/routes/index.svelte
Normal file
@@ -0,0 +1,4 @@
|
||||
<h1>Great success!</h1>
|
||||
|
||||
<a href="blog">blog</a>
|
||||
<a href="">empty anchor</a>
|
||||
15
test/apps/export-webpack/src/server.js
Normal file
15
test/apps/export-webpack/src/server.js
Normal file
@@ -0,0 +1,15 @@
|
||||
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';
|
||||
|
||||
polka()
|
||||
.use(
|
||||
sirv('static', { dev }),
|
||||
sapper.middleware()
|
||||
)
|
||||
.listen(PORT, err => {
|
||||
if (err) console.log('error', err);
|
||||
});
|
||||
82
test/apps/export-webpack/src/service-worker.js
Normal file
82
test/apps/export-webpack/src/service-worker.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import * as sapper from '@sapper/service-worker';
|
||||
|
||||
const ASSETS = `cache${sapper.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 = sapper.shell.concat(sapper.files);
|
||||
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${sapper.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/export-webpack/src/template.html
Normal file
14
test/apps/export-webpack/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>
|
||||
3
test/apps/export-webpack/static/global.css
Normal file
3
test/apps/export-webpack/static/global.css
Normal file
@@ -0,0 +1,3 @@
|
||||
body {
|
||||
font-family: 'Comic Sans MS';
|
||||
}
|
||||
19
test/apps/export-webpack/test.ts
Normal file
19
test/apps/export-webpack/test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as assert from 'assert';
|
||||
import * as api from '../../../api';
|
||||
import * as fs from 'fs';
|
||||
|
||||
describe('export-webpack', function() {
|
||||
this.timeout(10000);
|
||||
|
||||
// hooks
|
||||
before(async () => {
|
||||
await api.build({ cwd: __dirname, bundler: 'webpack' });
|
||||
await api.export({ cwd: __dirname, bundler: 'webpack' });
|
||||
});
|
||||
|
||||
it('injects <link rel=preload> tags', () => {
|
||||
const index = fs.readFileSync(`${__dirname}/__sapper__/export/index.html`, 'utf8');
|
||||
assert.ok(/rel=preload/, index);
|
||||
});
|
||||
|
||||
});
|
||||
73
test/apps/export-webpack/webpack.config.js
Normal file
73
test/apps/export-webpack/webpack.config.js
Normal 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', '.svelte'],
|
||||
mainFields: ['svelte', 'module', 'browser', 'main']
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(html|svelte)$/,
|
||||
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', '.svelte'],
|
||||
mainFields: ['svelte', 'module', 'browser', 'main']
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(html|svelte)$/,
|
||||
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'
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user