UNPKG

awesome-typescript-loader

Version:
432 lines (356 loc) 11.1 kB
import * as path from 'path'; import * as fs from 'fs'; import * as _ from 'lodash'; import * as child from 'child_process'; import * as webpack from 'webpack'; import { exec as shellExec } from 'shelljs'; import { LoaderConfig } from '../interfaces'; require('source-map-support').install(); import { expect } from 'chai'; export { expect }; const BPromise = require('bluebird'); const mkdirp = BPromise.promisify(require('mkdirp')); // const rimraf = BPromise.promisify(require('rimraf')); const readFile = BPromise.promisify(fs.readFile); const writeFile = BPromise.promisify(fs.writeFile); export const defaultOutputDir = path.join(process.cwd(), '.test'); export const defaultFixturesDir = path.join(process.cwd(), 'fixtures'); export interface ConfigOptions { loaderQuery?: LoaderConfig; watch?: boolean; include?: (string | RegExp)[]; exclude?: (string | RegExp)[]; } const TEST_DIR = path.join(process.cwd(), '.test'); const SRC_DIR = './src'; const OUT_DIR = './out'; const WEBPACK = path.join( path.dirname( path.dirname( require.resolve('webpack'))), 'bin', 'webpack.js'); mkdirp.sync(TEST_DIR); const LOADER = path.join(process.cwd(), 'index.js'); export function entry(file: string) { return config => { config.entry.index = path.join(process.cwd(), SRC_DIR, file); }; } export function query(q: any) { return config => { _.merge( config.module.loaders.find(loader => loader.loader === LOADER).query, q ); }; } export function webpackConfig(...enchance: any[]): webpack.Configuration { const config = { entry: { index: path.join(process.cwd(), SRC_DIR, 'index.ts') }, output: { path: path.join(process.cwd(), OUT_DIR), filename: '[name].js' }, resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'], }, module: { loaders: [ { test: /\.(tsx?|jsx?)/, loader: LOADER, include: [ path.join(process.cwd(), SRC_DIR) ], query: { silent: true } } ] } }; enchance.forEach(e => e(config)); return config; } export interface Output { type: 'stderr' | 'stdout'; data: string; } export type OutputMatcher = (o: Output) => boolean; export class Exec { process: child.ChildProcess; watchers: { resolve: any, reject: any, matchers: OutputMatcher[], }[] = []; exitCode: number | null; private _strictOutput = false; close() { this.process.kill(); } strictOutput() { this._strictOutput = true; } invoke({stdout, stderr}) { this.watchers = this.watchers.filter(watcher => { const output: Output = { type: stdout ? 'stdout' : 'stderr', data: stdout || stderr }; const index = watcher.matchers.findIndex(m => m(output)); if (this._strictOutput && index === -1) { watcher.reject(new Error(`Unexpected ${output.type}:\n${output.data}`)); return false; } watcher.matchers.splice(index, 1); if (watcher.matchers.length === 0) { watcher.resolve(); return false; } else { return true; } }); } wait(...matchers: OutputMatcher[]): Promise<any> { return new Promise((resolve, reject) => { const watcher = { resolve, reject, matchers, }; this.watchers.push(watcher); }); } alive(): Promise<any> { return new Promise((resolve, reject) => { if (this.exitCode != null) { resolve(this.exitCode); } else { this.process.on('exit', resolve); } }); } } export type Test = string | (string | [boolean, string])[] | RegExp | ((str: string) => boolean); export function streamTest(stream = 'stdout', test: Test) { let matcher: (str: string) => boolean; if (typeof test === 'string') { matcher = (o: string) => o.indexOf(test) !== -1; } else if (Array.isArray(test)) { matcher = (o: string) => test.every(test => { if (typeof test === 'string') { return o.indexOf(test) !== -1; } else { const [flag, str] = test; if (flag) { return o.indexOf(str) !== -1; } else { return o.indexOf(str) === -1; } } }); } else if (test instanceof RegExp) { matcher => (o: string) => test.test(o); } else { matcher = test; } return (o: Output) => (o.type === stream) && matcher(o.data); } export const stdout = (test: Test) => streamTest('stdout', test); export const stderr = (test: Test) => streamTest('stderr', test); export function execWebpack(args?: string[]) { return execNode(WEBPACK, args); } export function execNode(command: string, args: string[] = []) { return exec('node', [command].concat(args)); } export function exec(command: string, args?: string[]) { const p = shellExec(`${command} ${args.join(' ')}`, { async: true }) as child.ChildProcess; const waiter = new Exec(); p.stdout.on('data', (data) => { console.log(data.toString()); waiter.invoke({ stdout: data.toString(), stderr: null }); }); p.stderr.on('data', (data) => { console.error(data.toString()); waiter.invoke({ stdout: null, stderr: data.toString() }); }); process.on('beforeExit', () => { p.kill(); }); process.on('exit', (code) => { waiter.exitCode = code; p.kill(); }); waiter.process = p; return waiter; } export function expectErrors(stats: any, count: number, errors: string[] = []) { stats.compilation.errors.every(err => { const str = err.toString(); expect(errors.some(e => str.indexOf(e) !== -1), 'Error is not covered: \n' + str).true; }); expect(stats.compilation.errors.length).eq(count); } export function tsconfig(compilerOptions?: any, config?: any) { const res = _.merge({ compilerOptions: _.merge({ target: 'es6' }, compilerOptions) }, config); return file('tsconfig.json', json(res)); } export function install(...name: string[]) { return child.execSync(`yarn add ${name.join(' ')}`); } export function json(obj) { return JSON.stringify(obj, null, 4); } export function checkOutput(fileName: string, fragment: string) { const source = readOutput(fileName); if (!source) { process.exit(); } expect(source.replace(/\s/g, '')).include(fragment.replace(/\s/g, '')); } export function readOutput(fileName: string) { return fs.readFileSync(path.join(OUT_DIR, fileName || 'index.js')).toString(); } export function touchFile(fileName: string): Promise<any> { return readFile(fileName) .then(buf => buf.toString()) .then(source => writeFile(fileName, source)); } export function compile(config?): Promise<any> { return new Promise((resolve, reject) => { const compiler = webpack(config); compiler.run((err, stats) => { if (err) { reject(err); } else { resolve(stats); } }); }); } export interface TestEnv { TEST_DIR: string; OUT_DIR: string; SRC_DIR: string; LOADER: string; WEBPACK: string; } export function spec<T>(name: string, cb: (env: TestEnv, done?: () => void) => Promise<T>, disable = false) { const runner = function (done?) { const temp = path.join( TEST_DIR, path.basename(name).replace('.', '') + '-' + (new Date()).toTimeString() .replace(/.*(\d{2}:\d{2}:\d{2}).*/, "$1") .replace(/:/g, "-") ); mkdirp.sync(temp); let cwd = process.cwd(); process.chdir(temp); pkg(); const env = { TEST_DIR, OUT_DIR, SRC_DIR, LOADER, WEBPACK }; const promise = cb.call(this, env, done); return promise .then(a => { process.chdir(cwd); return a; }) .catch(e => { process.chdir(cwd); throw e; }); }; const asyncRunner = cb.length === 2 ? function (done) { runner.call(this, done).catch(done); return; } : function () { return runner.call(this); }; if (disable) { xit(name, asyncRunner); } else { it(name, asyncRunner); } } export function xspec<T>(name: string, cb: (env: TestEnv, done?: () => void) => Promise<T>) { return spec(name, cb, true); } export function watch(config, cb?: (err, stats) => void): Watch { let compiler = webpack(config); let watch = new Watch(); let webpackWatcher = compiler.watch({}, (err, stats) => { watch.invoke(err, stats); if (cb) { cb(err, stats); } }); watch.close = webpackWatcher.close; return watch; } export class Watch { close: (callback: () => void) => void; private resolves: {resolve: any, reject: any}[] = []; invoke(err, stats) { this.resolves.forEach(({resolve, reject}) => { if (err) { reject(err); } else { resolve(stats); } }); this.resolves = []; } wait(): Promise<any> { return new Promise((resolve, reject) => { this.resolves.push({resolve, reject}); }); } } export function pkg() { file('package.json', ` { "name": "test", "license": "MIT" } `); } export function src(fileName: string, text: string) { return new Fixture(path.join(SRC_DIR, fileName), text); } export function file(fileName: string, text: string) { return new Fixture(fileName, text); } export class Fixture { private text: string; private fileName: string; constructor(fileName: string, text: string) { this.text = text; this.fileName = fileName; mkdirp.sync(path.dirname(this.fileName)); fs.writeFileSync(this.fileName, text); } path() { return this.fileName; } toString() { return this.path(); } touch() { touchFile(this.fileName); } update(updater: (text: string) => string) { let newText = updater(this.text); this.text = newText; fs.writeFileSync(this.fileName, newText); } remove() { fs.unlinkSync(this.fileName); } }