diff --git a/rollup.config.js b/rollup.config.js index 51f1499..17c3bc7 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -32,6 +32,7 @@ export default [ `src/cli.ts`, `src/core.ts`, `src/middleware.ts`, + `src/rollup.ts`, `src/webpack.ts` ], output: { diff --git a/rollup.js b/rollup.js new file mode 100644 index 0000000..b37e9ab --- /dev/null +++ b/rollup.js @@ -0,0 +1 @@ +module.exports = require('./dist/rollup.ts.js'); \ No newline at end of file diff --git a/sapper-dev-client.js b/sapper-dev-client.js index b717a25..e5c0035 100644 --- a/sapper-dev-client.js +++ b/sapper-dev-client.js @@ -1,6 +1,8 @@ let source; function check() { + if (typeof module === 'undefined') return; + if (module.hot.status() === 'idle') { module.hot.check(true).then(modules => { console.log(`[SAPPER] applied HMR update`); diff --git a/src/api/build.ts b/src/api/build.ts index 0dce116..5f43d18 100644 --- a/src/api/build.ts +++ b/src/api/build.ts @@ -55,21 +55,20 @@ async function execute(emitter: EventEmitter, { const { client, server, serviceworker } = create_compilers({ webpack, rollup }); - const client_stats = await client.compile(); + const client_result = await client.compile(); emitter.emit('build', { type: 'client', // TODO duration/warnings - webpack_stats: client_stats + result: client_result }); - const client_info = client_stats.toJson(); - fs.writeFileSync(path.join(dest, 'client_assets.json'), JSON.stringify(client_info.assetsByChunkName)); + fs.writeFileSync(path.join(dest, 'client_assets.json'), JSON.stringify(client_result.assetsByChunkName)); const server_stats = await server.compile(); emitter.emit('build', { type: 'server', // TODO duration/warnings - webpack_stats: server_stats + result: server_stats }); let serviceworker_stats; @@ -77,7 +76,7 @@ async function execute(emitter: EventEmitter, { if (serviceworker) { create_serviceworker_manifest({ routes: route_objects, - client_files: client_stats.toJson().assets.map((chunk: { name: string }) => `client/${chunk.name}`) + client_files: client_result.assets.map((chunk: { name: string }) => `client/${chunk.name}`) }); serviceworker_stats = await serviceworker.compile(); @@ -85,7 +84,7 @@ async function execute(emitter: EventEmitter, { emitter.emit('build', { type: 'serviceworker', // TODO duration/warnings - webpack_stats: serviceworker_stats + result: serviceworker_stats }); } } \ No newline at end of file diff --git a/src/api/dev.ts b/src/api/dev.ts index 1352744..3b38521 100644 --- a/src/api/dev.ts +++ b/src/api/dev.ts @@ -5,11 +5,10 @@ import * as child_process from 'child_process'; import * as ports from 'port-authority'; import mkdirp from 'mkdirp'; import rimraf from 'rimraf'; -import format_messages from 'webpack-format-messages'; import { locations } from '../config'; import { EventEmitter } from 'events'; import { create_routes, create_main_manifests, create_compilers, create_serviceworker_manifest } from '../core'; -import { Compiler, Compilers } from '../core/create_compilers'; +import { Compiler, Compilers, CompileResult, CompileError } from '../core/create_compilers'; import Deferred from './utils/Deferred'; import * as events from './interfaces'; @@ -49,17 +48,19 @@ class Watcher extends EventEmitter { dest = locations.dest(), routes = locations.routes(), webpack = 'webpack', + rollup = 'rollup', port = +process.env.PORT }: { app: string, dest: string, routes: string, webpack: string, + rollup: string, port: number }) { super(); - this.dirs = { app, dest, routes, webpack }; + this.dirs = { app, dest, routes, webpack, rollup }; this.port = port; this.closed = false; @@ -181,7 +182,7 @@ class Watcher extends EventEmitter { this.deferreds.server = new Deferred(); }, - result: info => { + handle_result: (result: CompileResult) => { this.deferreds.client.promise.then(() => { const restart = () => { log = ''; @@ -263,11 +264,11 @@ class Watcher extends EventEmitter { // quite difficult }, - result: info => { - fs.writeFileSync(path.join(dest, 'client_assets.json'), JSON.stringify(info.assetsByChunkName, null, ' ')); + handle_result: (result: CompileResult) => { + fs.writeFileSync(path.join(dest, 'client_assets.json'), JSON.stringify(result.assetsByChunkName, null, ' ')); this.deferreds.client.fulfil(); - const client_files = info.assets.map((chunk: { name: string }) => `client/${chunk.name}`); + const client_files = result.assets.map((chunk: { name: string }) => `client/${chunk.name}`); create_serviceworker_manifest({ routes: create_routes(), @@ -285,11 +286,7 @@ class Watcher extends EventEmitter { watch_serviceworker = noop; this.watch(compilers.serviceworker, { - name: 'service worker', - - result: info => { - fs.writeFileSync(path.join(dest, 'serviceworker_info.json'), JSON.stringify(info, null, ' ')); - } + name: 'service worker' }); } : noop; @@ -336,80 +333,34 @@ class Watcher extends EventEmitter { } } - watch(compiler: Compiler, { name, invalid = noop, result }: { + watch(compiler: Compiler, { name, invalid = noop, handle_result = noop }: { name: string, invalid?: (filename: string) => void; - result: (stats: any) => void; + handle_result?: (result: CompileResult) => void; }) { compiler.oninvalid(invalid); - compiler.watch((err: Error, stats: any) => { + compiler.watch((err?: Error, result?: CompileResult) => { if (err) { this.emit('error', { type: name, message: err.message }); } else { - const messages = format_messages(stats); - const info = stats.toJson(); - this.emit('build', { type: name, - duration: info.time, - - errors: messages.errors.map((message: string) => { - const duplicate = this.current_build.unique_errors.has(message); - this.current_build.unique_errors.add(message); - - return mungeWebpackError(message, duplicate); - }), - - warnings: messages.warnings.map((message: string) => { - const duplicate = this.current_build.unique_warnings.has(message); - this.current_build.unique_warnings.add(message); - - return mungeWebpackError(message, duplicate); - }), + duration: result.duration, + errors: result.errors, + warnings: result.warnings }); - result(info); + handle_result(result); } }); } } -const locPattern = /\((\d+):(\d+)\)$/; - -function mungeWebpackError(message: string, duplicate: boolean) { - // TODO this is all a bit rube goldberg... - const lines = message.split('\n'); - - const file = lines.shift() - .replace('', '') // careful — there is a special character at the beginning of this string - .replace('', '') - .replace('./', ''); - - let line = null; - let column = null; - - const match = locPattern.exec(lines[0]); - if (match) { - lines[0] = lines[0].replace(locPattern, ''); - line = +match[1]; - column = +match[2]; - } - - return { - file, - line, - column, - message: lines.join('\n'), - originalMessage: message, - duplicate - }; -} - const INTERVAL = 10000; class DevServer { @@ -466,7 +417,7 @@ function noop() {} function watch_dir( dir: string, - filter: ({ path, stats }: { path: string, stats: fs.Stats }) => boolean, + filter: ({ path, stats }: { path: string, stats: fs.CompileResult }) => boolean, callback: () => void ) { let watch; diff --git a/src/api/interfaces.ts b/src/api/interfaces.ts index 381877a..0be020d 100644 --- a/src/api/interfaces.ts +++ b/src/api/interfaces.ts @@ -1,4 +1,5 @@ import * as child_process from 'child_process'; +import { CompileResult } from '../core/create_compilers'; export type ReadyEvent = { port: number; @@ -29,7 +30,7 @@ export type BuildEvent = { errors: Array<{ file: string, message: string, duplicate: boolean }>; warnings: Array<{ file: string, message: string, duplicate: boolean }>; duration: number; - webpack_stats: any; + result: CompileResult; } export type FileEvent = { diff --git a/src/cli/build.ts b/src/cli/build.ts index 53f3002..c5844f6 100644 --- a/src/cli/build.ts +++ b/src/cli/build.ts @@ -15,7 +15,7 @@ export function build() { emitter.on('build', event => { console.log(colors.inverse(`\nbuilt ${event.type}`)); - console.log(event.webpack_stats.toString({ colors: true })); + console.log(event.result.print()); }); emitter.on('error', event => { diff --git a/src/core/create_compilers.ts b/src/core/create_compilers.ts index a6794b9..1ee96c5 100644 --- a/src/core/create_compilers.ts +++ b/src/core/create_compilers.ts @@ -1,10 +1,167 @@ import * as fs from 'fs'; import * as path from 'path'; +import { locations } from '../config'; import relative from 'require-relative'; let r: any; let wp: any; +export class CompileError { + file: string; + line: number; + column: number; + message: string; +} + +export class CompileResult { + duration: number; + errors: CompileError[]; + warnings: CompileError[]; + assets: Array<{ name: string }>; + assetsByChunkName: Record; +} + +class RollupResult extends CompileResult { + constructor(duration: number, stats: any) { + super(); + + this.duration = duration; + + this.errors = []; + this.warnings = []; + + // TODO + this.assets = []; + + this.assetsByChunkName = { + // TODO need to hash these filenames and + // expose the info in the Rollup output + main: `client.js` + }; + } + + print() { + return 'TODO summarise build'; + } +} + +class WebpackResult extends CompileResult { + stats: any; + + constructor(stats: any) { + super(); + + this.stats = stats; + + const info = stats.toJson(); + + // TODO use import() + const format_messages = require('webpack-format-messages'); + const messages = format_messages(stats); + + this.errors = messages.errors.map(mungeWebpackError); + this.warnings = messages.warnings.map(mungeWebpackError); + + this.duration = info.time; + + this.assets = info.assets.map((chunk: { name: string }) => `client/${chunk.name}`); + this.assetsByChunkName = info.assetsByChunkName; + } + + print() { + return this.stats.toString({ colors: true }); + } +} + +export class RollupCompiler { + _: Promise; + _oninvalid: (filename: string) => void; + _start: number; + + constructor(config: any) { + this._ = this.get_config(path.resolve(config)); + } + + async get_config(input: string) { + const bundle = await r.rollup({ + input, + external: (id: string) => { + return (id[0] !== '.' && !path.isAbsolute(id)) || id.slice(-5, id.length) === '.json'; + } + }); + + const { code } = await bundle.generate({ format: 'cjs' }); + + // temporarily override require + const defaultLoader = require.extensions['.js']; + require.extensions['.js'] = (module: any, filename: string) => { + if (filename === input) { + module._compile(code, filename); + } else { + defaultLoader(module, filename); + } + }; + + const mod: any = require(input); + delete require.cache[input]; + + return mod; + } + + oninvalid(cb: (filename: string) => void) { + this._oninvalid = cb; + } + + async compile(): Promise { + const config = await this._; + + const start = Date.now(); + + const bundle = await r.rollup(config); + await bundle.write(config.output); + + return new RollupResult(Date.now() - start, bundle); + } + + async watch(cb: (err?: Error, stats?: any) => void) { + const config = await this._; + + const watcher = r.watch(config); + + watcher.on('event', (event: any) => { + switch (event.code) { + case 'FATAL': + // TODO kill the process? + cb(event.error); + break; + + case 'ERROR': + // TODO print warnings as well? + cb(event.error); + break; + + case 'START': + case 'END': + // TODO is there anything to do with this info? + break; + + case 'BUNDLE_START': + this._start = Date.now(); + // TODO figure out which file changed + this._oninvalid('[TODO] unknown file'); + break; + + case 'BUNDLE_END': + cb(null, new RollupResult(Date.now() - this._start, event.result)); + break; + + default: + console.log(`Unexpected event ${event.code}`); + } + }); + } +} + export class WebpackCompiler { _: any; @@ -16,7 +173,7 @@ export class WebpackCompiler { this._.hooks.invalid.tap('sapper', cb); } - compile() { + compile(): Promise { return new Promise((fulfil, reject) => { this._.run((err: Error, stats: any) => { if (err) { @@ -24,41 +181,26 @@ export class WebpackCompiler { process.exit(1); } - if (stats.hasErrors()) { - console.error(stats.toString({ colors: true })); + const result = new WebpackResult(stats); + + if (result.errors.length) { + // TODO print errors + // console.error(stats.toString({ colors: true })); reject(new Error(`Encountered errors while building app`)); } else { - fulfil(stats); + fulfil(result); } }); }); } - watch(cb: (err: Error, stats: any) => void) { - this._.watch({}, cb); - } -} - -export class RollupCompiler { - constructor(config: any) { - - } - - oninvalid(cb: (filename: string) => void) { - - } - - compile() { - return new Promise((fulfil, reject) => { - + watch(cb: (err?: Error, stats?: any) => void) { + this._.watch({}, (err?: Error, stats?: any) => { + cb(err, stats && new WebpackResult(stats)); }); } - - watch(cb: (err: Error, stats: any) => void) { - - } } export type Compiler = RollupCompiler | WebpackCompiler; @@ -95,4 +237,34 @@ export default function create_compilers({ webpack, rollup }: { webpack: string, } throw new Error(`Could not find config files for rollup or webpack`); +} + +const locPattern = /\((\d+):(\d+)\)$/; + +function mungeWebpackError(message: string) { + // TODO this is all a bit rube goldberg... + const lines = message.split('\n'); + + const file = lines.shift() + .replace('', '') // careful — there is a special character at the beginning of this string + .replace('', '') + .replace('./', ''); + + let line = null; + let column = null; + + const match = locPattern.exec(lines[0]); + if (match) { + lines[0] = lines[0].replace(locPattern, ''); + line = +match[1]; + column = +match[2]; + } + + return { + file, + line, + column, + message: lines.join('\n'), + originalMessage: message + }; } \ No newline at end of file diff --git a/src/core/create_manifests.ts b/src/core/create_manifests.ts index ea950e3..da760a0 100644 --- a/src/core/create_manifests.ts +++ b/src/core/create_manifests.ts @@ -105,11 +105,9 @@ function generate_client( code += ` - if (module.hot) { - import('${sapper_dev_client}').then(client => { - client.connect(${dev_port}); - }); - }`.replace(/^\t{3}/gm, ''); + import('${sapper_dev_client}').then(client => { + client.connect(${dev_port}); + });`.replace(/^\t{3}/gm, ''); } return code; diff --git a/src/middleware.ts b/src/middleware.ts index cd3bf94..b031af0 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -320,7 +320,7 @@ function get_page_handler( } const link = preloaded_chunks - .filter(file => !file.match(/\.map$/)) + .filter(file => file && !file.match(/\.map$/)) .map(file => `<${req.baseUrl}/client/${file}>;rel="preload";as="script"`) .join(', '); @@ -484,7 +484,7 @@ function get_page_handler( let scripts = [] .concat(chunks.main) // chunks main might be an array. it might not! thanks, webpack .filter(file => !file.match(/\.map$/)) - .map(file => ``) + .map(file => ``) .join(''); let inline_script = `__SAPPER__={${[ diff --git a/src/rollup.ts b/src/rollup.ts new file mode 100644 index 0000000..77d41fd --- /dev/null +++ b/src/rollup.ts @@ -0,0 +1,44 @@ +import { locations, dev } from './config'; + +export default { + dev: dev(), + + client: { + input: () => { + return `${locations.app()}/client.js` + }, + + output: () => { + return { + dir: `${locations.dest()}/client`, + format: 'esm' + }; + } + }, + + server: { + input: () => { + return `${locations.app()}/server.js` + }, + + output: () => { + return { + dir: locations.dest(), + format: 'cjs' + }; + } + }, + + serviceworker: { + input: () => { + return `${locations.app()}/service-worker.js`; + }, + + output: () => { + return { + dir: locations.dest(), + format: 'iife' + } + } + } +}; \ No newline at end of file