Add sol-compiler watch mode

This commit is contained in:
Leonid Logvinov 2018-12-19 14:11:08 +01:00
parent 8ddf925a8f
commit 657b698e1e
No known key found for this signature in database
GPG Key ID: 0DD294BFDE8C95D4
7 changed files with 88 additions and 7 deletions

View File

@ -1,4 +1,17 @@
[ [
{
"version": "2.0.0",
"changes": [
{
"note": "Add sol-compiler watch mode with -w flag",
"pr": "TODO"
},
{
"note": "Make error and warning colouring more visually pleasant and consistent with other compilers",
"pr": "TODO"
}
]
},
{ {
"version": "1.1.16", "version": "1.1.16",
"changes": [ "changes": [

View File

@ -44,7 +44,9 @@
"devDependencies": { "devDependencies": {
"@0x/dev-utils": "^1.0.21", "@0x/dev-utils": "^1.0.21",
"@0x/tslint-config": "^2.0.0", "@0x/tslint-config": "^2.0.0",
"@types/chokidar": "^1.7.5",
"@types/mkdirp": "^0.5.2", "@types/mkdirp": "^0.5.2",
"@types/pluralize": "^0.0.29",
"@types/require-from-string": "^1.2.0", "@types/require-from-string": "^1.2.0",
"@types/semver": "^5.5.0", "@types/semver": "^5.5.0",
"chai": "^4.0.1", "chai": "^4.0.1",
@ -74,10 +76,12 @@
"@0x/web3-wrapper": "^3.2.1", "@0x/web3-wrapper": "^3.2.1",
"@types/yargs": "^11.0.0", "@types/yargs": "^11.0.0",
"chalk": "^2.3.0", "chalk": "^2.3.0",
"chokidar": "^2.0.4",
"ethereum-types": "^1.1.4", "ethereum-types": "^1.1.4",
"ethereumjs-util": "^5.1.1", "ethereumjs-util": "^5.1.1",
"lodash": "^4.17.5", "lodash": "^4.17.5",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"pluralize": "^7.0.0",
"require-from-string": "^2.0.1", "require-from-string": "^2.0.1",
"semver": "5.5.0", "semver": "5.5.0",
"solc": "^0.4.23", "solc": "^0.4.23",

View File

@ -25,6 +25,10 @@ const SEPARATOR = ',';
type: 'string', type: 'string',
description: 'comma separated list of contracts to compile', description: 'comma separated list of contracts to compile',
}) })
.option('watch', {
alias: 'w',
default: false,
})
.help().argv; .help().argv;
const contracts = _.isUndefined(argv.contracts) const contracts = _.isUndefined(argv.contracts)
? undefined ? undefined
@ -37,7 +41,11 @@ const SEPARATOR = ',';
contracts, contracts,
}; };
const compiler = new Compiler(opts); const compiler = new Compiler(opts);
if (argv.watch) {
await compiler.watchAsync();
} else {
await compiler.compileAsync(); await compiler.compileAsync();
}
})().catch(err => { })().catch(err => {
logUtils.log(err); logUtils.log(err);
process.exit(1); process.exit(1);

View File

@ -6,13 +6,17 @@ import {
NPMResolver, NPMResolver,
RelativeFSResolver, RelativeFSResolver,
Resolver, Resolver,
SpyResolver,
URLResolver, URLResolver,
} from '@0x/sol-resolver'; } from '@0x/sol-resolver';
import { logUtils } from '@0x/utils'; import { logUtils } from '@0x/utils';
import chalk from 'chalk';
import * as chokidar from 'chokidar';
import { CompilerOptions, ContractArtifact, ContractVersionData, StandardOutput } from 'ethereum-types'; import { CompilerOptions, ContractArtifact, ContractVersionData, StandardOutput } from 'ethereum-types';
import * as fs from 'fs'; import * as fs from 'fs';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as path from 'path'; import * as path from 'path';
import * as pluralize from 'pluralize';
import * as semver from 'semver'; import * as semver from 'semver';
import solc = require('solc'); import solc = require('solc');
@ -30,6 +34,7 @@ import {
} from './utils/compiler'; } from './utils/compiler';
import { constants } from './utils/constants'; import { constants } from './utils/constants';
import { fsWrapper } from './utils/fs_wrapper'; import { fsWrapper } from './utils/fs_wrapper';
import { CompilationError } from './utils/types';
import { utils } from './utils/utils'; import { utils } from './utils/utils';
type TYPE_ALL_FILES_IDENTIFIER = '*'; type TYPE_ALL_FILES_IDENTIFIER = '*';
@ -129,6 +134,43 @@ export class Compiler {
const promisedOutputs = this._compileContractsAsync(this._getContractNamesToCompile(), false); const promisedOutputs = this._compileContractsAsync(this._getContractNamesToCompile(), false);
return promisedOutputs; return promisedOutputs;
} }
public async watchAsync(): Promise<void> {
console.clear(); // tslint:disable-line:no-console
logWithTime('Starting compilation in watch mode...');
const watcher = chokidar.watch('^$', { ignored: /(^|[\/\\])\../ });
const onFileChangedAsync = async () => {
watcher.unwatch('*'); // Stop watching
try {
await this.compileAsync();
logWithTime('Found 0 errors. Watching for file changes.');
} catch (err) {
if (err.typeName === 'CompilationError') {
logWithTime(`Found ${err.errorsCount} ${pluralize('error', err.errorsCount)}. Watching for file changes.`);
} else {
logWithTime('Found errors. Watching for file changes.');
}
}
const pathsToWatch = this._getPathsToWatch();
watcher.add(pathsToWatch);
};
await onFileChangedAsync();
watcher.on('change', (changedFilePath: string) => {
console.clear(); // tslint:disable-line:no-console
logWithTime('File change detected. Starting incremental compilation...');
onFileChangedAsync();
});
}
private _getPathsToWatch(): string[] {
const contractNames = this._getContractNamesToCompile();
const spyResolver = new SpyResolver(this._resolver);
for (const contractName of contractNames) {
const contractSource = spyResolver.resolve(contractName);
getSourceTreeHash(spyResolver, contractSource.path);
}
const pathsToWatch = _.uniq(spyResolver.resolvedContractSources.map(cs => cs.absolutePath));
return pathsToWatch;
}
private _getContractNamesToCompile(): string[] { private _getContractNamesToCompile(): string[] {
let contractNamesToCompile; let contractNamesToCompile;
if (this._specifiedContracts === ALL_CONTRACTS_IDENTIFIER) { if (this._specifiedContracts === ALL_CONTRACTS_IDENTIFIER) {
@ -298,3 +340,7 @@ export class Compiler {
logUtils.warn(`${contractName} artifact saved!`); logUtils.warn(`${contractName} artifact saved!`);
} }
} }
function logWithTime(arg: string): void {
logUtils.log(`[${chalk.gray(new Date().toLocaleTimeString())}] ${arg}`);
}

View File

@ -12,6 +12,7 @@ import { binPaths } from '../solc/bin_paths';
import { constants } from './constants'; import { constants } from './constants';
import { fsWrapper } from './fs_wrapper'; import { fsWrapper } from './fs_wrapper';
import { CompilationError } from './types';
/** /**
* Gets contract data on network or returns if an artifact does not exist. * Gets contract data on network or returns if an artifact does not exist.
@ -147,13 +148,13 @@ function printCompilationErrorsAndWarnings(solcErrors: solc.SolcError[]): void {
if (!_.isEmpty(errors)) { if (!_.isEmpty(errors)) {
errors.forEach(error => { errors.forEach(error => {
const normalizedErrMsg = getNormalizedErrMsg(error.formattedMessage || error.message); const normalizedErrMsg = getNormalizedErrMsg(error.formattedMessage || error.message);
logUtils.warn(chalk.red(normalizedErrMsg)); logUtils.log(chalk.red('error'), normalizedErrMsg);
}); });
throw new Error('Compilation errors encountered'); throw new CompilationError(errors.length);
} else { } else {
warnings.forEach(warning => { warnings.forEach(warning => {
const normalizedWarningMsg = getNormalizedErrMsg(warning.formattedMessage || warning.message); const normalizedWarningMsg = getNormalizedErrMsg(warning.formattedMessage || warning.message);
logUtils.warn(chalk.yellow(normalizedWarningMsg)); logUtils.log(chalk.yellow('warning'), normalizedWarningMsg);
}); });
} }
} }

View File

@ -29,3 +29,12 @@ export interface Token {
} }
export type DoneCallback = (err?: Error) => void; export type DoneCallback = (err?: Error) => void;
export class CompilationError extends Error {
public errorsCount: number;
public typeName = 'CompilationError';
constructor(errorsCount: number) {
super('Compilation errors encountered');
this.errorsCount = errorsCount;
}
}

View File

@ -52,7 +52,7 @@ describe('Compiler utils', () => {
const source = await fsWrapper.readFileAsync(path, { const source = await fsWrapper.readFileAsync(path, {
encoding: 'utf8', encoding: 'utf8',
}); });
const dependencies = parseDependencies({ source, path }); const dependencies = parseDependencies({ source, path, absolutePath: path });
const expectedDependencies = [ const expectedDependencies = [
'zeppelin-solidity/contracts/token/ERC20/ERC20.sol', 'zeppelin-solidity/contracts/token/ERC20/ERC20.sol',
'packages/sol-compiler/lib/test/fixtures/contracts/TokenTransferProxy.sol', 'packages/sol-compiler/lib/test/fixtures/contracts/TokenTransferProxy.sol',
@ -68,7 +68,7 @@ describe('Compiler utils', () => {
const source = await fsWrapper.readFileAsync(path, { const source = await fsWrapper.readFileAsync(path, {
encoding: 'utf8', encoding: 'utf8',
}); });
expect(parseDependencies({ source, path })).to.be.deep.equal([ expect(parseDependencies({ source, path, absolutePath: path })).to.be.deep.equal([
'zeppelin-solidity/contracts/ownership/Ownable.sol', 'zeppelin-solidity/contracts/ownership/Ownable.sol',
'zeppelin-solidity/contracts/token/ERC20/ERC20.sol', 'zeppelin-solidity/contracts/token/ERC20/ERC20.sol',
]); ]);
@ -77,7 +77,7 @@ describe('Compiler utils', () => {
it.skip('correctly parses commented out dependencies', async () => { it.skip('correctly parses commented out dependencies', async () => {
const path = ''; const path = '';
const source = `// import "./TokenTransferProxy.sol";`; const source = `// import "./TokenTransferProxy.sol";`;
expect(parseDependencies({ path, source })).to.be.deep.equal([]); expect(parseDependencies({ path, source, absolutePath: path })).to.be.deep.equal([]);
}); });
}); });
}); });