Improve internal API

This commit is contained in:
Rich Harris
2018-10-08 19:21:15 -04:00
committed by GitHub
parent 5e59855a15
commit 52f40f9e63
46 changed files with 696 additions and 1091 deletions

View File

@@ -2,44 +2,51 @@ import * as fs from 'fs';
import * as path from 'path';
import mkdirp from 'mkdirp';
import rimraf from 'rimraf';
import { EventEmitter } from 'events';
import minify_html from './utils/minify_html';
import { create_compilers, create_main_manifests, create_manifest_data, create_serviceworker_manifest } from '../core';
import * as events from './interfaces';
import { copy_shimport } from './utils/copy_shimport';
import { Dirs } from '../interfaces';
import read_template from '../core/read_template';
import { CompileResult } from '../core/create_compilers/interfaces';
import { noop } from './utils/noop';
import validate_bundler from './utils/validate_bundler';
type Opts = {
legacy: boolean;
bundler: 'rollup' | 'webpack';
cwd?: string;
src?: string;
routes?: string;
dest?: string;
output?: string;
static_files?: string;
legacy?: boolean;
bundler?: 'rollup' | 'webpack';
oncompile?: ({ type, result }: { type: string, result: CompileResult }) => void;
};
export function build(opts: Opts, dirs: Dirs) {
const emitter = new EventEmitter();
export async function build({
cwd = process.cwd(),
src = path.join(cwd, 'src'),
routes = path.join(cwd, 'src/routes'),
output = path.join(cwd, '__sapper__'),
static_files = path.join(cwd, 'static'),
dest = path.join(cwd, '__sapper__/build'),
execute(emitter, opts, dirs).then(
() => {
emitter.emit('done', <events.DoneEvent>{}); // TODO do we need to pass back any info?
},
error => {
emitter.emit('error', <events.ErrorEvent>{
error
});
}
);
bundler,
legacy = false,
oncompile = noop
}: Opts = {}) {
bundler = validate_bundler(bundler);
return emitter;
}
if (legacy && bundler === 'webpack') {
throw new Error(`Legacy builds are not supported for projects using webpack`);
}
async function execute(emitter: EventEmitter, opts: Opts, dirs: Dirs) {
rimraf.sync(path.join(dirs.dest, '**/*'));
mkdirp.sync(`${dirs.dest}/client`);
copy_shimport(dirs.dest);
rimraf.sync(path.join(dest, '**/*'));
mkdirp.sync(`${dest}/client`);
copy_shimport(dest);
// minify src/template.html
// TODO compile this to a function? could be quicker than str.replace(...).replace(...).replace(...)
const template = read_template();
const template = read_template(src);
// remove this in a future version
if (template.indexOf('%sapper.base%') === -1) {
@@ -48,47 +55,53 @@ async function execute(emitter: EventEmitter, opts: Opts, dirs: Dirs) {
throw error;
}
fs.writeFileSync(`${dirs.dest}/template.html`, minify_html(template));
fs.writeFileSync(`${dest}/template.html`, minify_html(template));
const manifest_data = create_manifest_data();
const manifest_data = create_manifest_data(routes);
// create src/manifest/client.js and src/manifest/server.js
create_main_manifests({ bundler: opts.bundler, manifest_data });
create_main_manifests({
bundler,
manifest_data,
cwd,
src,
dest,
routes,
output,
dev: false
});
const { client, server, serviceworker } = await create_compilers(opts.bundler);
const { client, server, serviceworker } = await create_compilers(bundler, cwd, src, dest, true);
const client_result = await client.compile();
emitter.emit('build', <events.BuildEvent>{
oncompile({
type: 'client',
// TODO duration/warnings
result: client_result
});
const build_info = client_result.to_json(manifest_data, dirs);
const build_info = client_result.to_json(manifest_data, { src, routes, dest });
if (opts.legacy) {
if (legacy) {
process.env.SAPPER_LEGACY_BUILD = 'true';
const { client } = await create_compilers(opts.bundler);
const { client } = await create_compilers(bundler, cwd, src, dest, true);
const client_result = await client.compile();
emitter.emit('build', <events.BuildEvent>{
oncompile({
type: 'client (legacy)',
// TODO duration/warnings
result: client_result
});
client_result.to_json(manifest_data, dirs);
client_result.to_json(manifest_data, { src, routes, dest });
build_info.legacy_assets = client_result.assets;
delete process.env.SAPPER_LEGACY_BUILD;
}
fs.writeFileSync(path.join(dirs.dest, 'build.json'), JSON.stringify(build_info));
fs.writeFileSync(path.join(dest, 'build.json'), JSON.stringify(build_info));
const server_stats = await server.compile();
emitter.emit('build', <events.BuildEvent>{
oncompile({
type: 'server',
// TODO duration/warnings
result: server_stats
});
@@ -97,14 +110,15 @@ async function execute(emitter: EventEmitter, opts: Opts, dirs: Dirs) {
if (serviceworker) {
create_serviceworker_manifest({
manifest_data,
client_files: client_result.chunks.map(chunk => `client/${chunk.file}`)
output,
client_files: client_result.chunks.map(chunk => `client/${chunk.file}`),
static_files
});
serviceworker_stats = await serviceworker.compile();
emitter.emit('build', <events.BuildEvent>{
oncompile({
type: 'serviceworker',
// TODO duration/warnings
result: serviceworker_stats
});
}

View File

@@ -5,30 +5,45 @@ import * as child_process from 'child_process';
import * as ports from 'port-authority';
import mkdirp from 'mkdirp';
import rimraf from 'rimraf';
import { locations } from '../config';
import { EventEmitter } from 'events';
import { create_manifest_data, create_main_manifests, create_compilers, create_serviceworker_manifest } from '../core';
import { Compiler, Compilers } from '../core/create_compilers';
import { CompileResult, CompileError } from '../core/create_compilers/interfaces';
import { CompileResult } from '../core/create_compilers/interfaces';
import Deferred from './utils/Deferred';
import * as events from './interfaces';
import validate_bundler from '../cli/utils/validate_bundler';
import validate_bundler from './utils/validate_bundler';
import { copy_shimport } from './utils/copy_shimport';
import { ManifestData } from '../interfaces';
import { ManifestData, FatalEvent, ErrorEvent, ReadyEvent, InvalidEvent } from '../interfaces';
import read_template from '../core/read_template';
import { noop } from './utils/noop';
export function dev(opts) {
type Opts = {
cwd?: string,
src?: string,
dest?: string,
routes?: string,
output?: string,
static_files?: string,
'dev-port'?: number,
live?: boolean,
hot?: boolean,
'devtools-port'?: number,
bundler?: 'rollup' | 'webpack',
port?: number
};
export function dev(opts: Opts) {
return new Watcher(opts);
}
class Watcher extends EventEmitter {
bundler: string;
bundler: 'rollup' | 'webpack';
dirs: {
cwd: string;
src: string;
dest: string;
routes: string;
rollup: string;
webpack: string;
output: string;
static_files: string;
}
port: number;
closed: boolean;
@@ -54,34 +69,23 @@ class Watcher extends EventEmitter {
}
constructor({
src = locations.src(),
dest = locations.dest(),
routes = locations.routes(),
cwd = process.cwd(),
src = path.join(cwd, 'src'),
routes = path.join(cwd, 'src/routes'),
output = path.join(cwd, '__sapper__'),
static_files = path.join(cwd, 'static'),
dest = path.join(cwd, '__sapper__/dev'),
'dev-port': dev_port,
live,
hot,
'devtools-port': devtools_port,
bundler,
webpack = 'webpack',
rollup = 'rollup',
port = +process.env.PORT
}: {
src: string,
dest: string,
routes: string,
'dev-port': number,
live: boolean,
hot: boolean,
'devtools-port': number,
bundler?: string,
webpack: string,
rollup: string,
port: number
}) {
}: Opts) {
super();
this.bundler = validate_bundler(bundler);
this.dirs = { src, dest, routes, webpack, rollup };
this.dirs = { cwd, src, dest, routes, output, static_files };
this.port = port;
this.closed = false;
@@ -101,7 +105,7 @@ class Watcher extends EventEmitter {
};
// remove this in a future version
const template = read_template();
const template = read_template(src);
if (template.indexOf('%sapper.base%') === -1) {
const error = new Error(`As of Sapper v0.10, your template.html file must include %sapper.base% in the <head>`);
error.code = `missing-sapper-base`;
@@ -120,7 +124,7 @@ class Watcher extends EventEmitter {
async init() {
if (this.port) {
if (!await ports.check(this.port)) {
this.emit('fatal', <events.FatalEvent>{
this.emit('fatal', <FatalEvent>{
message: `Port ${this.port} is unavailable`
});
return;
@@ -129,7 +133,7 @@ class Watcher extends EventEmitter {
this.port = await ports.find(3000);
}
const { dest } = this.dirs;
const { cwd, src, dest, routes, output, static_files } = this.dirs;
rimraf.sync(dest);
mkdirp.sync(`${dest}/client`);
if (this.bundler === 'rollup') copy_shimport(dest);
@@ -142,10 +146,16 @@ class Watcher extends EventEmitter {
let manifest_data: ManifestData;
try {
manifest_data = create_manifest_data();
create_main_manifests({ bundler: this.bundler, manifest_data, dev_port: this.dev_port });
manifest_data = create_manifest_data(routes);
create_main_manifests({
bundler: this.bundler,
manifest_data,
dev: true,
dev_port: this.dev_port,
cwd, src, dest, routes, output
});
} catch (err) {
this.emit('fatal', <events.FatalEvent>{
this.emit('fatal', <FatalEvent>{
message: err.message
});
return;
@@ -155,7 +165,7 @@ class Watcher extends EventEmitter {
this.filewatchers.push(
watch_dir(
locations.routes(),
routes,
({ path: file, stats }) => {
if (stats.isDirectory()) {
return path.basename(file)[0] !== '_';
@@ -164,19 +174,25 @@ class Watcher extends EventEmitter {
},
() => {
try {
const new_manifest_data = create_manifest_data();
create_main_manifests({ bundler: this.bundler, manifest_data, dev_port: this.dev_port });
const new_manifest_data = create_manifest_data(routes);
create_main_manifests({
bundler: this.bundler,
manifest_data, // TODO is this right? not new_manifest_data?
dev: true,
dev_port: this.dev_port,
cwd, src, dest, routes, output
});
manifest_data = new_manifest_data;
} catch (err) {
this.emit('error', <events.ErrorEvent>{
this.emit('error', <ErrorEvent>{
message: err.message
});
}
}
),
fs.watch(`${locations.src()}/template.html`, () => {
fs.watch(`${src}/template.html`, () => {
this.dev_server.send({
action: 'reload'
});
@@ -186,12 +202,12 @@ class Watcher extends EventEmitter {
let deferred = new Deferred();
// TODO watch the configs themselves?
const compilers: Compilers = await create_compilers(this.bundler, this.dirs);
const compilers: Compilers = await create_compilers(this.bundler, cwd, src, dest, false);
let log = '';
const emitFatal = () => {
this.emit('fatal', <events.FatalEvent>{
this.emit('fatal', <FatalEvent>{
message: `Server crashed`,
log
});
@@ -215,7 +231,7 @@ class Watcher extends EventEmitter {
ports.wait(this.port)
.then((() => {
this.emit('ready', <events.ReadyEvent>{
this.emit('ready', <ReadyEvent>{
port: this.port,
process: this.proc
});
@@ -233,7 +249,7 @@ class Watcher extends EventEmitter {
.catch(err => {
if (this.crashed) return;
this.emit('fatal', <events.FatalEvent>{
this.emit('fatal', <FatalEvent>{
message: `Server is not listening on port ${this.port}`,
log
});
@@ -312,7 +328,9 @@ class Watcher extends EventEmitter {
create_serviceworker_manifest({
manifest_data,
client_files
output,
client_files,
static_files
});
deferred.fulfil();
@@ -361,7 +379,7 @@ class Watcher extends EventEmitter {
};
process.nextTick(() => {
this.emit('invalid', <events.InvalidEvent>{
this.emit('invalid', <InvalidEvent>{
changed: Array.from(this.current_build.changed),
invalid: {
server: this.current_build.rebuilding.has('server'),
@@ -384,7 +402,7 @@ class Watcher extends EventEmitter {
compiler.watch((err?: Error, result?: CompileResult) => {
if (err) {
this.emit('error', <events.ErrorEvent>{
this.emit('error', <ErrorEvent>{
type: name,
message: err.message
});
@@ -455,14 +473,12 @@ class DevServer {
}
}
function noop() {}
function watch_dir(
dir: string,
filter: ({ path, stats }: { path: string, stats: fs.Stats }) => boolean,
callback: () => void
) {
let watch;
let watch: any;
let closed = false;
import('cheap-watch').then(CheapWatch => {
@@ -470,7 +486,7 @@ function watch_dir(
watch = new CheapWatch({ dir, filter, debounce: 50 });
watch.on('+', ({ isNew }) => {
watch.on('+', ({ isNew }: { isNew: boolean }) => {
if (isNew) callback();
});

View File

@@ -4,58 +4,52 @@ import * as sander from 'sander';
import * as url from 'url';
import fetch from 'node-fetch';
import * as ports from 'port-authority';
import { EventEmitter } from 'events';
import clean_html from './utils/clean_html';
import minify_html from './utils/minify_html';
import Deferred from './utils/Deferred';
import * as events from './interfaces';
import { noop } from './utils/noop';
type Opts = {
build: string,
dest: string,
static: string,
build_dir?: string,
export_dir?: string,
cwd?: string,
static?: string,
basepath?: string,
timeout: number | false
timeout?: number | false,
oninfo?: ({ message }: { message: string }) => void;
onfile?: ({ file, size, status }: { file: string, size: number, status: number }) => void;
};
export function exporter(opts: Opts) {
const emitter = new EventEmitter();
execute(emitter, opts).then(
() => {
emitter.emit('done', <events.DoneEvent>{}); // TODO do we need to pass back any info?
},
error => {
emitter.emit('error', <events.ErrorEvent>{
error
});
}
);
return emitter;
}
function resolve(from: string, to: string) {
return url.parse(url.resolve(from, to));
}
type URL = url.UrlWithStringQuery;
async function execute(emitter: EventEmitter, opts: Opts) {
const export_dir = path.join(opts.dest, opts.basepath);
export { _export as export };
async function _export({
cwd = process.cwd(),
static: static_files = path.join(cwd, 'static'),
build_dir = path.join(cwd, '__sapper__/build'),
basepath = '',
export_dir = path.join(cwd, '__sapper__/export', basepath),
timeout = 5000,
oninfo = noop,
onfile = noop
}: Opts = {}) {
// Prep output directory
sander.rimrafSync(export_dir);
sander.copydirSync(opts.static).to(export_dir);
sander.copydirSync(opts.build, 'client').to(export_dir, 'client');
sander.copydirSync(static_files).to(export_dir);
sander.copydirSync(build_dir, 'client').to(export_dir, 'client');
if (sander.existsSync(opts.build, 'service-worker.js')) {
sander.copyFileSync(opts.build, 'service-worker.js').to(export_dir, 'service-worker.js');
if (sander.existsSync(build_dir, 'service-worker.js')) {
sander.copyFileSync(build_dir, 'service-worker.js').to(export_dir, 'service-worker.js');
}
if (sander.existsSync(opts.build, 'service-worker.js.map')) {
sander.copyFileSync(opts.build, 'service-worker.js.map').to(export_dir, 'service-worker.js.map');
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);
@@ -64,19 +58,18 @@ async function execute(emitter: EventEmitter, opts: Opts) {
const host = `localhost:${port}`;
const origin = `${protocol}//${host}`;
const root = resolve(origin, opts.basepath || '');
const root = resolve(origin, basepath);
if (!root.href.endsWith('/')) root.href += '/';
emitter.emit('info', {
oninfo({
message: `Crawling ${root.href}`
});
const proc = child_process.fork(path.resolve(`${opts.build}/server/server.js`), [], {
cwd: process.cwd(),
const proc = child_process.fork(path.resolve(`${build_dir}/server/server.js`), [], {
cwd,
env: Object.assign({
PORT: port,
NODE_ENV: 'production',
SAPPER_DEST: opts.build,
SAPPER_EXPORT: 'true'
}, process.env)
});
@@ -98,7 +91,7 @@ async function execute(emitter: EventEmitter, opts: Opts) {
body = minify_html(body);
}
emitter.emit('file', <events.FileEvent>{
onfile({
file,
size: body.length,
status
@@ -119,9 +112,9 @@ async function execute(emitter: EventEmitter, opts: Opts) {
seen.add(pathname);
const timeout_deferred = new Deferred();
const timeout = setTimeout(() => {
const the_timeout = setTimeout(() => {
timeout_deferred.reject(new Error(`Timed out waiting for ${url.href}`));
}, opts.timeout);
}, timeout);
const r = await Promise.race([
fetch(url.href, {
@@ -130,7 +123,7 @@ async function execute(emitter: EventEmitter, opts: Opts) {
timeout_deferred.promise
]);
clearTimeout(timeout); // prevent it hanging at the end
clearTimeout(the_timeout); // prevent it hanging at the end
let type = r.headers.get('Content-Type');
let body = await r.text();

View File

@@ -1,7 +1,6 @@
import { locations } from '../config';
import { create_manifest_data } from '../core';
export function find_page(pathname: string, cwd = locations.routes()) {
export function find_page(pathname: string, cwd = 'src/routes') {
const { pages } = create_manifest_data(cwd);
for (let i = 0; i < pages.length; i += 1) {

View File

@@ -1,45 +0,0 @@
import * as child_process from 'child_process';
import { CompileResult } from '../core/create_compilers/interfaces';
export type ReadyEvent = {
port: number;
process: child_process.ChildProcess;
};
export type ErrorEvent = {
type: string;
message: string;
};
export type FatalEvent = {
message: string;
log?: string;
};
export type InvalidEvent = {
changed: string[];
invalid: {
client: boolean;
server: boolean;
serviceworker: boolean;
}
};
export type BuildEvent = {
type: string;
errors: Array<{ file: string, message: string, duplicate: boolean }>;
warnings: Array<{ file: string, message: string, duplicate: boolean }>;
duration: number;
result: CompileResult;
}
export type FileEvent = {
file: string;
size: number;
}
export type FailureEvent = {
}
export type DoneEvent = {};

1
src/api/utils/noop.ts Normal file
View File

@@ -0,0 +1 @@
export function noop() {}

View File

@@ -0,0 +1,38 @@
import * as fs from 'fs';
export default function validate_bundler(bundler?: 'rollup' | 'webpack') {
if (!bundler) {
bundler = (
fs.existsSync('rollup.config.js') ? 'rollup' :
fs.existsSync('webpack.config.js') ? 'webpack' :
null
);
if (!bundler) {
// TODO remove in a future version
deprecate_dir('rollup');
deprecate_dir('webpack');
throw new Error(`Could not find rollup.config.js or webpack.config.js`);
}
}
if (bundler !== 'rollup' && bundler !== 'webpack') {
throw new Error(`'${bundler}' is not a valid option for --bundler — must be either 'rollup' or 'webpack'`);
}
return bundler;
}
function deprecate_dir(bundler: 'rollup' | 'webpack') {
try {
const stats = fs.statSync(bundler);
if (!stats.isDirectory()) return;
} catch (err) {
// do nothing
return;
}
// TODO link to docs, once those docs exist
throw new Error(`As of Sapper 0.21, build configuration should be placed in a single ${bundler}.config.js file`);
}