diff --git a/src/api/export.ts b/src/api/export.ts index e69de29..7fb90df 100644 --- a/src/api/export.ts +++ b/src/api/export.ts @@ -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()); +} diff --git a/src/cli/build.ts b/src/cli/build.ts index 332ef32..2311dfc 100644 --- a/src/cli/build.ts +++ b/src/cli/build.ts @@ -5,14 +5,12 @@ import { locations } from '../config'; export function build() { return new Promise((fulfil, reject) => { try { - console.log('1'); const emitter = _build({ dest: locations.dest(), app: locations.app(), routes: locations.routes(), webpack: 'webpack' }); - console.log('2', emitter); emitter.on('build', event => { console.log(colors.inverse(`\nbuilt ${event.type}`)); diff --git a/src/cli/export.ts b/src/cli/export.ts index d90fad0..66cc2cb 100644 --- a/src/cli/export.ts +++ b/src/cli/export.ts @@ -1,106 +1,35 @@ -import * as child_process from 'child_process'; -import * as path from 'path'; -import * as sander from 'sander'; +import _exporter from '../api/export'; import * as colors from 'ansi-colors'; -import cheerio from 'cheerio'; -import URL from 'url-parse'; -import fetch from 'node-fetch'; -import * as ports from 'port-authority'; import prettyBytes from 'pretty-bytes'; -import { minify_html } from '../api/utils/minify_html'; import { locations } from '../config'; -export async function exporter(export_dir: string, { basepath = '' }) { - const build_dir = locations.dest(); +export function exporter(export_dir: string, { basepath = '' }) { + return new Promise((fulfil, reject) => { + try { + const emitter = _exporter({ + build: locations.dest(), + dest: export_dir, + basepath + }); - export_dir = path.join(export_dir, basepath); + emitter.on('file', event => { + console.log(`${colors.bold.cyan(event.file)} ${colors.gray(`(${prettyBytes(event.size)})`)}`); + }); - // Prep output directory - sander.rimrafSync(export_dir); + emitter.on('failure', event => { + console.log(`${colors.red(`> Received ${event.status} response when fetching ${event.pathname}`)}`); + }); - sander.copydirSync('assets').to(export_dir); - sander.copydirSync(build_dir, 'client').to(export_dir, 'client'); + emitter.on('error', event => { + reject(event.error); + }); - if (sander.existsSync(build_dir, 'service-worker.js')) { - sander.copyFileSync(build_dir, 'service-worker.js').to(export_dir, 'service-worker.js'); - } - - if (sander.existsSync(build_dir, 'service-worker.js.map')) { - sander.copyFileSync(build_dir, '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_dir}/server.js`), [], { - cwd: process.cwd(), - env: Object.assign({ - PORT: port, - NODE_ENV: 'production', - SAPPER_DEST: build_dir, - SAPPER_EXPORT: 'true' - }, process.env) + emitter.on('done', event => { + fulfil(); + }); + } catch (err) { + console.log(`${colors.bold.red(`> ${err.message}`)}`); + process.exit(1); + } }); - - 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); - } - - console.log(`${colors.bold.cyan(file)} ${colors.gray(`(${prettyBytes(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) { - console.log(`${colors.red(`> Received ${r.status} response when fetching ${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()); -} +} \ No newline at end of file