From 52f40f9e63dab19ad11f5073b2446b2632c85179 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 8 Oct 2018 19:21:15 -0400 Subject: [PATCH] Improve internal API --- mocha.opts | 2 +- package-lock.json | 15 -- package.json | 2 - rollup.config.js | 4 +- src/api.ts | 10 +- src/api/build.ts | 98 +++++---- src/api/dev.ts | 116 +++++----- src/api/export.ts | 73 +++---- src/api/find_page.ts | 3 +- src/api/interfaces.ts | 45 ---- src/api/utils/noop.ts | 1 + src/{cli => api}/utils/validate_bundler.ts | 0 src/cli.ts | 200 +++++++++++++++--- src/cli/build.ts | 54 ----- src/cli/dev.ts | 80 ------- src/cli/export.ts | 51 ----- src/cli/start.ts | 39 ---- src/cli/upgrade.ts | 53 ----- src/config.ts | 11 - src/config/env.ts | 7 + src/{ => config}/rollup.ts | 20 +- src/{ => config}/webpack.ts | 16 +- src/core/create_compilers/RollupCompiler.ts | 6 +- src/core/create_compilers/RollupResult.ts | 2 +- src/core/create_compilers/extract_css.ts | 2 +- src/core/create_compilers/index.ts | 17 +- src/core/create_manifest_data.ts | 5 +- src/core/create_manifests.ts | 68 +++--- src/core/read_template.ts | 3 +- src/core/utils.ts | 81 ------- src/interfaces.ts | 48 ++++- src/utils.ts | 111 +++++++++- test/apps/basics/{__test__.ts => test.ts} | 32 +-- .../apps/credentials/{__test__.ts => test.ts} | 32 +-- test/apps/encoding/{__test__.ts => test.ts} | 32 +-- test/apps/errors/{__test__.ts => test.ts} | 32 +-- test/apps/export/__test__.ts | 70 ------ test/apps/export/test.ts | 38 ++++ test/apps/ignore/{__test__.ts => test.ts} | 32 +-- test/apps/layout/{__test__.ts => test.ts} | 32 +-- test/apps/preloading/{__test__.ts => test.ts} | 31 +-- test/apps/redirects/{__test__.ts => test.ts} | 32 +-- test/apps/scroll/{__test__.ts => test.ts} | 32 +-- test/apps/store/__test__.ts | 60 ------ test/apps/store/test.ts | 36 ++++ .../with-basepath/{__test__.ts => test.ts} | 53 +---- 46 files changed, 696 insertions(+), 1091 deletions(-) delete mode 100644 src/api/interfaces.ts create mode 100644 src/api/utils/noop.ts rename src/{cli => api}/utils/validate_bundler.ts (100%) delete mode 100644 src/cli/build.ts delete mode 100644 src/cli/dev.ts delete mode 100644 src/cli/export.ts delete mode 100644 src/cli/start.ts delete mode 100644 src/cli/upgrade.ts delete mode 100644 src/config.ts create mode 100644 src/config/env.ts rename src/{ => config}/rollup.ts (57%) rename src/{ => config}/webpack.ts (66%) delete mode 100644 src/core/utils.ts rename test/apps/basics/{__test__.ts => test.ts} (86%) rename test/apps/credentials/{__test__.ts => test.ts} (62%) rename test/apps/encoding/{__test__.ts => test.ts} (66%) rename test/apps/errors/{__test__.ts => test.ts} (74%) delete mode 100644 test/apps/export/__test__.ts create mode 100644 test/apps/export/test.ts rename test/apps/ignore/{__test__.ts => test.ts} (61%) rename test/apps/layout/{__test__.ts => test.ts} (58%) rename test/apps/preloading/{__test__.ts => test.ts} (74%) rename test/apps/redirects/{__test__.ts => test.ts} (68%) rename test/apps/scroll/{__test__.ts => test.ts} (63%) delete mode 100644 test/apps/store/__test__.ts create mode 100644 test/apps/store/test.ts rename test/apps/with-basepath/{__test__.ts => test.ts} (50%) diff --git a/mocha.opts b/mocha.opts index e793278..c769c5c 100644 --- a/mocha.opts +++ b/mocha.opts @@ -2,4 +2,4 @@ --require ts-node/register --recursive test/unit/*/*.ts -test/apps/*/__test__.ts \ No newline at end of file +test/apps/*/test.ts \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8a86dca..c519e27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 528ff2a..6318672 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/rollup.config.js b/rollup.config.js index 8fd7c4b..7e13cd6 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -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', diff --git a/src/api.ts b/src/api.ts index b8cee72..d90cdfd 100644 --- a/src/api.ts +++ b/src/api.ts @@ -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 }; \ No newline at end of file +export { dev } from './api/dev'; +export { build } from './api/build'; +export { export } from './api/export'; +export { find_page } from './api/find_page'; \ No newline at end of file diff --git a/src/api/build.ts b/src/api/build.ts index c9d93f4..5fd251c 100644 --- a/src/api/build.ts +++ b/src/api/build.ts @@ -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', {}); // TODO do we need to pass back any info? - }, - error => { - emitter.emit('error', { - 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', { + 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', { + 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', { + 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', { + oncompile({ type: 'serviceworker', - // TODO duration/warnings result: serviceworker_stats }); } diff --git a/src/api/dev.ts b/src/api/dev.ts index 37833ed..405a727 100644 --- a/src/api/dev.ts +++ b/src/api/dev.ts @@ -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 `); 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', { + this.emit('fatal', { 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', { + this.emit('fatal', { 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', { + this.emit('error', { 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', { + this.emit('fatal', { message: `Server crashed`, log }); @@ -215,7 +231,7 @@ class Watcher extends EventEmitter { ports.wait(this.port) .then((() => { - this.emit('ready', { + this.emit('ready', { port: this.port, process: this.proc }); @@ -233,7 +249,7 @@ class Watcher extends EventEmitter { .catch(err => { if (this.crashed) return; - this.emit('fatal', { + this.emit('fatal', { 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', { + this.emit('invalid', { 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', { + this.emit('error', { 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(); }); diff --git a/src/api/export.ts b/src/api/export.ts index 9b9fa46..86bf804 100644 --- a/src/api/export.ts +++ b/src/api/export.ts @@ -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', {}); // TODO do we need to pass back any info? - }, - error => { - emitter.emit('error', { - 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', { + 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(); diff --git a/src/api/find_page.ts b/src/api/find_page.ts index e986176..6f7cac9 100644 --- a/src/api/find_page.ts +++ b/src/api/find_page.ts @@ -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) { diff --git a/src/api/interfaces.ts b/src/api/interfaces.ts deleted file mode 100644 index 34d8ded..0000000 --- a/src/api/interfaces.ts +++ /dev/null @@ -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 = {}; \ No newline at end of file diff --git a/src/api/utils/noop.ts b/src/api/utils/noop.ts new file mode 100644 index 0000000..628cbae --- /dev/null +++ b/src/api/utils/noop.ts @@ -0,0 +1 @@ +export function noop() {} \ No newline at end of file diff --git a/src/cli/utils/validate_bundler.ts b/src/api/utils/validate_bundler.ts similarity index 100% rename from src/cli/utils/validate_bundler.ts rename to src/api/utils/validate_bundler.ts diff --git a/src/cli.ts b/src/cli.ts index e44e5d3..d5bb287 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -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()); + } + }); +} \ No newline at end of file diff --git a/src/cli/build.ts b/src/cli/build.ts deleted file mode 100644 index 20051bb..0000000 --- a/src/cli/build.ts +++ /dev/null @@ -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); - } - }); -} \ No newline at end of file diff --git a/src/cli/dev.ts b/src/cli/dev.ts deleted file mode 100644 index ecaa4a3..0000000 --- a/src/cli/dev.ts +++ /dev/null @@ -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); - } -} \ No newline at end of file diff --git a/src/cli/export.ts b/src/cli/export.ts deleted file mode 100644 index 214228e..0000000 --- a/src/cli/export.ts +++ /dev/null @@ -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); - } - }); -} \ No newline at end of file diff --git a/src/cli/start.ts b/src/cli/start.ts deleted file mode 100644 index ba658ae..0000000 --- a/src/cli/start.ts +++ /dev/null @@ -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}`); -} diff --git a/src/cli/upgrade.ts b/src/cli/upgrade.ts deleted file mode 100644 index 7d8b16b..0000000 --- a/src/cli/upgrade.ts +++ /dev/null @@ -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 = /