Skip to content
This repository has been archived by the owner on Apr 22, 2021. It is now read-only.

Commit

Permalink
Support local zos
Browse files Browse the repository at this point in the history
  • Loading branch information
ylv-io committed Jun 4, 2019
1 parent 93e1dca commit 0942fa1
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 83 deletions.
88 changes: 58 additions & 30 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
const path = require('path');
const { pathExists } = require('fs-extra');

const { getOptions, parseQuery } = require('loader-utils');

const { exec, readFile, wait } = require('./lib/util');
const {
exec,
readFile,
wait,
packageExist,
which,
} = require('./lib/util');
const { getConfig, getLocalDependencies } = require('./lib/truffle');

// Lock to prevent race conditions
let isZeppelinBusy = false;

const oz = 'zos';

module.exports = async function loader(source) {
const callback = this.async();
const addDependency = this.addDependency;

let notInstalledMessage;

try {
const params = parseQuery(this.resourceQuery || '?');
const options = getOptions(this);
Expand All @@ -24,45 +35,62 @@ module.exports = async function loader(source) {
const contractsBuildDirectory = config.contracts_build_directory;
const contractFileName = path.basename(contractFilePath);
const contractName = params.contract || contractFileName.charAt(0).toUpperCase() + contractFileName.slice(1, contractFileName.length - 4);
const compiledContractPath = path
.resolve(contractsBuildDirectory, `${contractName}.json`);
const compiledContractPath = path.resolve(contractsBuildDirectory, `${contractName}.json`);

// if loader is disabled do not compile/push/upgrade, but still serve .json contracts from file system.
if (!disabled) {
// wait until compile/push/update is done
while (isZeppelinBusy) await wait(500);
// check if local version is installed
const isLocal = await packageExist(oz, cwd);
// check if global version is installed
const isGlobal = !!which.sync(oz, { nothrow: true });
// if not installed at all do nothing
if (isLocal || isGlobal) {
// wait until compile/push/update is done
while (isZeppelinBusy) await wait(500);

isZeppelinBusy = true;
isZeppelinBusy = true;

try {
const execOptions = {
cwd,
env: {
...process.env,
// disable an interactive in ZeppelinOS by setting env variable to prevent blocking
ZOS_NON_INTERACTIVE: 'FULL',
},
};
// push new code into local blockchain
let result = await exec(`zos push --network ${network}`, execOptions);
// update a proxy contract
result = await exec(`zos update ${contractName} --network ${network}`, execOptions);
} finally {
// release the lock
isZeppelinBusy = false;
try {
const execOptions = {
cwd,
env: {
...process.env,
// disable an interactive in ZeppelinOS by setting env variable to prevent blocking
ZOS_NON_INTERACTIVE: 'FULL',
},
};
// local has priority over global
const command = isLocal ? `npx ${oz}` : oz;
// push new code into local blockchain
let result = await exec(`${command} push --network ${network}`, execOptions);
// update a proxy contract
result = await exec(`${command} update ${contractName} --network ${network}`, execOptions);
} finally {
// release the lock
isZeppelinBusy = false;
}
} else {
notInstalledMessage = ` ${oz} is not installed either locally or globally.`;
}
}

// read JSON contract produced by compile and return it
const solJSON = await readFile(compiledContractPath, 'utf8');
// get all contract's local dependencies
const deps = await getLocalDependencies(contractName, contractsBuildDirectory, contractFolderPath);
// add these imports as dependencies for a contract
deps.map(imp => addDependency(imp));
// return result to webpack
callback(null, solJSON);
// check if compiled contract exists
const isContractPathExists = await pathExists(compiledContractPath);
if (isContractPathExists) {
// read JSON contract produced by compile and return it
const solJSON = await readFile(compiledContractPath, 'utf8');
// get all contract's local dependencies
const deps = await getLocalDependencies(contractName, contractsBuildDirectory, contractFolderPath);
// add these imports as dependencies for a contract
deps.map(imp => addDependency(imp));
// return result to webpack
callback(null, solJSON);
} else {
callback(new Error(`The contract '${compiledContractPath}' doesn't exist. Try to compile your contracts first.${notInstalledMessage}`), '{}');
}
} catch (e) {
// report error here, because configuration seems to be lacking
e.message = `${e.message}${notInstalledMessage}`;
callback(e, '{}');
}
};
11 changes: 9 additions & 2 deletions lib/util.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
const util = require('util');
const fs = require('fs');
const path = require('path');
const which = require('which');
const { pathExists, readFile } = require('fs-extra');

const childProcess = require('child_process');

const { promisify } = util;
const exec = promisify(childProcess.exec);
const readFile = promisify(fs.readFile);
const wait = promisify(setTimeout);

async function packageExist(pckg, dir) {
return pathExists(path.resolve(dir, `node_modules/.bin/${pckg}`));
}

module.exports = {
exec,
readFile,
wait,
packageExist,
which: promisify(which),
};
31 changes: 27 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@
"dependencies": {
"array-flatten": "^2.1.2",
"find-up": "^3.0.0",
"fs-extra": "^8.0.1",
"loader-utils": "^1.2.3",
"path-is-inside": "^1.0.2",
"truffle-config": "^1.1.4"
"truffle-config": "^1.1.4",
"which": "^1.3.1"
},
"devDependencies": {
"@babel/core": "^7.3.4",
Expand Down
132 changes: 86 additions & 46 deletions test/loader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,10 @@ const execOptions = {
env: {
...process.env,
// disable an interactive in ZeppelinOS by setting env variable to prevent blocking
DISABLE_INTERACTIVITY: 'FULL',
ZOS_NON_INTERACTIVE: 'FULL',
},
};

util.exec = jest.fn();

const execute = async (options, source, contractName) => {
const stats = await compiler(source, options);
const output = stats.toJson().modules[0].source;
Expand All @@ -47,47 +45,89 @@ const execute = async (options, source, contractName) => {
expect(contract.abi.length).toBeGreaterThan(0);
};

beforeEach(() => {
jest.resetAllMocks();
});

test('Runs truffle compile, zos push, and zos update commands to produce fresh .json files', async (done) => {
const contractName = 'Contract';
const { network } = defaultOptions;
await execute(defaultOptions, contractFilePath, contractName);
expect(util.exec).toHaveBeenCalledTimes(2);
expect(util.exec).toHaveBeenCalledWith(`zos update ${contractName} --network ${network}`, execOptions);
expect(util.exec).toHaveBeenCalledWith(`zos push --network ${network}`, execOptions);
done();
});

test('Runs truffle compile, zos push, and zos update commands to produce fresh .json files with contract not being the same as filename', async (done) => {
const { network } = defaultOptions;
const contractName = 'Counter';
await execute(defaultOptions, `${contractFilePath}?contract=${contractName}`, contractName);
expect(util.exec).toHaveBeenCalledTimes(2);
expect(util.exec).toHaveBeenCalledWith(`zos update ${contractName} --network ${network}`, execOptions);
expect(util.exec).toHaveBeenCalledWith(`zos push --network ${network}`, execOptions);
done();
});

test('Serves json files from file system while disabled', async (done) => {
const contractName = 'Contract';
await execute(disabledOptions, contractFilePath, contractName);
expect(util.exec).toHaveBeenCalledTimes(0);
done();
});

test('Discovers parent contracts as dependencies', async (done) => {
const ret = ['B.sol', 'Base.sol'];
const deps = await getLocalDependencies('C', path.resolve(contractsBuildDir), path.resolve(contractsDir));
expect(deps.length).toEqual(2);
expect(deps.map(dep => path.basename(dep))).toEqual(ret);
done();
});

test('Discovers parent contracts as dependencies for contract inside one .sol file with many contracts', async (done) => {
const deps = await getLocalDependencies('Contract', path.resolve(contractsBuildDir), path.resolve(contractsDir));
expect(deps.length).toEqual(0);
done();
describe('Hot Loader', () => {
beforeAll(() => {
util.exec = jest.fn();
util.packageExist = jest.fn();
util.which.sync = jest.fn();
});

beforeEach(() => {
jest.clearAllMocks();
});

const coreTests = (command) => {
test('Runs truffle compile, zos push, and zos update commands to produce fresh .json files', async (done) => {
const contractName = 'Contract';
const { network } = defaultOptions;
await execute(defaultOptions, contractFilePath, contractName);
expect(util.exec).toHaveBeenCalledTimes(2);
expect(util.exec).toHaveBeenCalledWith(`${command} update ${contractName} --network ${network}`, execOptions);
expect(util.exec).toHaveBeenCalledWith(`${command} push --network ${network}`, execOptions);
done();
});

test('Runs truffle compile, zos push, and zos update commands to produce fresh .json files with contract not being the same as filename', async (done) => {
const { network } = defaultOptions;
const contractName = 'Counter';
await execute(defaultOptions, `${contractFilePath}?contract=${contractName}`, contractName);
expect(util.exec).toHaveBeenCalledTimes(2);
expect(util.exec).toHaveBeenCalledWith(`${command} update ${contractName} --network ${network}`, execOptions);
expect(util.exec).toHaveBeenCalledWith(`${command} push --network ${network}`, execOptions);
done();
});

test('Serves json files from file system while disabled', async (done) => {
const contractName = 'Contract';
await execute(disabledOptions, contractFilePath, contractName);
expect(util.exec).toHaveBeenCalledTimes(0);
done();
});

test('Discovers parent contracts as dependencies', async (done) => {
const ret = ['B.sol', 'Base.sol'];
const deps = await getLocalDependencies('C', path.resolve(contractsBuildDir), path.resolve(contractsDir));
expect(deps.length).toEqual(2);
expect(deps.map(dep => path.basename(dep))).toEqual(ret);
done();
});

test('Discovers parent contracts as dependencies for contract inside one .sol file with many contracts', async (done) => {
const deps = await getLocalDependencies('Contract', path.resolve(contractsBuildDir), path.resolve(contractsDir));
expect(deps.length).toEqual(0);
done();
});
};

describe('with zos not installed either globally or locally', () => {
beforeAll(() => {
util.which.sync.mockImplementation(pckg => null);
util.packageExist.mockImplementation((pckg, dir) => Promise.resolve(false));
});

test('Serves json files from file system', async (done) => {
const contractName = 'Contract';
await execute(disabledOptions, contractFilePath, contractName);
expect(util.exec).toHaveBeenCalledTimes(0);
done();
});
});

describe('with only global zos installed', () => {
beforeAll(() => {
util.which.sync.mockImplementation(pckg => 'zos');
util.packageExist.mockImplementation((pckg, dir) => Promise.resolve(false));
});

coreTests('zos');
});

describe('with local zos installed', () => {
beforeAll(() => {
util.packageExist.mockImplementation((pckg, dir) => Promise.resolve(true));
util.which.sync.mockImplementation(pckg => null);
});

coreTests('npx zos');
});
});

0 comments on commit 0942fa1

Please sign in to comment.