awesome-typescript-loader
Version:
Awesome TS loader for webpack
432 lines (356 loc) • 11.1 kB
text/typescript
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);
}
}