feat(scaffold): allow association/file

- Refactor everything to TS
- Add missing types
- Declare __ everywhere
This commit is contained in:
Anthony Laibe 2018-11-28 14:23:32 +00:00
parent 5b3d8943cd
commit f68f1fc9b6
25 changed files with 684 additions and 381 deletions

View File

@ -72,6 +72,7 @@
"@babel/preset-react": "7.0.0", "@babel/preset-react": "7.0.0",
"@babel/preset-typescript": "7.1.0", "@babel/preset-typescript": "7.1.0",
"@babel/runtime-corejs2": "7.1.2", "@babel/runtime-corejs2": "7.1.2",
"ajv": "6.5.5",
"ascii-table": "0.0.9", "ascii-table": "0.0.9",
"async": "2.6.1", "async": "2.6.1",
"babel-loader": "8.0.4", "babel-loader": "8.0.4",
@ -164,6 +165,25 @@
"uuid": "3.3.2", "uuid": "3.3.2",
"viz.js": "1.8.2", "viz.js": "1.8.2",
"web3": "1.0.0-beta.34", "web3": "1.0.0-beta.34",
"web3-bzz": "1.0.0-beta.34",
"web3-core-helpers": "1.0.0-beta.34",
"web3-core-method": "1.0.0-beta.34",
"web3-core-promievent": "1.0.0-beta.34",
"web3-core-requestmanager": "1.0.0-beta.34",
"web3-core-subscriptions": "1.0.0-beta.34",
"web3-core": "1.0.0-beta.34",
"web3-eth-abi": "1.0.0-beta.34",
"web3-eth-accounts": "1.0.0-beta.34",
"web3-eth-contract": "1.0.0-beta.34",
"web3-eth-iban": "1.0.0-beta.34",
"web3-eth-personal": "1.0.0-beta.34",
"web3-eth": "1.0.0-beta.34",
"web3-net": "1.0.0-beta.34",
"web3-providers-http": "1.0.0-beta.34",
"web3-providers-ipc": "1.0.0-beta.34",
"web3-providers-ws": "1.0.0-beta.34",
"web3-shh": "1.0.0-beta.34",
"web3-utils": "1.0.0-beta.34",
"webpack": "4.19.0", "webpack": "4.19.0",
"webpack-bundle-analyzer": "2.13.1", "webpack-bundle-analyzer": "2.13.1",
"websocket": "1.0.28", "websocket": "1.0.28",
@ -174,6 +194,7 @@
"@babel/cli": "7.1.2", "@babel/cli": "7.1.2",
"@babel/plugin-proposal-optional-chaining": "7.0.0", "@babel/plugin-proposal-optional-chaining": "7.0.0",
"@types/async": "2.0.50", "@types/async": "2.0.50",
"@types/handlebars": "4.0.39",
"@types/i18n": "0.8.3", "@types/i18n": "0.8.3",
"@types/node": "10.11.7", "@types/node": "10.11.7",
"@types/os-locale": "2.1.0", "@types/os-locale": "2.1.0",

View File

@ -311,39 +311,12 @@ class Cmd {
scaffold() { scaffold() {
program program
.command('scaffold [contract] [fields...]') .command('scaffold [contractOrFile] [fields...]')
.option('--framework <framework>', 'UI framework to use. (default: react)') .option('--framework <framework>', 'UI framework to use. (default: react)')
.option('--contract-language <language>', 'Language used for the smart contract generation (default: solidity)') .option('--contract-language <language>', 'Language used for the smart contract generation (default: solidity)')
.option('--overwrite', 'Overwrite existing files. (default: false)') .option('--overwrite', 'Overwrite existing files. (default: false)')
.description(__('Generates a contract and a function tester for you\nExample: ContractName field1:uint field2:address --contract-language solidity --framework react')) .description(__('Generates a contract and a function tester for you\nExample: ContractName field1:uint field2:address --contract-language solidity --framework react'))
.action(function(contract, fields, options) { .action(function(contractOrFile, fields, options) {
if (contract === undefined) {
console.log("contract name is required");
process.exit(0);
}
let fieldMapping = {};
if (fields.length > 0) {
const typeRegex = /^(u?int[0-9]{0,3}(\[\])?|string|bool|address|bytes[0-9]{0,3})(\[\])?$/;
const varRegex = /^[a-zA-Z][a-zA-Z0-9_]*$/;
fieldMapping = fields.reduce((acc, curr) => {
const c = curr.split(':');
if (!varRegex.test(c[0])) {
console.log("Invalid variable name: " + c[0]);
process.exit(1);
}
if (!typeRegex.test(c[1])) {
console.log("Invalid datatype: " + c[1] + " - The dApp generator might not support this type at the moment");
process.exit(1);
}
acc[c[0]] = c[1];
return acc;
}, {});
}
i18n.setOrDetectLocale(options.locale); i18n.setOrDetectLocale(options.locale);
options.env = 'development'; options.env = 'development';
options.logFile = options.logfile; // fix casing options.logFile = options.logfile; // fix casing
@ -351,11 +324,8 @@ class Cmd {
options.onlyCompile = options.contracts; options.onlyCompile = options.contracts;
options.client = options.client || 'geth'; options.client = options.client || 'geth';
options.webpackConfigName = options.pipeline || 'development'; options.webpackConfigName = options.pipeline || 'development';
options.contract = contract; options.contractOrFile = contractOrFile;
options.framework = options.framework || 'react'; options.fields = fields;
options.contractLanguage = options.contractLanguage || 'solidity';
options.overwrite = options.overwrite || false;
options.fields = fieldMapping;
embark.scaffold(options); embark.scaffold(options);
}); });

View File

@ -471,9 +471,8 @@ class EmbarkController {
callback(); callback();
}, },
function generateContract(callback) { function generateContract(callback) {
engine.events.request('scaffolding:generate:contract', options, function(err, file) { engine.events.request('scaffolding:generate:contract', options, function(files) {
// Add contract file to the manager files.forEach(file => engine.events.request('config:contractsFiles:add', file));
engine.events.request('config:contractsFiles:add', file);
callback(); callback();
}); });
}, },

View File

@ -9,8 +9,6 @@ import Web3 from "web3";
import { Embark, Events } from "../../../typings/embark"; import { Embark, Events } from "../../../typings/embark";
import Suggestions from "./suggestions"; import Suggestions from "./suggestions";
declare const __: any;
class Console { class Console {
private embark: Embark; private embark: Embark;
private events: Events; private events: Events;

View File

@ -1,125 +0,0 @@
const Handlebars = require('handlebars');
const fs = require('../../core/fs');
let utils = require('../../utils/utils.js');
Handlebars.registerHelper('capitalize', function(word) {
return word.charAt(0).toUpperCase() + word.slice(1);
});
Handlebars.registerHelper('ifview', function(stateMutability, options) {
let result = stateMutability === 'view' || stateMutability === 'pure' || stateMutability === 'constant';
if (result) {
return options.fn(this);
}
return options.inverse(this);
});
Handlebars.registerHelper('ifeq', function(elem, value, options) {
if (elem === value) {
return options.fn(this);
}
return options.inverse(this);
});
Handlebars.registerHelper('ifarr', function(elem, options) {
if (elem.indexOf('[]') > -1) {
return options.fn(this);
}
return options.inverse(this);
});
Handlebars.registerHelper('iflengthgt', function(arr, val, options) {
if (arr.length > val) {
return options.fn(this);
}
return options.inverse(this);
});
Handlebars.registerHelper('emptyname', function(name, index) {
return name ? name : 'output' + index;
});
Handlebars.registerHelper('trim', function(name) {
return name.replace('[]', '');
});
Handlebars.registerHelper('methodname', function(abiDefinition, functionName, inputs) {
let funCount = abiDefinition.filter(x => x.name === functionName).length;
if (funCount === 1) {
return '.' + functionName;
}
return new Handlebars.SafeString(`['${functionName}(${inputs !== null ? inputs.map(input => input.type).join(',') : ''})']`);
});
class ScaffoldingReact {
constructor(embark, options) {
this.embark = embark;
this.options = options;
this.embark.registerDappGenerator('react', this.build.bind(this));
}
_generateFile(contract, templateFilename, extension, data, overwrite) {
const filename = contract.className.toLowerCase() + '.' + extension;
const filePath = './app/' + filename;
if (!overwrite && fs.existsSync(filePath)) {
throw new Error("file '" + filePath + "' already exists");
}
const templatePath = utils.joinPath(__dirname, 'templates/' + templateFilename);
const source = fs.readFileSync(templatePath).toString();
const template = Handlebars.compile(source);
// Write template
const result = template(data);
fs.writeFileSync(filePath, result);
return filePath;
}
build(contract, overwrite, cb) {
const packageInstallCmd = 'npm install react react-bootstrap react-dom';
utils.runCmd(packageInstallCmd, null, (err) => {
if (err) {
this.embark.logger.error(err.message);
process.exit(1);
}
try {
const filename = contract.className.toLowerCase();
this._generateFile(contract, 'index.html.hbs', 'html',
{
'title': contract.className,
filename
}, overwrite);
const filePath = this._generateFile(contract, 'dapp.js.hbs', 'js',
{
'title': contract.className,
'contractName': contract.className,
'functions': contract.abiDefinition.filter(x => x.type === 'function')
}, overwrite);
// Update config
const contents = fs.readFileSync("./embark.json");
let embarkJson = JSON.parse(contents);
embarkJson.app["js/" + filename + ".js"] = "app/" + filename + '.js';
embarkJson.app[filename + ".html"] = "app/" + filename + '.html';
fs.writeFileSync("./embark.json", JSON.stringify(embarkJson, null, 4));
this.embark.logger.info('app/' + filename + ".html generated");
this.embark.logger.info('app/' + filename + ".js generated");
cb(null, filePath);
} catch (error) {
this.embark.logger.error(error.message);
process.exit(1);
}
});
}
}
module.exports = ScaffoldingReact;

View File

@ -1,57 +0,0 @@
const Handlebars = require('handlebars');
const fs = require('../../core/fs');
const utils = require('../../utils/utils');
const capitalize = string => string.charAt(0).toUpperCase() + string.slice(1);
class ScaffoldingSolidity {
constructor(embark, options){
this.embark = embark;
this.options = options;
this.embark.registerDappGenerator('solidity', this.build.bind(this));
}
_generateFile(contract, templateFilename, extension, data, overwrite){
const filename = capitalize(contract.className.toLowerCase()) + '.' + extension;
const contractDirs = this.embark.config.embarkConfig.contracts;
const contractDir = Array.isArray(contractDirs) ? contractDirs[0] : contractDirs;
const filePath = fs.dappPath(contractDir.replace(/\*/g, ''), filename);
if (!overwrite && fs.existsSync(filePath)){
throw new Error("file '" + filePath + "' already exists");
}
const templatePath = utils.joinPath(__dirname, 'templates/' + templateFilename);
const source = fs.readFileSync(templatePath).toString();
const template = Handlebars.compile(source);
// Write template
const result = template(data);
fs.writeFileSync(filePath, result);
return filePath;
}
build(contract, overwrite, cb){
try {
contract.className = capitalize(contract.className);
const filename = contract.className;
const filePath = this._generateFile(contract, 'contract.sol.hbs', 'sol', {
'contractName': contract.className,
'structName': contract.className + "Struct",
'fields': Object.keys(contract.fields).map(f => {
return {name:f, type: contract.fields[f]};
})
}, overwrite);
this.embark.logger.info("contracts/" + filename + ".sol generated");
cb(null, filePath);
} catch(error) {
this.embark.logger.error(error.message);
process.exit(1);
}
}
}
module.exports = ScaffoldingSolidity;

View File

@ -1,50 +0,0 @@
pragma solidity ^0.5.0;
contract {{contractName}} {
struct {{structName}} {
{{#each fields}}
{{type}} {{name}};
{{/each}}
}
{{structName}}[] items;
event ItemCreated(uint id, address createdBy);
event ItemDeleted(uint id, address deletedBy);
event ItemUpdated(uint id, address updatedBy);
function add({{#each fields}}{{type}} _{{name}}{{#unless @last}}, {{/unless}}{{/each}}) public {
uint id = items.length++;
items[id] = {{structName}}({
{{#each fields}}
{{name}}: _{{name}}{{#unless @last}},{{/unless}}
{{/each}}
});
emit ItemCreated(id, msg.sender);
}
function edit(uint _id, {{#each fields}}{{type}} _{{name}}{{#unless @last}}, {{/unless}}{{/each}}) public {
require(_id < items.length, "Invalid {{structName}} id");
{{#each fields}}
items[_id].{{name}} = _{{name}};
{{/each}}
emit ItemUpdated(_id, msg.sender);
}
function remove(uint _id) public {
require(_id < items.length, "Invalid {{structName}} id");
delete items[_id];
emit ItemDeleted(_id, msg.sender);
}
function get(uint _id) public view returns ({{#each fields}}{{type}}{{#unless @last}},{{/unless}}{{/each}}) {
require(_id < items.length, "Invalid ArrayContractStruct id");
return ({{#each fields}}items[_id].{{name}}{{#unless @last}},{{/unless}}{{/each}});
}
}

View File

@ -0,0 +1,3 @@
export interface Builder {
build(): Promise<string[]>;
}

View File

@ -0,0 +1,33 @@
import Ajv from "ajv";
import { Logger } from "../../../typings/logger";
export enum Framework {
React = "react",
}
export enum ContractLanguage {
Solidity = "solidity",
}
export class CommandOptions {
constructor(private readonly logger: Logger,
public readonly framework: Framework = Framework.React,
public readonly contractLanguage: ContractLanguage = ContractLanguage.Solidity,
public readonly overwrite: boolean = false,
) {
}
public validate() {
if (!Object.values(Framework).includes(this.framework)) {
this.logger.error(__("Selected framework not supported"));
this.logger.error(__("Supported Frameworks are: %s", Object.values(Framework).join(", ")));
process.exit(1);
}
if (!Object.values(ContractLanguage).includes(this.contractLanguage)) {
this.logger.error(__("Selected contract language not supported"));
this.logger.error(__("Supported Contract Languages are: %s", Object.values(ContractLanguage).join(", ")));
process.exit(1);
}
}
}

View File

@ -0,0 +1,73 @@
import Handlebars from "handlebars";
import * as path from "path";
import { Embark } from "../../../../../typings/embark";
import { Builder } from "../../builder";
import { CommandOptions } from "../../commandOptions";
import { SmartContractsRecipe } from "../../smartContractsRecipe";
const fs = require("../../../../core/fs");
require("../../handlebarHelpers");
const templatePath = path.join(__dirname, "templates", "contract.sol.hbs");
export class SolidityBuilder implements Builder {
constructor(private embark: Embark,
private description: SmartContractsRecipe,
private options: CommandOptions) {
}
public async build() {
return Object.keys(this.description.data).map((contractName) => {
const code = this.generateCode(contractName);
const file = this.saveFile(contractName, code);
this.printInstructions(contractName);
return file;
});
}
private generateCode(contractName: string) {
const source = fs.readFileSync(templatePath, "utf-8");
const template = Handlebars.compile(source);
const attributes = this.description.standardAttributes(contractName);
const ipfs = this.description.ipfsAttributes(contractName);
const associations = this.description.associationAttributes(contractName);
const data = {
associations,
attributes,
contractName,
ipfs,
structName: `${contractName}Struct`,
};
return template(data);
}
private saveFile(contractName: string, code: string) {
const filename = `${contractName}.sol`;
const contractDirs = this.embark.config.embarkConfig.contracts;
const contractDir = Array.isArray(contractDirs) ? contractDirs[0] : contractDirs;
const filePath = fs.dappPath(contractDir.replace(/\*/g, ""), filename);
if (!this.options.overwrite && fs.existsSync(filePath)) {
this.embark.logger.error(__(`The contract ${contractName} already exists, skipping.`));
return;
}
fs.writeFileSync(filePath, code);
return filePath;
}
private printInstructions(contractName: string) {
const associations = Object.keys(this.description.associationAttributes(contractName));
if (!associations.length) {
return;
}
const args = associations.map((name) => `"$${name}"`).join(", ");
this.embark.logger.info(`In order to deploy your contracts, you will have to specify the dependencies.`);
this.embark.logger.info(`You can do it by adding to your contracts config the following snippets:`);
this.embark.logger.info(`${contractName}: { args: [${args}] }`);
}
}

View File

@ -0,0 +1,157 @@
pragma solidity ^0.5.0;
{{#each associations}}
import "./{{@key}}.sol";
{{/each}}
contract {{contractName}} {
uint constant IMPOSSIBLE_INDEX = 99999999999;
struct {{structName}} {
{{#each attributes}}
{{this}} {{@key}};
{{/each}}
}
{{structName}}[] items;
{{#each associations}}
{{#ifeq this 'hasMany'}}
mapping(uint => uint[]) {{lowercase @key}}Mapping;
{{/ifeq}}
{{#ifeq this 'belongsTo'}}
mapping(uint => uint) {{lowercase @key}}Mapping;
{{/ifeq}}
{{/each}}
{{#each ipfs}}
mapping(uint => string) {{@key}};
{{/each}}
{{#each associations}}
{{@key}} {{lowercase @key}};
{{/each}}
event ItemCreated(uint id, address createdBy);
event ItemDeleted(uint id, address deletedBy);
event ItemUpdated(uint id, address updatedBy);
constructor({{#each associations}}address _{{@key}}{{#unless @last}}, {{/unless}}{{/each}}) public {
{{#each associations}}
{{lowercase @key}} = {{@key}}(_{{@key}});
{{/each}}
}
function getLength() public view returns(uint count) {
return items.length;
}
function add({{#each attributes}}{{this}}{{#ifstring this}} memory{{/ifstring}} _{{@key}}{{#unless @last}}, {{/unless}}{{/each}}) public {
uint id = items.length++;
items[id] = {{structName}}({
{{#each attributes}}
{{@key}}: _{{@key}}{{#unless @last}},{{/unless}}
{{/each}}
});
{{#each associations}}
{{#ifeq this 'hasMany'}}
{{lowercase @key}}Mapping[id] = new uint[](0);
{{/ifeq}}
{{/each}}
emit ItemCreated(id, msg.sender);
}
function edit(uint _id, {{#each attributes}}{{this}}{{#ifstring this}} memory{{/ifstring}} _{{@key}}{{#unless @last}}, {{/unless}}{{/each}}) public {
require(_id < items.length, "Invalid {{structName}} id");
{{#each attributes}}
items[_id].{{@key}} = _{{@key}};
{{/each}}
emit ItemUpdated(_id, msg.sender);
}
function remove(uint _id) public {
require(_id < items.length, "Invalid {{structName}} id");
delete items[_id];
emit ItemDeleted(_id, msg.sender);
}
function get(uint _id) public view returns ({{#each attributes}}{{this}}{{#ifstring this}} memory{{/ifstring}}{{#unless @last}}, {{/unless}}{{/each}}) {
require(_id < items.length, "Invalid {{structName}} id");
return ({{#each attributes}}items[_id].{{@key}}{{#unless @last}},{{/unless}}{{/each}});
}
{{#each ipfs}}
function add{{capitalize @key}}(uint _id, string memory _{{@key}}) public {
require(_id < items.length, "Invalid {{structName}} id");
{{@key}}[_id] = _{{@key}};
}
function remove{{capitalize @key}}(uint _id) public {
require(_id < items.length, "Invalid {{structName}} id");
{{@key}}[_id] = "";
}
function get{{capitalize @key}}(uint _id) public view returns(string memory) {
require(_id < items.length, "Invalid {{structName}} id");
return {{@key}}[_id];
}
{{/each}}
{{#each associations}}
function add{{capitalize @key}}(uint _id, uint _{{@key}}Id) public {
require(_id < items.length, "Invalid {{structName}} id");
require(_{{@key}}Id < {{lowercase @key}}.getLength(), "Invalid {{@key}} id");
{{#ifeq this 'hasMany'}}
uint index = indexOf({{lowercase @key}}Mapping[_id], _{{@key}}Id);
require(index == IMPOSSIBLE_INDEX, "_{{@key}}Id already added");
{{lowercase @key}}Mapping[_id].push(_{{@key}}Id);
{{/ifeq}}
{{#ifeq this 'belongsTo'}}
{{lowercase @key}}Mapping[_id] = _{{@key}}Id;
{{/ifeq}}
}
function remove{{capitalize @key}}(uint _id, uint _{{@key}}Id) public {
require(_id < items.length, "Invalid {{structName}} id");
require(_{{@key}}Id < {{lowercase @key}}.getLength(), "Invalid {{@key}} id");
{{#ifeq this 'hasMany'}}
uint index = indexOf({{lowercase @key}}Mapping[_id], _{{@key}}Id);
require(index != IMPOSSIBLE_INDEX, "_{{@key}}Id not found");
delete {{lowercase @key}}Mapping[_id][index];
{{/ifeq}}
{{#ifeq this 'belongsTo'}}
{{lowercase @key}}Mapping[_id] = 0;
{{/ifeq}}
}
function get{{capitalize @key}}(uint _id) public view returns(uint{{#ifeq this 'hasMany'}}[] memory{{/ifeq}}) {
require(_id < items.length, "Invalid {{structName}} id");
return {{lowercase @key}}Mapping[_id];
}
{{/each}}
function indexOf(uint[] storage values, uint value) private view returns(uint) {
for (uint i = 0; i < values.length; i++) {
if (values[i] == value) {
return i;
}
}
return IMPOSSIBLE_INDEX;
}
}

View File

@ -0,0 +1,101 @@
import Handlebars from "handlebars";
import * as path from "path";
import { Contract } from "../../../../../typings/contract";
import { Embark } from "../../../../../typings/embark";
import { Builder } from "../../builder";
import { CommandOptions } from "../../commandOptions";
import { SmartContractsRecipe } from "../../smartContractsRecipe";
const fs = require("../../../../core/fs");
const utils = require("../../../../utils/utils");
require("../../handlebarHelpers");
const indexTemplatePath = path.join(__dirname, "templates", "index.html.hbs");
const dappTemplatePath = path.join(__dirname, "templates", "dapp.js.hbs");
export class ReactBuilder implements Builder {
constructor(private embark: Embark,
private description: SmartContractsRecipe,
private contracts: Contract[],
private options: CommandOptions) {
}
public async build() {
await this.installDependencies();
return [].concat.apply([], Object.keys(this.description.data).map((contractName) => {
const [indexCode, dappCode] = this.generateCodes(contractName);
if (indexCode && dappCode) {
const files = this.saveFiles(contractName, indexCode, dappCode);
this.updateEmbarkJson(contractName, files);
return files;
} else {
return [];
}
}));
}
private updateEmbarkJson(contractName: string, files: string[]) {
const embarkJsonPath = path.join(fs.dappPath(), "embark.json");
const embarkJson = fs.readJSONSync(embarkJsonPath);
embarkJson.app[`js/${contractName}.js`] = `app/${contractName}.js`;
embarkJson.app[`${contractName}.html`] = `app/${contractName}.html`;
fs.writeFileSync(embarkJsonPath, JSON.stringify(embarkJson, null, 2));
}
private generateCodes(contractName: string) {
const indexSource = fs.readFileSync(indexTemplatePath, "utf-8");
const dappSource = fs.readFileSync(dappTemplatePath, "utf-8");
const indexTemplate = Handlebars.compile(indexSource);
const dappTemplate = Handlebars.compile(dappSource);
const indexData = {
filename: contractName.toLowerCase(),
title: contractName,
};
const contract = this.contracts.find((c) => c.className === contractName);
if (!contract) {
return [];
}
const dappData = {
contractName,
functions: contract.abiDefinition.filter((entry) => entry.type === "function"),
};
return [indexTemplate(indexData), dappTemplate(dappData)];
}
private installDependencies() {
const cmd = "npm install react react-bootstrap react-dom";
return new Promise<void>((resolve, reject) => {
utils.runCmd(cmd, null, (error: string) => {
if (error) {
return reject(new Error(error));
}
resolve();
});
});
}
private saveFiles(contractName: string, indexCode: string, dappCode: string) {
const indexFilePath = path.join(fs.dappPath(), "app", `${contractName}.html`);
const dappFilePath = path.join(fs.dappPath(), "app", `${contractName}.js`);
if (!this.options.overwrite && (fs.existsSync(indexFilePath) || fs.existsSync(dappFilePath))) {
return [];
}
fs.writeFileSync(indexFilePath, indexCode);
fs.writeFileSync(dappFilePath, dappCode);
this.embark.logger.info(__(`${indexFilePath} generated`));
this.embark.logger.info(__(`${dappFilePath} generated`));
return [indexFilePath, dappFilePath];
}
}

View File

@ -171,7 +171,7 @@ class {{capitalize name}}Form{{@index}} extends Component {
function {{contractName}}UI(props) { function {{contractName}}UI(props) {
return (<div> return (<div>
<h1>{{title}}</h1> <h1>{{contractName}}</h1>
{{#each functions}} {{#each functions}}
<{{capitalize name}}Form{{@index}} /> <{{capitalize name}}Form{{@index}} />
{{/each}} {{/each}}

View File

@ -0,0 +1,62 @@
import Handlebars from "handlebars";
import { ABIDefinition } from "web3/eth/abi";
Handlebars.registerHelper("capitalize", (word: string) => {
return word.charAt(0).toUpperCase() + word.slice(1);
});
Handlebars.registerHelper("lowercase", (word: string) => {
return word.toLowerCase();
});
Handlebars.registerHelper("ifview", function(stateMutability: string, options: Handlebars.HelperOptions) {
const isView = stateMutability === "view" || stateMutability === "pure" || stateMutability === "constant";
if (isView) {
return options.fn(this);
}
return options.inverse(this);
});
Handlebars.registerHelper("ifstring", function(value: string, options: Handlebars.HelperOptions) {
if (value === "string") {
return options.fn(this);
}
return options.inverse(this);
});
Handlebars.registerHelper("ifeq", function(elem: string, value: string, options: Handlebars.HelperOptions) {
if (elem === value) {
return options.fn(this);
}
return options.inverse(this);
});
Handlebars.registerHelper("ifarr", function(elem: string, options: Handlebars.HelperOptions) {
if (elem.indexOf("[]") > -1) {
return options.fn(this);
}
return options.inverse(this);
});
Handlebars.registerHelper("iflengthgt", function(arr, val, options: Handlebars.HelperOptions) {
if (arr.length > val) {
return options.fn(this);
}
return options.inverse(this);
});
Handlebars.registerHelper("emptyname", (name: string, index: string) => {
return name ? name : "output" + index;
});
Handlebars.registerHelper("trim", (name: string) => {
return name.replace("[]", "");
});
Handlebars.registerHelper("methodname", (abiDefinition: ABIDefinition[], functionName: string, inputs: any[]) => {
const funCount = abiDefinition.filter((x) => x.name === functionName).length;
if (funCount === 1) {
return "." + functionName;
}
return new Handlebars.SafeString(`["${functionName}(${inputs !== null ? inputs.map((input) => input.type).join(",") : ""})"]`);
});

View File

@ -1,96 +0,0 @@
class Scaffolding {
constructor(embark, _options) {
this.embark = embark;
this.options = _options;
this.plugins = _options.plugins;
embark.events.setCommandHandler("scaffolding:generate:contract", (options, cb) => {
this.framework = options.contractLanguage;
this.fields = options.fields;
this.generate(options.contract, options.overwrite, true, cb);
});
embark.events.setCommandHandler("scaffolding:generate:ui", (options, cb) => {
this.framework = options.framework;
this.fields = options.fields;
this.generate(options.contract, options.overwrite, false, cb);
});
}
getScaffoldPlugin(framework) {
let dappGenerators = this.plugins.getPluginsFor('dappGenerator');
let builder = null;
dappGenerators.forEach((plugin) => {
plugin.dappGenerators.forEach((d) => {
if (d.framework === framework) {
builder = d.cb;
}
});
});
return builder;
}
loadFrameworkModule() {
switch (this.framework) {
case 'react':
this.plugins.loadInternalPlugin('scaffolding-react', this.options);
break;
case 'solidity':
this.plugins.loadInternalPlugin('scaffolding-solidity', this.options);
break;
default:
this.embark.logger.error(__('Selected framework not supported'));
this.embark.logger.error(__('Supported Frameworks are: %s', 'react, solidity'));
process.exit(1);
}
}
generate(contractName, overwrite, isContractGeneration, cb) {
this.loadFrameworkModule();
const build = this.getScaffoldPlugin(this.framework);
if (!build) {
this.embark.logger.error("Could not find plugin for framework '" + this.framework + "'");
process.exit(1);
}
const hasFields = Object.getOwnPropertyNames(this.fields).length !== 0;
if (isContractGeneration && !hasFields) {
// This happens when you execute "scaffold ContractName",
// assuming the contract already exists in a .sol file
cb();
return;
}
let contract;
if (isContractGeneration && hasFields) {
contract = {className: contractName, fields: this.fields};
try {
build(contract, overwrite, cb);
} catch (err) {
this.embark.logger.error(err.message);
}
} else {
// Contract already exists
this.embark.events.request("contracts:list", (_err, contractsList) => {
if (_err) throw new Error(_err);
const contract = contractsList.find(x => x.className === contractName);
if (!contract) {
this.embark.logger.error("contract '" + contractName + "' does not exist");
cb();
return;
}
try {
build(contract, overwrite, cb);
} catch (err) {
this.embark.logger.error(err.message);
}
});
}
}
}
module.exports = Scaffolding;

View File

@ -0,0 +1,63 @@
import { Contract } from "../../../typings/contract";
import { Embark } from "../../../typings/embark";
import { CommandOptions, ContractLanguage, Framework } from "./commandOptions";
import { SolidityBuilder } from "./contractLanguage/solidityBuilder";
import { ReactBuilder } from "./framework/reactBuilder";
import { SmartContractsRecipe } from "./smartContractsRecipe";
export default class Scaffolding {
constructor(private embark: Embark, private options: any) {
this.embark.events.setCommandHandler("scaffolding:generate:contract", (cmdLineOptions: any, cb: (files: string[]) => void) => {
this.generateContract(cmdLineOptions).then(cb);
});
this.embark.events.setCommandHandler("scaffolding:generate:ui", (cmdLineOptions: any, cb: (files: string[]) => void) => {
this.generateUi(cmdLineOptions).then(cb);
});
}
private contractLanguageStrategy(recipe: SmartContractsRecipe, options: CommandOptions) {
switch (options.contractLanguage) {
case ContractLanguage.Solidity: {
return new SolidityBuilder(this.embark, recipe, options);
}
}
}
private frameworkStrategy(recipe: SmartContractsRecipe, contracts: Contract[], options: CommandOptions) {
switch (options.framework) {
case Framework.React: {
return new ReactBuilder(this.embark, recipe, contracts, options);
}
}
}
private parseAndValidate(cmdLineOptions: any) {
const options = new CommandOptions(this.embark.logger, cmdLineOptions.framework, cmdLineOptions.contractLanguage, cmdLineOptions.overwrite);
const recipe = new SmartContractsRecipe(this.embark.logger, cmdLineOptions.contractOrFile, cmdLineOptions.fields);
options.validate();
recipe.validate();
return {recipe, options};
}
private async generateContract(cmdLineOptions: any) {
const {recipe, options} = this.parseAndValidate(cmdLineOptions);
return await this.contractLanguageStrategy(recipe, options).build();
}
private async generateUi(cmdLineOptions: any) {
const {recipe, options} = this.parseAndValidate(cmdLineOptions);
const contracts = await this.getContracts();
return await this.frameworkStrategy(recipe, contracts, options).build();
}
private getContracts() {
return new Promise<Contract[]>((resolve) => {
this.embark.events.request("contracts:list", (_: null, contracts: Contract[]) => {
resolve(contracts);
});
});
}
}

View File

@ -0,0 +1,17 @@
export const schema = {
minProperties: 1,
patternProperties: {
".*": {
minProperties: 1,
patternProperties: {
"^[a-zA-Z][a-zA-Z0-9_]*$": {
pattern: "^(u?int[0-9]{0,3}(\[\])?|string|bool|address|belongsTo|hasMany|ipfsText|ipfsImage|bytes[0-9]{0,3})(\[\])?$",
type: "string",
},
},
type: ["object"],
},
},
title: "Scafold Schema",
type: "object",
};

View File

@ -0,0 +1,106 @@
import Ajv from "ajv";
import { Logger } from "../../../typings/logger";
import { schema } from "./schema";
const fs = require("../../core/fs");
const ajv = new Ajv();
const scaffoldingSchema = ajv.compile(schema);
interface Properties {
[propertyName: string]: string;
}
interface Data {
[contractName: string]: Properties;
}
const ASSOCIATIONS = ["belongsTo", "hasMany"];
const IPFS = ["ipfsText", "ipfsImage"];
export class SmartContractsRecipe {
public data: Data;
constructor(private readonly logger: Logger,
private readonly contractOrFile: string,
private readonly fields: string[],
) {
if (fs.existsSync(contractOrFile)) {
this.data = fs.readJSONSync(contractOrFile);
} else {
this.data = this.build();
}
}
public standardAttributes(contractName: string): Properties {
return Object.keys(this.data[contractName]).reduce((acc: Properties, propertyName: string) => {
const type = this.data[contractName][propertyName];
if (!ASSOCIATIONS.includes(type) && !IPFS.includes(type)) {
acc[propertyName] = type;
}
return acc;
}, {});
}
public ipfsAttributes(contractName: string): Properties {
return Object.keys(this.data[contractName]).reduce((acc: Properties, propertyName: string) => {
const type = this.data[contractName][propertyName];
if (IPFS.includes(type)) {
acc[propertyName] = type;
}
return acc;
}, {});
}
public associationAttributes(contractName: string): Properties {
return Object.keys(this.data[contractName]).reduce((acc: Properties, propertyName: string) => {
const type = this.data[contractName][propertyName];
if (ASSOCIATIONS.includes(type)) {
acc[propertyName] = type;
}
return acc;
}, {});
}
public validate() {
if (!scaffoldingSchema(this.data)) {
this.logger.error(__("The scaffolding schema is not valid:"));
this.logger.error(ajv.errorsText(scaffoldingSchema.errors));
process.exit(1);
}
const contractNames = Object.keys(this.data);
contractNames.forEach((contractName) => {
if (contractName[0] !== contractName[0].toUpperCase()) {
this.logger.error(__(`${contractName} must be capitalized.`));
process.exit(1);
}
Object.keys(this.associationAttributes(contractName)).forEach((associationName) => {
if (associationName === contractName) {
this.logger.error(__(`${contractName} is referring to himself.`));
process.exit(1);
}
if (!contractNames.includes(associationName)) {
this.logger.error(__(`${contractName} not found. Please make sure it is in the description.`));
process.exit(1);
}
if (Object.keys(this.data[associationName]).includes(contractName)) {
this.logger.error(__(`${associationName} has a cyclic dependencies with ${contractName}.`));
process.exit(1);
}
});
});
}
private build() {
return {
[this.contractOrFile]: this.fields.reduce((acc: Properties, property) => {
const [name, value] = property.split(":");
acc[name] = value;
return acc;
}, {}),
};
}
}

6
src/typings/contract.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
import { ABIDefinition } from "web3/eth/abi";
export interface Contract {
abiDefinition: ABIDefinition[];
className: string;
}

View File

@ -5,7 +5,7 @@ export interface Events {
request: any; request: any;
emit: any; emit: any;
once: any; once: any;
setCommandHandler: any; setCommandHandler(name: string, callback: (options: any, cb: () => void) => void): void;
} }
export interface Embark { export interface Embark {
@ -13,6 +13,13 @@ export interface Embark {
registerAPICall: any; registerAPICall: any;
registerConsoleCommand: any; registerConsoleCommand: any;
logger: Logger; logger: Logger;
config: {}; config: {
embarkConfig: {
contracts: string[] | string;
config: {
contracts: string;
};
};
};
registerActionForEvent(name: string, action: (callback: () => void) => void): void; registerActionForEvent(name: string, action: (callback: () => void) => void): void;
} }

1
src/typings/i18n/index.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare function __(...values: string[]): string;

8
src/typings/plugins.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
export interface Plugin {
dappGenerators: any;
}
export interface Plugins {
getPluginsFor(name: string): [Plugin];
loadInternalPlugin(name: string, options: any): void;
}

View File

@ -13,7 +13,8 @@
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"*": ["./typings/*"] "*": ["./typings/*"]
} },
"noImplicitThis": false
}, },
"include": ["./src/**/*"] "include": ["./src/**/*"]
} }

View File

@ -825,6 +825,11 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/handlebars@4.0.39":
version "4.0.39"
resolved "https://registry.yarnpkg.com/@types/handlebars/-/handlebars-4.0.39.tgz#961fb54db68030890942e6aeffe9f93a957807bd"
integrity sha512-vjaS7Q0dVqFp85QhyPSZqDKnTTCemcSHNHFvDdalO1s0Ifz5KuE64jQD5xoUkfdWwF4WpqdJEl7LsWH8rzhKJA==
"@types/i18n@0.8.3": "@types/i18n@0.8.3":
version "0.8.3" version "0.8.3"
resolved "https://registry.yarnpkg.com/@types/i18n/-/i18n-0.8.3.tgz#f602164f2fae486ea87590f6be5d6dd5db1664e6" resolved "https://registry.yarnpkg.com/@types/i18n/-/i18n-0.8.3.tgz#f602164f2fae486ea87590f6be5d6dd5db1664e6"
@ -1187,6 +1192,16 @@ ajv-keywords@^3.0.0, ajv-keywords@^3.1.0:
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a"
integrity sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo= integrity sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=
ajv@6.5.5, ajv@^6.0.1, ajv@^6.1.0, ajv@^6.5.3, ajv@^6.5.5:
version "6.5.5"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.5.tgz#cf97cdade71c6399a92c6d6c4177381291b781a1"
integrity sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==
dependencies:
fast-deep-equal "^2.0.1"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ajv@^4.7.0: ajv@^4.7.0:
version "4.11.8" version "4.11.8"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
@ -1205,16 +1220,6 @@ ajv@^5.0.0, ajv@^5.1.0, ajv@^5.1.5, ajv@^5.2.0:
fast-json-stable-stringify "^2.0.0" fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.3.0" json-schema-traverse "^0.3.0"
ajv@^6.0.1, ajv@^6.1.0, ajv@^6.5.3, ajv@^6.5.5:
version "6.5.5"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.5.tgz#cf97cdade71c6399a92c6d6c4177381291b781a1"
integrity sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==
dependencies:
fast-deep-equal "^2.0.1"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
align-text@^0.1.1, align-text@^0.1.3: align-text@^0.1.1, align-text@^0.1.3:
version "0.1.4" version "0.1.4"
resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"