From b02183af53c6884198a3343bed5a1e299f3b2bc3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 16 Feb 2018 14:25:53 -0500 Subject: [PATCH] fix exporting --- .gitignore | 1 + src/cli/export.ts | 77 ++++++++++--------------- src/core/create_template.ts | 28 +++++++-- src/interfaces.ts | 2 +- src/middleware/index.ts | 46 ++++++++++++++- test/app/app/manifest/client.js | 12 ---- test/app/app/manifest/server.js | 28 --------- test/app/app/manifest/service-worker.js | 37 ------------ test/common/test.js | 1 + 9 files changed, 102 insertions(+), 130 deletions(-) delete mode 100644 test/app/app/manifest/client.js delete mode 100644 test/app/app/manifest/server.js delete mode 100644 test/app/app/manifest/service-worker.js diff --git a/.gitignore b/.gitignore index a557e2c..f589ba6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ yarn.lock node_modules cypress/screenshots test/app/.sapper +test/app/app/manifest runtime.js runtime.js.map cli.js diff --git a/src/cli/export.ts b/src/cli/export.ts index 02cf349..a60851f 100644 --- a/src/cli/export.ts +++ b/src/cli/export.ts @@ -3,9 +3,7 @@ import * as path from 'path'; import * as sander from 'sander'; import express from 'express'; import cheerio from 'cheerio'; -import fetch from 'node-fetch'; import URL from 'url-parse'; -import { create_assets } from 'sapper/core.js'; const { OUTPUT_DIR = 'dist' } = process.env; @@ -15,61 +13,50 @@ function read_json(file: string) { return JSON.parse(sander.readFileSync(file, { encoding: 'utf-8' })); } -export default async function exporter({ src, dest }) { // TODO dest is a terrible name in this context +export default async function exporter(dir: string) { // dir === '.sapper' // Prep output directory sander.rimrafSync(OUTPUT_DIR); sander.copydirSync('assets').to(OUTPUT_DIR); - sander.copydirSync(dest, 'client').to(OUTPUT_DIR, 'client'); - - // Intercept server route fetches - function save(res) { - res = res.clone(); - - return res.text().then(body => { - const { pathname } = new URL(res.url); - let dest = OUTPUT_DIR + pathname; - - const type = res.headers.get('Content-Type'); - if (type && type.startsWith('text/html')) dest += '/index.html'; - - sander.writeFileSync(dest, body); - - return body; - }); - } + sander.copydirSync(dir, 'client').to(OUTPUT_DIR, 'client'); + sander.copyFileSync(dir, 'service-worker.js').to(OUTPUT_DIR, 'service-worker.js'); const port = await require('get-port')(3000); const origin = `http://localhost:${port}`; - global.fetch = (url, opts) => { - if (url[0] === '/') { - url = `http://localhost:${port}${url}`; - - return fetch(url, opts) - .then(r => { - save(r); - return r; - }); - } - - return fetch(url, opts); - }; - - const proc = child_process.fork(path.resolve(`${dest}/server.js`), [], { + const proc = child_process.fork(path.resolve(`${dir}/server.js`), [], { cwd: process.cwd(), env: { PORT: port, - NODE_ENV: 'production' + NODE_ENV: 'production', + SAPPER_EXPORT: 'true' + } + }); + + const seen = new Set(); + const saved = new Set(); + + proc.on('message', message => { + if (!message.__sapper__) return; + + const url = new URL(message.url, origin); + + if (saved.has(url.pathname)) return; + saved.add(url.pathname); + + if (message.type === 'text/html') { + const dest = `${OUTPUT_DIR}/${url.pathname}/index.html`; + sander.writeFileSync(dest, message.body); + } else { + const dest = `${OUTPUT_DIR}/${url.pathname}`; + sander.writeFileSync(dest, message.body); } }); await require('wait-port')({ port }); - const seen = new Set(); - - function handle(url) { + function handle(url: URL) { if (url.origin !== origin) return; if (seen.has(url.pathname)) return; @@ -77,14 +64,12 @@ export default async function exporter({ src, dest }) { // TODO dest is a terrib return fetch(url.href) .then(r => { - save(r); - if (r.headers.get('Content-Type') === 'text/html') { - return r.text().then(body => { + return r.text().then((body: string) => { const $ = cheerio.load(body); - const hrefs = []; + const hrefs: string[] = []; - $('a[href]').each((i, $a) => { + $('a[href]').each((i: number, $a) => { hrefs.push($a.attribs.href); }); @@ -94,7 +79,7 @@ export default async function exporter({ src, dest }) { // TODO dest is a terrib }); } }) - .catch(err => { + .catch((err: Error) => { console.error(`Error rendering ${url.pathname}: ${err.message}`); }); } diff --git a/src/core/create_template.ts b/src/core/create_template.ts index 9345fdb..84eb5dc 100644 --- a/src/core/create_template.ts +++ b/src/core/create_template.ts @@ -35,9 +35,11 @@ export default function create_templates() { return key in data ? data[key] : ''; }); }, - stream: (res: any, data: Record>) => { + stream: (req: any, res: any, data: Record>) => { let i = 0; + let body = ''; + function stream_inner(): Promise { if (i >= template.length) { return; @@ -46,11 +48,26 @@ export default function create_templates() { const start = template.indexOf('%sapper', i); if (start === -1) { - res.end(template.slice(i)); + const chunk = template.slice(i); + body += chunk; + res.end(chunk); + + if (process.send) { + process.send({ + __sapper__: true, + url: req.url, + method: req.method, + type: 'text/html', + body + }); + } + return; } - res.write(template.slice(i, start)); + const chunk = template.slice(i, start); + body += chunk; + res.write(chunk); const end = template.indexOf('%', start + 1); if (end === -1) { @@ -61,8 +78,9 @@ export default function create_templates() { const match = /sapper\.(\w+)/.exec(tag); if (!match || !(match[1] in data)) throw new Error(`Bad template`); // TODO ditto - return Promise.resolve(data[match[1]]).then(datamatch => { - res.write(datamatch); + return Promise.resolve(data[match[1]]).then(chunk => { + body += chunk; + res.write(chunk); i = end + 1; return stream_inner(); }); diff --git a/src/interfaces.ts b/src/interfaces.ts index 0313be6..3d72869 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -11,5 +11,5 @@ export type Route = { export type Template = { render: (data: Record) => string; - stream: (res, data: Record>) => void; + stream: (req, res, data: Record>) => void; }; \ No newline at end of file diff --git a/src/middleware/index.ts b/src/middleware/index.ts index f893f2b..2838d45 100644 --- a/src/middleware/index.ts +++ b/src/middleware/index.ts @@ -112,7 +112,7 @@ function get_route_handler(chunks: Record, get_assets: () => Ass return { rendered: mod.render(data), serialized }; }); - return template.stream(res, { + return template.stream(req, res, { scripts: promise.then(({ serialized }) => { const main = ``; @@ -137,6 +137,17 @@ function get_route_handler(chunks: Record, get_assets: () => Ass }); res.end(page); + + if (process.send) { + process.send({ + __sapper__: true, + url: req.url, + method: req.method, + status: 200, + type: 'text/html', + body: page + }); + } } } @@ -147,6 +158,37 @@ function get_route_handler(chunks: Record, get_assets: () => Ass const method_export = method === 'delete' ? 'del' : method; const handler = mod[method_export]; if (handler) { + if (process.env.SAPPER_EXPORT) { + const { write, end, setHeader } = res; + const chunks: any[] = []; + const headers: Record = {}; + + // intercept data so that it can be exported + res.write = function(chunk: any) { + chunks.push(new Buffer(chunk)); + write.apply(res, arguments); + }; + + res.setHeader = function(name: string, value: string) { + headers[name.toLowerCase()] = value; + setHeader.apply(res, arguments); + }; + + res.end = function(chunk?: any) { + if (chunk) chunks.push(new Buffer(chunk)); + end.apply(res, arguments); + + process.send({ + __sapper__: true, + url: req.url, + method: req.method, + status: res.statusCode, + type: headers['content-type'], + body: Buffer.concat(chunks).toString() + }); + }; + } + handler(req, res, next); } else { // no matching handler for method — 404 @@ -168,6 +210,8 @@ function get_route_handler(chunks: Record, get_assets: () => Ass // no matching route — 404 next(); } catch (error) { + console.error(error); + res.statusCode = 500; res.setHeader('Content-Type', 'text/html'); diff --git a/test/app/app/manifest/client.js b/test/app/app/manifest/client.js deleted file mode 100644 index 9ee00eb..0000000 --- a/test/app/app/manifest/client.js +++ /dev/null @@ -1,12 +0,0 @@ -// This file is generated by Sapper — do not edit it! -export const routes = [ - { pattern: /^\/?$/, params: () => ({}), load: () => import(/* webpackChunkName: "_" */ '../../routes/index.html') }, - { pattern: /^\/4xx\/?$/, params: () => ({}), load: () => import(/* webpackChunkName: "_4xx" */ '../../routes/4xx.html') }, - { pattern: /^\/about\/?$/, params: () => ({}), load: () => import(/* webpackChunkName: "about" */ '../../routes/about.html') }, - { pattern: /^\/show-url\/?$/, params: () => ({}), load: () => import(/* webpackChunkName: "show_url" */ '../../routes/show-url.html') }, - { pattern: /^\/slow-preload\/?$/, params: () => ({}), load: () => import(/* webpackChunkName: "slow_preload" */ '../../routes/slow-preload.html') }, - { pattern: /^\/delete-test\/?$/, params: () => ({}), load: () => import(/* webpackChunkName: "delete_test" */ '../../routes/delete-test.html') }, - { pattern: /^\/5xx\/?$/, params: () => ({}), load: () => import(/* webpackChunkName: "_5xx" */ '../../routes/5xx.html') }, - { pattern: /^\/blog\/?$/, params: () => ({}), load: () => import(/* webpackChunkName: "blog" */ '../../routes/blog/index.html') }, - { pattern: /^\/blog(?:\/([^\/]+))?\/?$/, params: match => ({ slug: match[1] }), load: () => import(/* webpackChunkName: "blog_$slug$" */ '../../routes/blog/[slug].html') } -]; \ No newline at end of file diff --git a/test/app/app/manifest/server.js b/test/app/app/manifest/server.js deleted file mode 100644 index e7fb5ec..0000000 --- a/test/app/app/manifest/server.js +++ /dev/null @@ -1,28 +0,0 @@ -// This file is generated by Sapper — do not edit it! -import _ from '../../routes/index.html'; -import _4xx from '../../routes/4xx.html'; -import about from '../../routes/about.html'; -import show_url from '../../routes/show-url.html'; -import slow_preload from '../../routes/slow-preload.html'; -import delete_test from '../../routes/delete-test.html'; -import _5xx from '../../routes/5xx.html'; -import blog from '../../routes/blog/index.html'; -import * as api_blog_contents from '../../routes/api/blog/contents.js'; -import * as api_delete_$id$ from '../../routes/api/delete/[id].js'; -import * as api_blog_$slug$ from '../../routes/api/blog/[slug].js'; -import blog_$slug$ from '../../routes/blog/[slug].html'; - -export const routes = [ - { id: '_', type: 'page', pattern: /^\/?$/, params: () => ({}), module: _ }, - { id: '_4xx', type: 'page', pattern: /^\/4xx\/?$/, params: () => ({}), module: _4xx }, - { id: 'about', type: 'page', pattern: /^\/about\/?$/, params: () => ({}), module: about }, - { id: 'show_url', type: 'page', pattern: /^\/show-url\/?$/, params: () => ({}), module: show_url }, - { id: 'slow_preload', type: 'page', pattern: /^\/slow-preload\/?$/, params: () => ({}), module: slow_preload }, - { id: 'delete_test', type: 'page', pattern: /^\/delete-test\/?$/, params: () => ({}), module: delete_test }, - { id: '_5xx', type: 'page', pattern: /^\/5xx\/?$/, params: () => ({}), module: _5xx }, - { id: 'blog', type: 'page', pattern: /^\/blog\/?$/, params: () => ({}), module: blog }, - { id: 'api_blog_contents', type: 'route', pattern: /^\/api\/blog\/contents\/?$/, params: () => ({}), module: api_blog_contents }, - { id: 'api_delete_$id$', type: 'route', pattern: /^\/api\/delete(?:\/([^\/]+))?\/?$/, params: match => ({ id: match[1] }), module: api_delete_$id$ }, - { id: 'api_blog_$slug$', type: 'route', pattern: /^\/api\/blog(?:\/([^\/]+))?\/?$/, params: match => ({ slug: match[1] }), module: api_blog_$slug$ }, - { id: 'blog_$slug$', type: 'page', pattern: /^\/blog(?:\/([^\/]+))?\/?$/, params: match => ({ slug: match[1] }), module: blog_$slug$ } -]; \ No newline at end of file diff --git a/test/app/app/manifest/service-worker.js b/test/app/app/manifest/service-worker.js deleted file mode 100644 index 6b11ba8..0000000 --- a/test/app/app/manifest/service-worker.js +++ /dev/null @@ -1,37 +0,0 @@ - -export const timestamp = 1518800295364; - -export const assets = [ - "favicon.png", - "global.css", - "great-success.png", - "manifest.json", - "svelte-logo-192.png", - "svelte-logo-512.png" -]; - -export const shell = [ - "/client/_.0.3a37f8afa58c59f4bdf9.js", - "/client/blog.1.3a37f8afa58c59f4bdf9.js", - "/client/blog_$slug$.2.3a37f8afa58c59f4bdf9.js", - "/client/about.3.3a37f8afa58c59f4bdf9.js", - "/client/_5xx.4.3a37f8afa58c59f4bdf9.js", - "/client/_4xx.5.3a37f8afa58c59f4bdf9.js", - "/client/slow_preload.6.3a37f8afa58c59f4bdf9.js", - "/client/show_url.7.3a37f8afa58c59f4bdf9.js", - "/client/delete_test.8.3a37f8afa58c59f4bdf9.js", - "/client/main.3a37f8afa58c59f4bdf9.js" -]; - -export const routes = [ - { pattern: /^\/?$/ }, - { pattern: /^\/4xx\/?$/ }, - { pattern: /^\/about\/?$/ }, - { pattern: /^\/show-url\/?$/ }, - { pattern: /^\/slow-preload\/?$/ }, - { pattern: /^\/delete-test\/?$/ }, - { pattern: /^\/5xx\/?$/ }, - { pattern: /^\/blog\/?$/ }, - { pattern: /^\/blog(?:\/([^\/]+))?\/?$/ } -]; - \ No newline at end of file diff --git a/test/common/test.js b/test/common/test.js index a2076b1..803ba1f 100644 --- a/test/common/test.js +++ b/test/common/test.js @@ -60,6 +60,7 @@ function run(env) { let handler; proc.on('message', message => { + if (message.__sapper__) return; if (handler) handler(message); });