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,4 +2,4 @@
--require ts-node/register
--recursive
test/unit/*/*.ts
test/apps/*/__test__.ts
test/apps/*/test.ts

15
package-lock.json generated
View File

@@ -4423,12 +4423,6 @@
"error-ex": "^1.2.0"
}
},
"parse-ms": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-1.0.1.tgz",
"integrity": "sha1-VjRtR0nXjyNDDKDHE4UK75GqNh0=",
"dev": true
},
"parseurl": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
@@ -4599,15 +4593,6 @@
"integrity": "sha512-wa5+qGVg9Yt7PB6rYm3kXlKzgzgivYTLRandezh43jjRqgyDyP+9YxfJpJiLs9yKD1WeU8/OvtToWpW7255FtA==",
"dev": true
},
"pretty-ms": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-3.2.0.tgz",
"integrity": "sha512-ZypexbfVUGTFxb0v+m1bUyy92DHe5SyYlnyY0msyms5zd3RwyvNgyxZZsXXgoyzlxjx5MiqtXUdhUfvQbe0A2Q==",
"dev": true,
"requires": {
"parse-ms": "^1.0.0"
}
},
"process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",

View File

@@ -44,7 +44,6 @@
"polka": "^0.4.0",
"port-authority": "^1.0.5",
"pretty-bytes": "^5.0.0",
"pretty-ms": "^3.1.0",
"puppeteer": "^1.9.0",
"require-relative": "^0.8.7",
"rimraf": "^2.6.2",
@@ -61,7 +60,6 @@
"sirv": "^0.2.2",
"svelte": "^2.6.3",
"svelte-loader": "^2.9.0",
"tiny-glob": "^0.2.2",
"ts-node": "^7.0.1",
"typescript": "^2.8.3",
"webpack": "^4.8.3",

View File

@@ -43,8 +43,8 @@ export default [
`src/api.ts`,
`src/cli.ts`,
`src/core.ts`,
`src/rollup.ts`,
`src/webpack.ts`
`src/config/rollup.ts`,
`src/config/webpack.ts`
],
output: {
dir: 'dist',

View File

@@ -1,6 +1,4 @@
import { dev } from './api/dev';
import { build } from './api/build';
import { exporter } from './api/export';
import { find_page } from './api/find_page';
export { dev, build, exporter, find_page };
export { dev } from './api/dev';
export { build } from './api/build';
export { export } from './api/export';
export { find_page } from './api/find_page';

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

@@ -2,11 +2,21 @@ import * as fs from 'fs';
import * as path from 'path';
import sade from 'sade';
import colors from 'kleur';
import prettyMs from 'pretty-ms';
import * as pkg from '../package.json';
import { elapsed, repeat, left_pad, format_milliseconds } from './utils';
import { InvalidEvent, ErrorEvent, FatalEvent, BuildEvent, ReadyEvent } from './interfaces';
const prog = sade('sapper').version(pkg.version);
if (process.argv[2] === 'start') {
// remove this in a future version
console.error(colors.bold.red(`'sapper start' has been removed`));
console.error(`Use 'node [build_dir]' instead`);
process.exit(1);
}
const start = Date.now();
prog.command('dev')
.describe('Start a development server')
.option('-p, --port', 'Specify a port')
@@ -21,10 +31,93 @@ prog.command('dev')
'dev-port': number,
live: boolean,
hot: boolean,
bundler?: string
bundler?: 'rollup' | 'webpack'
}) => {
const { dev } = await import('./cli/dev');
dev(opts);
const cwd = path.resolve(process.env.SAPPER_BASE || '');
const { dev } = await import('./api/dev');
try {
const watcher = dev({
cwd,
port: opts.port,
'dev-port': opts['dev-port'],
live: opts.live,
hot: opts.hot,
bundler: opts.bundler
});
let first = true;
watcher.on('ready', async (event: ReadyEvent) => {
if (first) {
console.log(colors.bold.cyan(`> Listening on http://localhost:${event.port}`));
if (opts.open) {
const { exec } = await import('child_process');
exec(`open http://localhost:${event.port}`);
}
first = false;
}
// TODO clear screen?
event.process.stdout.on('data', data => {
process.stdout.write(data);
});
event.process.stderr.on('data', data => {
process.stderr.write(data);
});
});
watcher.on('invalid', (event: InvalidEvent) => {
const changed = event.changed.map(filename => path.relative(process.cwd(), filename)).join(', ');
console.log(`\n${colors.bold.cyan(changed)} changed. rebuilding...`);
});
watcher.on('error', (event: ErrorEvent) => {
console.log(colors.red(`${event.type}`));
console.log(colors.red(event.message));
});
watcher.on('fatal', (event: FatalEvent) => {
console.log(colors.bold.red(`> ${event.message}`));
if (event.log) console.log(event.log);
});
watcher.on('build', (event: BuildEvent) => {
if (event.errors.length) {
console.log(colors.bold.red(`${event.type}`));
event.errors.filter(e => !e.duplicate).forEach(error => {
if (error.file) console.log(colors.bold(error.file));
console.log(error.message);
});
const hidden = event.errors.filter(e => e.duplicate).length;
if (hidden > 0) {
console.log(`${hidden} duplicate ${hidden === 1 ? 'error' : 'errors'} hidden\n`);
}
} else if (event.warnings.length) {
console.log(colors.bold.yellow(`${event.type}`));
event.warnings.filter(e => !e.duplicate).forEach(warning => {
if (warning.file) console.log(colors.bold(warning.file));
console.log(warning.message);
});
const hidden = event.warnings.filter(e => e.duplicate).length;
if (hidden > 0) {
console.log(`${hidden} duplicate ${hidden === 1 ? 'warning' : 'warnings'} hidden\n`);
}
} else {
console.log(`${colors.bold.green(`${event.type}`)} ${colors.gray(`(${format_milliseconds(event.duration)})`)}`);
}
});
} catch (err) {
console.log(colors.bold.red(`> ${err.message}`));
process.exit(1);
}
});
prog.command('build [dest]')
@@ -36,25 +129,20 @@ prog.command('build [dest]')
.action(async (dest = '__sapper__/build', opts: {
port: string,
legacy: boolean,
bundler?: string
bundler?: 'rollup' | 'webpack'
}) => {
console.log(`> Building...`);
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
process.env.SAPPER_DEST = dest;
const start = Date.now();
const cwd = path.resolve(process.env.SAPPER_BASE || '');
try {
const { build } = await import('./cli/build');
await build(opts);
await _build(opts.bundler, opts.legacy, cwd, dest);
const launcher = path.resolve(dest, 'index.js');
fs.writeFileSync(launcher, `
// generated by sapper build at ${new Date().toISOString()}
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
process.env.SAPPER_DEST = __dirname;
process.env.PORT = process.env.PORT || ${opts.port || 3000};
console.log('Starting server on port ' + process.env.PORT);
@@ -68,15 +156,6 @@ prog.command('build [dest]')
}
});
prog.command('start [dir]')
.describe('Start your app')
.option('-p, --port', 'Specify a port')
.option('-o, --open', 'Open a browser window')
.action(async (dir = 'build', opts: { port: number, open: boolean }) => {
const { start } = await import('./cli/start');
start(dir, opts);
});
prog.command('export [dest]')
.describe('Export your app as static files (if possible)')
.option('--build', '(Re)build app before exporting', true)
@@ -88,26 +167,46 @@ prog.command('export [dest]')
.action(async (dest = '__sapper__/export', opts: {
build: boolean,
legacy: boolean,
bundler?: string,
bundler?: 'rollup' | 'webpack',
'build-dir': string,
basepath?: string,
timeout: number | false
}) => {
process.env.NODE_ENV = 'production';
process.env.SAPPER_DEST = opts['build-dir'];
const start = Date.now();
const cwd = path.resolve(process.env.SAPPER_BASE || '');
try {
if (opts.build) {
console.log(`> Building...`);
const { build } = await import('./cli/build');
await build(opts);
await _build(opts.bundler, opts.legacy, cwd, opts['build-dir']);
console.error(`\n> Built in ${elapsed(start)}`);
}
const { exporter } = await import('./cli/export');
await exporter(dest, opts);
const { export: _export } = await import('./api/export');
const { default: pb } = await import('pretty-bytes');
await _export({
static: path.resolve(cwd, process.env.SAPPER_STATIC || 'static'),
build_dir: opts['build-dir'],
export_dir: dest,
basepath: opts.basepath,
timeout: opts.timeout,
oninfo: event => {
console.log(colors.bold.cyan(`> ${event.message}`));
},
onfile: event => {
const size_color = event.size > 150000 ? colors.bold.red : event.size > 50000 ? colors.bold.yellow : colors.bold.gray;
const size_label = size_color(left_pad(pb(event.size), 10));
const file_label = event.status === 200
? event.file
: colors.bold[event.status >= 400 ? 'red' : 'yellow'](`(${event.status}) ${event.file}`);
console.log(`${size_label} ${file_label}`);
}
});
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold.cyan(`npx serve ${dest}`)} to run the app.`);
} catch (err) {
console.error(colors.bold.red(`> ${err.message}`));
@@ -115,10 +214,41 @@ prog.command('export [dest]')
}
});
// TODO upgrade
prog.parse(process.argv);
function elapsed(start: number) {
return prettyMs(Date.now() - start);
}
async function _build(
bundler: 'rollup' | 'webpack',
legacy: boolean,
cwd: string,
dest: string
) {
const { build } = await import('./api/build');
await build({
bundler,
legacy,
cwd,
src: path.resolve(cwd, process.env.SAPPER_SRC || 'src'),
routes: path.resolve(cwd, process.env.SAPPER_ROUTES || 'src/routes'),
dest: path.resolve(cwd, dest),
oncompile: event => {
let banner = `built ${event.type}`;
let c = colors.cyan;
const { warnings } = event.result;
if (warnings.length > 0) {
banner += ` with ${warnings.length} ${warnings.length === 1 ? 'warning' : 'warnings'}`;
c = colors.yellow;
}
console.log();
console.log(c(`┌─${repeat('─', banner.length)}─┐`));
console.log(c(`${colors.bold(banner) }`));
console.log(c(`└─${repeat('─', banner.length)}─┘`));
console.log(event.result.print());
}
});
}

View File

@@ -1,54 +0,0 @@
import { build as _build } from '../api/build';
import colors from 'kleur';
import { locations } from '../config';
import validate_bundler from './utils/validate_bundler';
import { repeat } from '../utils';
export function build(opts: { bundler?: string, legacy?: boolean }) {
const bundler = validate_bundler(opts.bundler);
if (opts.legacy && bundler === 'webpack') {
throw new Error(`Legacy builds are not supported for projects using webpack`);
}
return new Promise((fulfil, reject) => {
try {
const emitter = _build({
legacy: opts.legacy,
bundler
}, {
dest: locations.dest(),
src: locations.src(),
routes: locations.routes()
});
emitter.on('build', event => {
let banner = `built ${event.type}`;
let c = colors.cyan;
const { warnings } = event.result;
if (warnings.length > 0) {
banner += ` with ${warnings.length} ${warnings.length === 1 ? 'warning' : 'warnings'}`;
c = colors.yellow;
}
console.log();
console.log(c(`┌─${repeat('─', banner.length)}─┐`));
console.log(c(`${colors.bold(banner) }`));
console.log(c(`└─${repeat('─', banner.length)}─┘`));
console.log(event.result.print());
});
emitter.on('error', event => {
reject(event.error);
});
emitter.on('done', event => {
fulfil();
});
} catch (err) {
reject(err);
}
});
}

View File

@@ -1,80 +0,0 @@
import * as path from 'path';
import colors from 'kleur';
import * as child_process from 'child_process';
import prettyMs from 'pretty-ms';
import { dev as _dev } from '../api/dev';
import * as events from '../api/interfaces';
export function dev(opts: { port: number, open: boolean, bundler?: string }) {
try {
const watcher = _dev(opts);
let first = true;
watcher.on('ready', (event: events.ReadyEvent) => {
if (first) {
console.log(colors.bold.cyan(`> Listening on http://localhost:${event.port}`));
if (opts.open) child_process.exec(`open http://localhost:${event.port}`);
first = false;
}
// TODO clear screen?
event.process.stdout.on('data', data => {
process.stdout.write(data);
});
event.process.stderr.on('data', data => {
process.stderr.write(data);
});
});
watcher.on('invalid', (event: events.InvalidEvent) => {
const changed = event.changed.map(filename => path.relative(process.cwd(), filename)).join(', ');
console.log(`\n${colors.bold.cyan(changed)} changed. rebuilding...`);
});
watcher.on('error', (event: events.ErrorEvent) => {
console.log(colors.red(`${event.type}`));
console.log(colors.red(event.message));
});
watcher.on('fatal', (event: events.FatalEvent) => {
console.log(colors.bold.red(`> ${event.message}`));
if (event.log) console.log(event.log);
});
watcher.on('build', (event: events.BuildEvent) => {
if (event.errors.length) {
console.log(colors.bold.red(`${event.type}`));
event.errors.filter(e => !e.duplicate).forEach(error => {
if (error.file) console.log(colors.bold(error.file));
console.log(error.message);
});
const hidden = event.errors.filter(e => e.duplicate).length;
if (hidden > 0) {
console.log(`${hidden} duplicate ${hidden === 1 ? 'error' : 'errors'} hidden\n`);
}
} else if (event.warnings.length) {
console.log(colors.bold.yellow(`${event.type}`));
event.warnings.filter(e => !e.duplicate).forEach(warning => {
if (warning.file) console.log(colors.bold(warning.file));
console.log(warning.message);
});
const hidden = event.warnings.filter(e => e.duplicate).length;
if (hidden > 0) {
console.log(`${hidden} duplicate ${hidden === 1 ? 'warning' : 'warnings'} hidden\n`);
}
} else {
console.log(`${colors.bold.green(`${event.type}`)} ${colors.gray(`(${prettyMs(event.duration)})`)}`);
}
});
} catch (err) {
console.log(colors.bold.red(`> ${err.message}`));
process.exit(1);
}
}

View File

@@ -1,51 +0,0 @@
import { exporter as _exporter } from '../api/export';
import colors from 'kleur';
import pb from 'pretty-bytes';
import { locations } from '../config';
import { left_pad } from '../utils';
export function exporter(export_dir: string, {
basepath = '',
timeout
}: {
basepath: string,
timeout: number | false
}) {
return new Promise((fulfil, reject) => {
try {
const emitter = _exporter({
build: locations.dest(),
static: locations.static(),
dest: export_dir,
basepath,
timeout
});
emitter.on('file', event => {
const size_color = event.size > 150000 ? colors.bold.red : event.size > 50000 ? colors.bold.yellow : colors.bold.gray;
const size_label = size_color(left_pad(pb(event.size), 10));
const file_label = event.status === 200
? event.file
: colors.bold[event.status >= 400 ? 'red' : 'yellow'](`(${event.status}) ${event.file}`);
console.log(`${size_label} ${file_label}`);
});
emitter.on('info', event => {
console.log(colors.bold.cyan(`> ${event.message}`));
});
emitter.on('error', event => {
reject(event.error);
});
emitter.on('done', event => {
fulfil();
});
} catch (err) {
console.log(`${colors.bold.red(`> ${err.message}`)}`);
process.exit(1);
}
});
}

View File

@@ -1,39 +0,0 @@
import * as fs from 'fs';
import * as path from 'path';
import * as child_process from 'child_process';
import colors from 'kleur';
import * as ports from 'port-authority';
export async function start(dir: string, opts: { port: number, open: boolean }) {
let port = opts.port || +process.env.PORT;
const resolved = path.resolve(dir);
const server = path.resolve(dir, 'server.js');
if (!fs.existsSync(server)) {
console.log(`${colors.bold.red(`> ${dir}/server.js does not exist — type ${colors.bold.cyan(dir === 'build' ? `npx sapper build` : `npx sapper build ${dir}`)} to create it`)}`);
return;
}
if (port) {
if (!await ports.check(port)) {
console.log(`${colors.bold.red(`> Port ${port} is unavailable`)}`);
return;
}
} else {
port = await ports.find(3000);
}
child_process.fork(server, [], {
cwd: process.cwd(),
env: Object.assign({
NODE_ENV: 'production',
PORT: port,
SAPPER_DEST: dir
}, process.env)
});
await ports.wait(port);
console.log(`${colors.bold.cyan(`> Listening on http://localhost:${port}`)}`);
if (opts.open) child_process.exec(`open http://localhost:${port}`);
}

View File

@@ -1,53 +0,0 @@
import * as fs from 'fs';
import colors from 'kleur';
export default async function upgrade() {
const upgraded = [
await upgrade_sapper_main()
].filter(Boolean);
if (upgraded.length === 0) {
console.log(`No changes!`);
}
}
async function upgrade_sapper_main() {
const _2xx = read('templates/2xx.html');
const _4xx = read('templates/4xx.html');
const _5xx = read('templates/5xx.html');
const pattern = /<script src='\%sapper\.main\%'><\/script>/;
let replaced = false;
['2xx', '4xx', '5xx'].forEach(code => {
const file = `templates/${code}.html`
const template = read(file);
if (!template) return;
if (/\%sapper\.main\%/.test(template)) {
if (!pattern.test(template)) {
console.log(`${colors.red(`Could not replace %sapper.main% in ${file}`)}`);
} else {
write(file, template.replace(pattern, `%sapper.scripts%`));
console.log(`${colors.green(`Replaced %sapper.main% in ${file}`)}`);
replaced = true;
}
}
});
return replaced;
}
function read(file: string) {
try {
return fs.readFileSync(file, 'utf-8');
} catch (err) {
console.error(err);
return null;
}
}
function write(file: string, data: string) {
fs.writeFileSync(file, data);
}

View File

@@ -1,11 +0,0 @@
import * as path from 'path';
export const dev = () => process.env.NODE_ENV !== 'production';
export const locations = {
base: () => path.resolve(process.env.SAPPER_BASE || ''),
src: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_SRC || 'src'),
static: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_STATIC || 'static'),
routes: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_ROUTES || 'src/routes'),
dest: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_DEST || `__sapper__/${dev() ? 'dev' : 'build'}`)
};

7
src/config/env.ts Normal file
View File

@@ -0,0 +1,7 @@
export let dev: boolean;
export let src: string;
export let dest: string;
export const set_dev = (_: boolean) => dev = _;
export const set_src = (_: string) => src = _;
export const set_dest = (_: string) => dest = _;

View File

@@ -1,15 +1,15 @@
import { locations, dev } from './config';
import { dev, src, dest } from './env';
export default {
dev: dev(),
dev,
client: {
input: () => {
return `${locations.src()}/client.js`
return `${src}/client.js`
},
output: () => {
let dir = `${locations.dest()}/client`;
let dir = `${dest}/client`;
if (process.env.SAPPER_LEGACY_BUILD) dir += `/legacy`;
return {
@@ -17,7 +17,7 @@ export default {
entryFileNames: '[name].[hash].js',
chunkFileNames: '[name].[hash].js',
format: 'esm',
sourcemap: dev()
sourcemap: dev
};
}
},
@@ -25,27 +25,27 @@ export default {
server: {
input: () => {
return {
server: `${locations.src()}/server.js`
server: `${src}/server.js`
};
},
output: () => {
return {
dir: `${locations.dest()}/server`,
dir: `${dest}/server`,
format: 'cjs',
sourcemap: dev()
sourcemap: dev
};
}
},
serviceworker: {
input: () => {
return `${locations.src()}/service-worker.js`;
return `${src}/service-worker.js`;
},
output: () => {
return {
file: `${locations.dest()}/service-worker.js`,
file: `${dest}/service-worker.js`,
format: 'iife'
}
}

View File

@@ -1,18 +1,18 @@
import { locations, dev } from './config';
import { dev, src, dest } from './env';
export default {
dev: dev(),
dev,
client: {
entry: () => {
return {
main: `${locations.src()}/client`
main: `${src}/client`
};
},
output: () => {
return {
path: `${locations.dest()}/client`,
path: `${dest}/client`,
filename: '[hash]/[name].js',
chunkFilename: '[hash]/[name].[id].js',
publicPath: `client/`
@@ -23,13 +23,13 @@ export default {
server: {
entry: () => {
return {
server: `${locations.src()}/server`
server: `${src}/server`
};
},
output: () => {
return {
path: `${locations.dest()}/server`,
path: `${dest}/server`,
filename: '[name].js',
chunkFilename: '[hash]/[name].[id].js',
libraryTarget: 'commonjs2'
@@ -40,13 +40,13 @@ export default {
serviceworker: {
entry: () => {
return {
'service-worker': `${locations.src()}/service-worker`
'service-worker': `${src}/service-worker`
};
},
output: () => {
return {
path: locations.dest(),
path: dest,
filename: '[name].js',
chunkFilename: '[name].[id].[hash].js'
}

View File

@@ -135,10 +135,10 @@ export default class RollupCompiler {
});
}
static async load_config() {
if (!rollup) rollup = relative('rollup', process.cwd());
static async load_config(cwd: string) {
if (!rollup) rollup = relative('rollup', cwd);
const input = path.resolve('rollup.config.js');
const input = path.resolve(cwd, 'rollup.config.js');
const bundle = await rollup.rollup({
input,

View File

@@ -5,7 +5,7 @@ import RollupCompiler from './RollupCompiler';
import extract_css from './extract_css';
import { left_pad } from '../../utils';
import { CompileResult, BuildInfo, CompileError, Chunk, CssFile } from './interfaces';
import { ManifestData, Dirs, PageComponent } from '../../interfaces';
import { ManifestData, Dirs } from '../../interfaces';
export default class RollupResult implements CompileResult {
duration: number;

View File

@@ -4,7 +4,7 @@ import hash from 'string-hash';
import * as codec from 'sourcemap-codec';
import { PageComponent, Dirs } from '../../interfaces';
import { CompileResult } from './interfaces';
import { posixify } from '../utils'
import { posixify } from '../../utils'
const inline_sourcemap_header = 'data:application/json;charset=utf-8;base64,';

View File

@@ -1,6 +1,7 @@
import * as path from 'path';
import RollupCompiler from './RollupCompiler';
import { WebpackCompiler } from './WebpackCompiler';
import { set_dev, set_src, set_dest } from '../../config/env';
export type Compiler = RollupCompiler | WebpackCompiler;
@@ -10,9 +11,19 @@ export type Compilers = {
serviceworker?: Compiler;
}
export default async function create_compilers(bundler: 'rollup' | 'webpack'): Promise<Compilers> {
export default async function create_compilers(
bundler: 'rollup' | 'webpack',
cwd: string,
src: string,
dest: string,
dev: boolean
): Promise<Compilers> {
set_dev(dev);
set_src(src);
set_dest(dest);
if (bundler === 'rollup') {
const config = await RollupCompiler.load_config();
const config = await RollupCompiler.load_config(cwd);
validate_config(config, 'rollup');
normalize_rollup_config(config.client);
@@ -30,7 +41,7 @@ export default async function create_compilers(bundler: 'rollup' | 'webpack'): P
}
if (bundler === 'webpack') {
const config = require(path.resolve('webpack.config.js'));
const config = require(path.resolve(cwd, 'webpack.config.js'));
validate_config(config, 'webpack');
return {

View File

@@ -1,10 +1,9 @@
import * as fs from 'fs';
import * as path from 'path';
import { locations } from '../config';
import { Page, PageComponent, ServerRoute, ManifestData } from '../interfaces';
import { posixify, reserved_words } from './utils';
import { posixify, reserved_words } from '../utils';
export default function create_manifest_data(cwd = locations.routes()): ManifestData {
export default function create_manifest_data(cwd: string): ManifestData {
// TODO remove in a future version
if (!fs.existsSync(cwd)) {
throw new Error(`As of Sapper 0.21, the routes/ directory should become src/routes/`);

View File

@@ -1,41 +1,56 @@
import * as fs from 'fs';
import * as path from 'path';
import glob from 'tiny-glob/sync.js';
import { posixify, stringify, write_if_changed } from './utils';
import { dev, locations } from '../config';
import { Page, PageComponent, ServerRoute, ManifestData } from '../interfaces';
import { posixify, stringify, walk, write_if_changed } from '../utils';
import { Page, PageComponent, ManifestData } from '../interfaces';
export function create_main_manifests({ bundler, manifest_data, dev_port }: {
export function create_main_manifests({
bundler,
manifest_data,
dev_port,
dev,
cwd,
src,
dest,
routes,
output
}: {
bundler: string,
manifest_data: ManifestData;
dev_port?: number;
dev: boolean;
cwd: string;
src: string;
dest: string;
routes: string;
output: string
}) {
const manifest_dir = path.resolve('__sapper__');
if (!fs.existsSync(manifest_dir)) fs.mkdirSync(manifest_dir);
if (!fs.existsSync(output)) fs.mkdirSync(output);
const path_to_routes = path.relative(manifest_dir, locations.routes());
const path_to_routes = path.relative(output, routes);
const client_manifest = generate_client(manifest_data, path_to_routes, bundler, dev_port);
const server_manifest = generate_server(manifest_data, path_to_routes);
const client_manifest = generate_client(manifest_data, path_to_routes, bundler, dev, dev_port);
const server_manifest = generate_server(manifest_data, path_to_routes, cwd, src, dest, dev);
write_if_changed(
`${manifest_dir}/_layout.html`,
`${output}/_layout.html`,
`<svelte:component this={child.component} {...child.props}/>`
);
write_if_changed(`${manifest_dir}/client.js`, client_manifest);
write_if_changed(`${manifest_dir}/server.js`, server_manifest);
write_if_changed(`${output}/client.js`, client_manifest);
write_if_changed(`${output}/server.js`, server_manifest);
}
export function create_serviceworker_manifest({ manifest_data, client_files }: {
export function create_serviceworker_manifest({ manifest_data, output, client_files, static_files }: {
manifest_data: ManifestData;
output: string;
client_files: string[];
static_files: string;
}) {
let files;
let files: string[];
// TODO remove in a future version
if (fs.existsSync(locations.static())) {
files = glob('**', { cwd: locations.static(), filesOnly: true });
if (fs.existsSync(static_files)) {
files = walk(static_files);
} else {
// TODO remove in a future version
if (fs.existsSync('assets')) {
throw new Error(`As of Sapper 0.21, the assets/ directory should become static/`);
}
@@ -55,13 +70,14 @@ export function create_serviceworker_manifest({ manifest_data, client_files }: {
export const routes = [\n\t${manifest_data.pages.map((r: Page) => `{ pattern: ${r.pattern} }`).join(',\n\t')}\n];
`.replace(/^\t\t/gm, '').trim();
write_if_changed(`__sapper__/service-worker.js`, code);
write_if_changed(`${output}/service-worker.js`, code);
}
function generate_client(
manifest_data: ManifestData,
path_to_routes: string,
bundler: string,
dev: boolean,
dev_port?: number
) {
const template_file = path.resolve(__dirname, '../templates/client.js');
@@ -120,7 +136,7 @@ function generate_client(
let footer = '';
if (dev()) {
if (dev) {
const sapper_dev_client = posixify(
path.resolve(__dirname, '../sapper-dev-client.js')
);
@@ -145,7 +161,11 @@ function generate_client(
function generate_server(
manifest_data: ManifestData,
path_to_routes: string
path_to_routes: string,
cwd: string,
src: string,
dest: string,
dev: boolean
) {
const template_file = path.resolve(__dirname, '../templates/server.js');
const template = fs.readFileSync(template_file, 'utf-8');
@@ -206,13 +226,13 @@ function generate_server(
error
};`.replace(/^\t\t/gm, '').trim();
const build_dir = path.relative(process.cwd(), locations.dest());
const src_dir = path.relative(process.cwd(), locations.src());
const build_dir = path.relative(cwd, dest);
const src_dir = path.relative(cwd, src);
return `// This file is generated by Sapper — do not edit it!\n` + template
.replace('__BUILD__DIR__', JSON.stringify(build_dir))
.replace('__SRC__DIR__', JSON.stringify(src_dir))
.replace('__DEV__', dev() ? 'true' : 'false')
.replace('__DEV__', dev ? 'true' : 'false')
.replace(/const manifest = __MANIFEST__;/, code);
}

View File

@@ -1,7 +1,6 @@
import * as fs from 'fs';
import { locations } from '../config';
export default function read_template(dir = locations.src()) {
export default function read_template(dir: string) {
try {
return fs.readFileSync(`${dir}/template.html`, 'utf-8');
} catch (err) {

View File

@@ -1,81 +0,0 @@
import * as fs from 'fs';
const previous_contents = new Map();
export function write_if_changed(file: string, code: string) {
if (code !== previous_contents.get(file)) {
previous_contents.set(file, code);
fs.writeFileSync(file, code);
fudge_mtime(file);
}
}
export function posixify(file: string) {
return file.replace(/[/\\]/g, '/');
}
export function stringify(string: string, includeQuotes: boolean = true) {
const quoted = JSON.stringify(string);
return includeQuotes ? quoted : quoted.slice(1, -1);
}
export function fudge_mtime(file: string) {
// need to fudge the mtime so that webpack doesn't go doolally
const { atime, mtime } = fs.statSync(file);
fs.utimesSync(
file,
new Date(atime.getTime() - 999999),
new Date(mtime.getTime() - 999999)
);
}
export const reserved_words = new Set([
'arguments',
'await',
'break',
'case',
'catch',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'else',
'enum',
'eval',
'export',
'extends',
'false',
'finally',
'for',
'function',
'if',
'implements',
'import',
'in',
'instanceof',
'interface',
'let',
'new',
'null',
'package',
'private',
'protected',
'public',
'return',
'static',
'super',
'switch',
'this',
'throw',
'true',
'try',
'typeof',
'var',
'void',
'while',
'with',
'yield',
]);

View File

@@ -1,3 +1,6 @@
import * as child_process from 'child_process';
import { CompileResult } from './core/create_compilers/interfaces';
export type Route = {
id: string;
handlers: {
@@ -52,4 +55,47 @@ export type ManifestData = {
components: PageComponent[];
pages: Page[];
server_routes: ServerRoute[];
};
};
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 = {};

View File

@@ -1,3 +1,6 @@
import * as fs from 'fs';
import * as path from 'path';
export function left_pad(str: string, len: number) {
while (str.length < len) str = ` ${str}`;
return str;
@@ -7,4 +10,110 @@ export function repeat(str: string, i: number) {
let result = '';
while (i--) result += str;
return result;
}
}
export function format_milliseconds(ms: number) {
if (ms < 1000) return `${ms}ms`;
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
const minutes = ~~(ms / 60000);
const seconds = Math.round((ms % 60000) / 1000);
return `${minutes}m${seconds < 10 ? '0' : ''}${seconds}s`;
}
export function elapsed(start: number) {
return format_milliseconds(Date.now() - start);
}
export function walk(cwd: string, dir = cwd, files: string[] = []) {
fs.readdirSync(dir).forEach(file => {
const resolved = path.resolve(dir, file);
if (fs.statSync(resolved).isDirectory()) {
walk(cwd, resolved, files);
} else {
files.push(posixify(path.relative(cwd, resolved)));
}
});
return files;
}
export function posixify(str: string) {
return str.replace(/\\/g, '/');
}
const previous_contents = new Map();
export function write_if_changed(file: string, code: string) {
if (code !== previous_contents.get(file)) {
previous_contents.set(file, code);
fs.writeFileSync(file, code);
fudge_mtime(file);
}
}
export function stringify(string: string, includeQuotes: boolean = true) {
const quoted = JSON.stringify(string);
return includeQuotes ? quoted : quoted.slice(1, -1);
}
export function fudge_mtime(file: string) {
// need to fudge the mtime so that webpack doesn't go doolally
const { atime, mtime } = fs.statSync(file);
fs.utimesSync(
file,
new Date(atime.getTime() - 999999),
new Date(mtime.getTime() - 999999)
);
}
export const reserved_words = new Set([
'arguments',
'await',
'break',
'case',
'catch',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'else',
'enum',
'eval',
'export',
'extends',
'false',
'finally',
'for',
'function',
'if',
'implements',
'import',
'in',
'instanceof',
'interface',
'let',
'new',
'null',
'package',
'private',
'protected',
'public',
'return',
'static',
'super',
'switch',
'this',
'throw',
'true',
'try',
'typeof',
'var',
'void',
'while',
'with',
'yield',
]);

View File

@@ -1,4 +1,3 @@
import * as path from 'path';
import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import * as http from 'http';
@@ -23,34 +22,11 @@ describe('basics', function() {
let goto: (href: string) => Promise<void>;
// hooks
before(() => {
return new Promise((fulfil, reject) => {
// TODO this is brittle. Make it unnecessary
process.chdir(__dirname);
process.env.NODE_ENV = 'production';
before(async () => {
await build({ cwd: __dirname });
// TODO this API isn't great. Rethink it
const emitter = build({
bundler: 'rollup'
}, {
src: path.join(__dirname, 'src'),
routes: path.join(__dirname, 'src/routes'),
dest: path.join(__dirname, '__sapper__/build')
});
emitter.on('error', reject);
emitter.on('done', async () => {
try {
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes, prefetch, goto } = await runner.start());
fulfil();
} catch (err) {
reject(err);
}
});
});
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes, prefetch, goto } = await runner.start());
});
after(() => runner.end());

View File

@@ -1,4 +1,3 @@
import * as path from 'path';
import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api';
@@ -17,34 +16,11 @@ describe('credentials', function() {
let prefetchRoutes: () => Promise<void>;
// hooks
before(() => {
return new Promise((fulfil, reject) => {
// TODO this is brittle. Make it unnecessary
process.chdir(__dirname);
process.env.NODE_ENV = 'production';
before(async () => {
await build({ cwd: __dirname });
// TODO this API isn't great. Rethink it
const emitter = build({
bundler: 'rollup'
}, {
src: path.join(__dirname, 'src'),
routes: path.join(__dirname, 'src/routes'),
dest: path.join(__dirname, '__sapper__/build')
});
emitter.on('error', reject);
emitter.on('done', async () => {
try {
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes } = await runner.start());
fulfil();
} catch (err) {
reject(err);
}
});
});
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes } = await runner.start());
});
after(() => runner.end());

View File

@@ -1,4 +1,3 @@
import * as path from 'path';
import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api';
@@ -17,34 +16,11 @@ describe('encoding', function() {
let prefetchRoutes: () => Promise<void>;
// hooks
before(() => {
return new Promise((fulfil, reject) => {
// TODO this is brittle. Make it unnecessary
process.chdir(__dirname);
process.env.NODE_ENV = 'production';
before(async () => {
await build({ cwd: __dirname });
// TODO this API isn't great. Rethink it
const emitter = build({
bundler: 'rollup'
}, {
src: path.join(__dirname, 'src'),
routes: path.join(__dirname, 'src/routes'),
dest: path.join(__dirname, '__sapper__/build')
});
emitter.on('error', reject);
emitter.on('done', async () => {
try {
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes } = await runner.start());
fulfil();
} catch (err) {
reject(err);
}
});
});
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes } = await runner.start());
});
after(() => runner.end());

View File

@@ -1,4 +1,3 @@
import * as path from 'path';
import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api';
@@ -17,34 +16,11 @@ describe('errors', function() {
let prefetchRoutes: () => Promise<void>;
// hooks
before(() => {
return new Promise((fulfil, reject) => {
// TODO this is brittle. Make it unnecessary
process.chdir(__dirname);
process.env.NODE_ENV = 'production';
before(async () => {
await build({ cwd: __dirname });
// TODO this API isn't great. Rethink it
const emitter = build({
bundler: 'rollup'
}, {
src: path.join(__dirname, 'src'),
routes: path.join(__dirname, 'src/routes'),
dest: path.join(__dirname, '__sapper__/build')
});
emitter.on('error', reject);
emitter.on('done', async () => {
try {
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes } = await runner.start());
fulfil();
} catch (err) {
reject(err);
}
});
});
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes } = await runner.start());
});
after(() => runner.end());

View File

@@ -1,70 +0,0 @@
import * as path from 'path';
import * as assert from 'assert';
import { walk } from '../../utils';
import * as api from '../../../api';
describe('export', function() {
this.timeout(10000);
// hooks
before(() => {
return new Promise((fulfil, reject) => {
// TODO this is brittle. Make it unnecessary
process.chdir(__dirname);
process.env.NODE_ENV = 'production';
// TODO this API isn't great. Rethink it
const builder = api.build({
bundler: 'rollup'
}, {
src: path.join(__dirname, 'src'),
routes: path.join(__dirname, 'src/routes'),
dest: path.join(__dirname, '__sapper__/build')
});
builder.on('error', reject);
builder.on('done', () => {
// TODO it'd be nice if build and export returned promises.
// not sure how best to combine promise and event emitter
const exporter = api.exporter({
build: '__sapper__/build',
dest: '__sapper__/export',
static: 'static',
basepath: '',
timeout: 5000
});
exporter.on('error', (err: Error) => {
console.error(err);
reject(err);
});
exporter.on('done', () => fulfil());
});
});
});
it('crawls a site', () => {
const files = walk('__sapper__/export');
const client_assets = files.filter(file => file.startsWith('client/'));
const non_client_assets = files.filter(file => !file.startsWith('client/')).sort();
assert.ok(client_assets.length > 0);
assert.deepEqual(non_client_assets, [
'blog.json',
'blog/bar.json',
'blog/bar/index.html',
'blog/baz.json',
'blog/baz/index.html',
'blog/foo.json',
'blog/foo/index.html',
'blog/index.html',
'global.css',
'index.html',
'service-worker.js'
]);
});
// TODO test timeout, basepath
});

38
test/apps/export/test.ts Normal file
View File

@@ -0,0 +1,38 @@
import * as assert from 'assert';
import { walk } from '../../utils';
import * as api from '../../../api';
describe('export', function() {
this.timeout(10000);
// hooks
before(async () => {
await api.build({ cwd: __dirname });
await api.export({ cwd: __dirname });
});
it('crawls a site', () => {
const files = walk(`${__dirname}/__sapper__/export`);
const client_assets = files.filter(file => file.startsWith('client/'));
const non_client_assets = files.filter(file => !file.startsWith('client/')).sort();
assert.ok(client_assets.length > 0);
assert.deepEqual(non_client_assets, [
'blog.json',
'blog/bar.json',
'blog/bar/index.html',
'blog/baz.json',
'blog/baz/index.html',
'blog/foo.json',
'blog/foo/index.html',
'blog/index.html',
'global.css',
'index.html',
'service-worker.js'
]);
});
// TODO test timeout, basepath
});

View File

@@ -1,4 +1,3 @@
import * as path from 'path';
import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api';
@@ -12,34 +11,11 @@ describe('ignore', function() {
let base: string;
// hooks
before(() => {
return new Promise((fulfil, reject) => {
// TODO this is brittle. Make it unnecessary
process.chdir(__dirname);
process.env.NODE_ENV = 'production';
before(async () => {
await build({ cwd: __dirname });
// TODO this API isn't great. Rethink it
const emitter = build({
bundler: 'rollup'
}, {
src: path.join(__dirname, 'src'),
routes: path.join(__dirname, 'src/routes'),
dest: path.join(__dirname, '__sapper__/build')
});
emitter.on('error', reject);
emitter.on('done', async () => {
try {
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page } = await runner.start());
fulfil();
} catch (err) {
reject(err);
}
});
});
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page } = await runner.start());
});
after(() => runner.end());

View File

@@ -1,4 +1,3 @@
import * as path from 'path';
import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api';
@@ -16,34 +15,11 @@ describe('layout', function() {
let start: () => Promise<void>;
// hooks
before(() => {
return new Promise((fulfil, reject) => {
// TODO this is brittle. Make it unnecessary
process.chdir(__dirname);
process.env.NODE_ENV = 'production';
before(async () => {
await build({ cwd: __dirname });
// TODO this API isn't great. Rethink it
const emitter = build({
bundler: 'rollup'
}, {
src: path.join(__dirname, 'src'),
routes: path.join(__dirname, 'src/routes'),
dest: path.join(__dirname, '__sapper__/build')
});
emitter.on('error', reject);
emitter.on('done', async () => {
try {
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start } = await runner.start());
fulfil();
} catch (err) {
reject(err);
}
});
});
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start } = await runner.start());
});
after(() => runner.end());

View File

@@ -19,34 +19,11 @@ describe('preloading', function() {
let prefetchRoutes: () => Promise<void>;
// hooks
before(() => {
return new Promise((fulfil, reject) => {
// TODO this is brittle. Make it unnecessary
process.chdir(__dirname);
process.env.NODE_ENV = 'production';
before(async () => {
await build({ cwd: __dirname });
// TODO this API isn't great. Rethink it
const emitter = build({
bundler: 'rollup'
}, {
src: path.join(__dirname, 'src'),
routes: path.join(__dirname, 'src/routes'),
dest: path.join(__dirname, '__sapper__/build')
});
emitter.on('error', reject);
emitter.on('done', async () => {
try {
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes } = await runner.start());
fulfil();
} catch (err) {
reject(err);
}
});
});
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes } = await runner.start());
});
after(() => runner.end());

View File

@@ -1,4 +1,3 @@
import * as path from 'path';
import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api';
@@ -17,34 +16,11 @@ describe('redirects', function() {
let prefetchRoutes: () => Promise<void>;
// hooks
before(() => {
return new Promise((fulfil, reject) => {
// TODO this is brittle. Make it unnecessary
process.chdir(__dirname);
process.env.NODE_ENV = 'production';
before(async () => {
await build({ cwd: __dirname });
// TODO this API isn't great. Rethink it
const emitter = build({
bundler: 'rollup'
}, {
src: path.join(__dirname, 'src'),
routes: path.join(__dirname, 'src/routes'),
dest: path.join(__dirname, '__sapper__/build')
});
emitter.on('error', reject);
emitter.on('done', async () => {
try {
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes } = await runner.start());
fulfil();
} catch (err) {
reject(err);
}
});
});
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes } = await runner.start());
});
after(() => runner.end());

View File

@@ -1,5 +1,4 @@
import * as assert from 'assert';
import * as path from 'path';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api';
import { AppRunner } from '../AppRunner';
@@ -17,34 +16,11 @@ describe('scroll', function() {
let prefetchRoutes: () => Promise<void>;
// hooks
before(() => {
return new Promise((fulfil, reject) => {
// TODO this is brittle. Make it unnecessary
process.chdir(__dirname);
process.env.NODE_ENV = 'production';
before(async () => {
await build({ cwd: __dirname });
// TODO this API isn't great. Rethink it
const emitter = build({
bundler: 'rollup'
}, {
src: path.join(__dirname, 'src'),
routes: path.join(__dirname, 'src/routes'),
dest: path.join(__dirname, '__sapper__/build')
});
emitter.on('error', reject);
emitter.on('done', async () => {
try {
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes } = await runner.start());
fulfil();
} catch (err) {
reject(err);
}
});
});
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start, prefetchRoutes } = await runner.start());
});
after(() => runner.end());

View File

@@ -1,60 +0,0 @@
import * as path from 'path';
import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api';
import { AppRunner } from '../AppRunner';
describe('store', function() {
this.timeout(10000);
let runner: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
// hooks
before(() => {
return new Promise((fulfil, reject) => {
// TODO this is brittle. Make it unnecessary
process.chdir(__dirname);
process.env.NODE_ENV = 'production';
// TODO this API isn't great. Rethink it
const emitter = build({
bundler: 'rollup'
}, {
src: path.join(__dirname, 'src'),
routes: path.join(__dirname, 'src/routes'),
dest: path.join(__dirname, '__sapper__/build')
});
emitter.on('error', reject);
emitter.on('done', async () => {
try {
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start } = await runner.start());
fulfil();
} catch (err) {
reject(err);
}
});
});
});
after(() => runner.end());
const title = () => page.$eval('h1', node => node.textContent);
it('renders store props', async () => {
await page.goto(`${base}/store`);
assert.equal(await title(), 'hello world');
await start();
assert.equal(await title(), 'hello world');
});
});

36
test/apps/store/test.ts Normal file
View File

@@ -0,0 +1,36 @@
import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import { build } from '../../../api';
import { AppRunner } from '../AppRunner';
describe('store', function() {
this.timeout(10000);
let runner: AppRunner;
let page: puppeteer.Page;
let base: string;
// helpers
let start: () => Promise<void>;
// hooks
before(async () => {
await build({ cwd: __dirname });
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page, start } = await runner.start());
});
after(() => runner.end());
const title = () => page.$eval('h1', node => node.textContent);
it('renders store props', async () => {
await page.goto(`${base}/store`);
assert.equal(await title(), 'hello world');
await start();
assert.equal(await title(), 'hello world');
});
});

View File

@@ -1,4 +1,3 @@
import * as path from 'path';
import * as assert from 'assert';
import * as puppeteer from 'puppeteer';
import * as api from '../../../api';
@@ -13,50 +12,16 @@ describe('with-basepath', function() {
let base: string;
// hooks
before(() => {
return new Promise((fulfil, reject) => {
// TODO this is brittle. Make it unnecessary
process.chdir(__dirname);
process.env.NODE_ENV = 'production';
before(async () => {
await api.build({ cwd: __dirname });
// TODO this API isn't great. Rethink it
const builder = api.build({
bundler: 'rollup'
}, {
src: path.join(__dirname, 'src'),
routes: path.join(__dirname, 'src/routes'),
dest: path.join(__dirname, '__sapper__/build')
});
builder.on('error', reject);
builder.on('done', () => {
// TODO it'd be nice if build and export returned promises.
// not sure how best to combine promise and event emitter
const exporter = api.exporter({
build: '__sapper__/build',
dest: '__sapper__/export',
static: 'static',
basepath: 'custom-basepath',
timeout: 5000
});
exporter.on('error', (err: Error) => {
console.error(err);
reject(err);
});
exporter.on('done', async () => {
try {
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page } = await runner.start());
fulfil();
} catch (err) {
reject(err);
}
});
});
await api.export({
cwd: __dirname,
basepath: 'custom-basepath'
});
runner = new AppRunner(__dirname, '__sapper__/build/server/server.js');
({ base, page } = await runner.start());
});
after(() => runner.end());
@@ -81,7 +46,7 @@ describe('with-basepath', function() {
});
it('crawls an exported site with basepath', () => {
const files = walk('__sapper__/export');
const files = walk(`${__dirname}/__sapper__/export`);
const client_assets = files.filter(file => file.startsWith('custom-basepath/client/'));
const non_client_assets = files.filter(file => !file.startsWith('custom-basepath/client/')).sort();