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

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@ yarn.lock
node_modules
cypress/screenshots
test/app/.sapper
test/app/app/manifest
runtime.js
runtime.js.map
cli.js

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');

View File

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

View File

@@ -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$ }
];

View File

@@ -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(?:\/([^\/]+))?\/?$/ }
];

View File

@@ -60,6 +60,7 @@ function run(env) {
let handler;
proc.on('message', message => {
if (message.__sapper__) return;
if (handler) handler(message);
});