fix exporting

This commit is contained in:
Rich Harris
2018-02-16 14:25:53 -05:00
parent f9828f9fd2
commit b02183af53
9 changed files with 102 additions and 130 deletions

View File

@@ -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}`);
});
}

View File

@@ -35,9 +35,11 @@ export default function create_templates() {
return key in data ? data[key] : '';
});
},
stream: (res: any, data: Record<string, string | Promise<string>>) => {
stream: (req: any, res: any, data: Record<string, string | Promise<string>>) => {
let i = 0;
let body = '';
function stream_inner(): Promise<void> {
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();
});

View File

@@ -11,5 +11,5 @@ export type Route = {
export type Template = {
render: (data: Record<string, string>) => string;
stream: (res, data: Record<string, string | Promise<string>>) => void;
stream: (req, res, data: Record<string, string | Promise<string>>) => void;
};

View File

@@ -112,7 +112,7 @@ function get_route_handler(chunks: Record<string, string>, get_assets: () => Ass
return { rendered: mod.render(data), serialized };
});
return template.stream(res, {
return template.stream(req, res, {
scripts: promise.then(({ serialized }) => {
const main = `<script src='/client/${chunks.main}'></script>`;
@@ -137,6 +137,17 @@ function get_route_handler(chunks: Record<string, string>, 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<string, string>, 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<string, string> = {};
// 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<string, string>, get_assets: () => Ass
// no matching route — 404
next();
} catch (error) {
console.error(error);
res.statusCode = 500;
res.setHeader('Content-Type', 'text/html');