From 351ab13d298033749f73b29bd87756d873769b95 Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Fri, 15 Feb 2019 23:52:30 -0800 Subject: [PATCH 1/2] fix: add link rel=preload for exported sites --- package-lock.json | 48 ++++++++--- package.json | 1 + src/api/export.ts | 54 ++++++++---- test/apps/export-webpack/src/client.js | 9 ++ .../export-webpack/src/routes/_error.svelte | 3 + .../src/routes/blog/[slug].html | 13 +++ .../src/routes/blog/[slug].json.js | 19 +++++ .../export-webpack/src/routes/blog/_posts.js | 5 ++ .../export-webpack/src/routes/blog/index.html | 17 ++++ .../src/routes/blog/index.json.js | 9 ++ .../export-webpack/src/routes/index.svelte | 4 + test/apps/export-webpack/src/server.js | 15 ++++ .../apps/export-webpack/src/service-worker.js | 82 +++++++++++++++++++ test/apps/export-webpack/src/template.html | 14 ++++ test/apps/export-webpack/static/global.css | 3 + test/apps/export-webpack/test.ts | 19 +++++ test/apps/export-webpack/webpack.config.js | 73 +++++++++++++++++ 17 files changed, 358 insertions(+), 30 deletions(-) create mode 100644 test/apps/export-webpack/src/client.js create mode 100644 test/apps/export-webpack/src/routes/_error.svelte create mode 100644 test/apps/export-webpack/src/routes/blog/[slug].html create mode 100644 test/apps/export-webpack/src/routes/blog/[slug].json.js create mode 100644 test/apps/export-webpack/src/routes/blog/_posts.js create mode 100644 test/apps/export-webpack/src/routes/blog/index.html create mode 100644 test/apps/export-webpack/src/routes/blog/index.json.js create mode 100644 test/apps/export-webpack/src/routes/index.svelte create mode 100644 test/apps/export-webpack/src/server.js create mode 100644 test/apps/export-webpack/src/service-worker.js create mode 100644 test/apps/export-webpack/src/template.html create mode 100644 test/apps/export-webpack/static/global.css create mode 100644 test/apps/export-webpack/test.ts create mode 100644 test/apps/export-webpack/webpack.config.js diff --git a/package-lock.json b/package-lock.json index 4d5712c7c..ad316bcab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index faf807549..3e3426c1d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/api/export.ts b/src/api/export.ts index ed601e4c8..6981f118f 100644 --- a/src/api/export.ts +++ b/src/api/export.ts @@ -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('', + ``) + } + }); + 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 = //m.exec(cleaned); - const base_href = base_match && get_href(base_match[1]); - const base = resolve(url.href, base_href); + const base_match = //m.exec(cleaned); + const base_href = base_match && get_href(base_match[1]); + const base = resolve(url.href, base_href); - let match; - let pattern = //gm; + let match; + let pattern = //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; + } } } diff --git a/test/apps/export-webpack/src/client.js b/test/apps/export-webpack/src/client.js new file mode 100644 index 000000000..6cce7e658 --- /dev/null +++ b/test/apps/export-webpack/src/client.js @@ -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); \ No newline at end of file diff --git a/test/apps/export-webpack/src/routes/_error.svelte b/test/apps/export-webpack/src/routes/_error.svelte new file mode 100644 index 000000000..4cd55d28d --- /dev/null +++ b/test/apps/export-webpack/src/routes/_error.svelte @@ -0,0 +1,3 @@ +

{status}

+ +

{error.message}

\ No newline at end of file diff --git a/test/apps/export-webpack/src/routes/blog/[slug].html b/test/apps/export-webpack/src/routes/blog/[slug].html new file mode 100644 index 000000000..2febdf20d --- /dev/null +++ b/test/apps/export-webpack/src/routes/blog/[slug].html @@ -0,0 +1,13 @@ + + + + +

{post.title}

\ No newline at end of file diff --git a/test/apps/export-webpack/src/routes/blog/[slug].json.js b/test/apps/export-webpack/src/routes/blog/[slug].json.js new file mode 100644 index 000000000..66781ad28 --- /dev/null +++ b/test/apps/export-webpack/src/routes/blog/[slug].json.js @@ -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' })); + } +} \ No newline at end of file diff --git a/test/apps/export-webpack/src/routes/blog/_posts.js b/test/apps/export-webpack/src/routes/blog/_posts.js new file mode 100644 index 000000000..d283aa6cf --- /dev/null +++ b/test/apps/export-webpack/src/routes/blog/_posts.js @@ -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' } +]; \ No newline at end of file diff --git a/test/apps/export-webpack/src/routes/blog/index.html b/test/apps/export-webpack/src/routes/blog/index.html new file mode 100644 index 000000000..d12e4321d --- /dev/null +++ b/test/apps/export-webpack/src/routes/blog/index.html @@ -0,0 +1,17 @@ + + + + +

blog

+ +{#each posts as post} +

{post.title}

+{/each} \ No newline at end of file diff --git a/test/apps/export-webpack/src/routes/blog/index.json.js b/test/apps/export-webpack/src/routes/blog/index.json.js new file mode 100644 index 000000000..0097f7793 --- /dev/null +++ b/test/apps/export-webpack/src/routes/blog/index.json.js @@ -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)); +} \ No newline at end of file diff --git a/test/apps/export-webpack/src/routes/index.svelte b/test/apps/export-webpack/src/routes/index.svelte new file mode 100644 index 000000000..cdeb5a8e1 --- /dev/null +++ b/test/apps/export-webpack/src/routes/index.svelte @@ -0,0 +1,4 @@ +

Great success!

+ +blog +empty anchor \ No newline at end of file diff --git a/test/apps/export-webpack/src/server.js b/test/apps/export-webpack/src/server.js new file mode 100644 index 000000000..2c6932da6 --- /dev/null +++ b/test/apps/export-webpack/src/server.js @@ -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); + }); diff --git a/test/apps/export-webpack/src/service-worker.js b/test/apps/export-webpack/src/service-worker.js new file mode 100644 index 000000000..8adb97a43 --- /dev/null +++ b/test/apps/export-webpack/src/service-worker.js @@ -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; + } + }) + ); +}); diff --git a/test/apps/export-webpack/src/template.html b/test/apps/export-webpack/src/template.html new file mode 100644 index 000000000..0eb1f3ba4 --- /dev/null +++ b/test/apps/export-webpack/src/template.html @@ -0,0 +1,14 @@ + + + + + + %sapper.base% + %sapper.styles% + %sapper.head% + + +
%sapper.html%
+ %sapper.scripts% + + diff --git a/test/apps/export-webpack/static/global.css b/test/apps/export-webpack/static/global.css new file mode 100644 index 000000000..800f57ad1 --- /dev/null +++ b/test/apps/export-webpack/static/global.css @@ -0,0 +1,3 @@ +body { + font-family: 'Comic Sans MS'; +} \ No newline at end of file diff --git a/test/apps/export-webpack/test.ts b/test/apps/export-webpack/test.ts new file mode 100644 index 000000000..44b892275 --- /dev/null +++ b/test/apps/export-webpack/test.ts @@ -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 tags', () => { + const index = fs.readFileSync(`${__dirname}/__sapper__/export/index.html`, 'utf8'); + assert.ok(/rel=preload/, index); + }); + +}); diff --git a/test/apps/export-webpack/webpack.config.js b/test/apps/export-webpack/webpack.config.js new file mode 100644 index 000000000..a8b950fbc --- /dev/null +++ b/test/apps/export-webpack/webpack.config.js @@ -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' + } +}; From 37780656fd428b141b15b2fadcededf50ceb3e16 Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Sat, 16 Feb 2019 00:13:27 -0800 Subject: [PATCH 2/2] fix incorrect test --- test/apps/export-webpack/test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/apps/export-webpack/test.ts b/test/apps/export-webpack/test.ts index 44b892275..44abb78c6 100644 --- a/test/apps/export-webpack/test.ts +++ b/test/apps/export-webpack/test.ts @@ -13,7 +13,7 @@ describe('export-webpack', function() { it('injects tags', () => { const index = fs.readFileSync(`${__dirname}/__sapper__/export/index.html`, 'utf8'); - assert.ok(/rel=preload/, index); + assert.ok(/rel=preload/.test(index)); }); });