create API for exporting

This commit is contained in:
Rich Harris
2018-05-22 11:57:24 -04:00
parent 4f49fd8d5c
commit 0dbf75f100
3 changed files with 156 additions and 99 deletions

View File

@@ -0,0 +1,130 @@
import * as child_process from 'child_process';
import * as path from 'path';
import * as sander from 'sander';
import cheerio from 'cheerio';
import URL from 'url-parse';
import fetch from 'node-fetch';
import * as ports from 'port-authority';
import { EventEmitter } from 'events';
import { minify_html } from './utils/minify_html';
import { locations } from '../config';
export default function exporter(opts: {}) {
const emitter = new EventEmitter();
execute(emitter, opts).then(
() => {
emitter.emit('done', {}); // TODO do we need to pass back any info?
},
error => {
emitter.emit('error', {
error
});
}
);
return emitter;
}
async function execute(emitter: EventEmitter, {
build = 'build',
dest = 'export',
basepath = ''
} = {}) {
const export_dir = path.join(dest, basepath);
// Prep output directory
sander.rimrafSync(export_dir);
sander.copydirSync('assets').to(export_dir);
sander.copydirSync(build, 'client').to(export_dir, 'client');
if (sander.existsSync(build, 'service-worker.js')) {
sander.copyFileSync(build, 'service-worker.js').to(export_dir, 'service-worker.js');
}
if (sander.existsSync(build, 'service-worker.js.map')) {
sander.copyFileSync(build, 'service-worker.js.map').to(export_dir, 'service-worker.js.map');
}
const port = await ports.find(3000);
const origin = `http://localhost:${port}`;
const proc = child_process.fork(path.resolve(`${build}/server.js`), [], {
cwd: process.cwd(),
env: Object.assign({
PORT: port,
NODE_ENV: 'production',
SAPPER_DEST: build,
SAPPER_EXPORT: 'true'
}, process.env)
});
const seen = new Set();
const saved = new Set();
proc.on('message', message => {
if (!message.__sapper__) return;
let file = new URL(message.url, origin).pathname.slice(1);
let { body } = message;
if (saved.has(file)) return;
saved.add(file);
const is_html = message.type === 'text/html';
if (is_html) {
file = file === '' ? 'index.html' : `${file}/index.html`;
body = minify_html(body);
}
emitter.emit('file', {
file,
size: body.length
});
sander.writeFileSync(export_dir, file, body);
});
async function handle(url: URL) {
const r = await fetch(url.href);
const range = ~~(r.status / 100);
if (range >= 4) {
emitter.emit('failure', {
status: r.status,
pathname: url.pathname
});
return;
}
if (range === 2) {
if (r.headers.get('Content-Type') === 'text/html') {
const body = await r.text();
const $ = cheerio.load(body);
const urls: URL[] = [];
const base = new URL($('base').attr('href') || '/', url.href);
$('a[href]').each((i: number, $a) => {
const url = new URL($a.attribs.href, base.href);
if (url.origin === origin && !seen.has(url.pathname)) {
seen.add(url.pathname);
urls.push(url);
}
});
for (const url of urls) {
await handle(url);
}
}
}
}
return ports.wait(port)
.then(() => handle(new URL(`/${basepath}`, origin))) // TODO all static routes
.then(() => proc.kill());
}