diff --git a/dapps/creators/react/.npmrc b/dapps/creators/react/.npmrc new file mode 100644 index 000000000..e031d3432 --- /dev/null +++ b/dapps/creators/react/.npmrc @@ -0,0 +1,4 @@ +engine-strict = true +package-lock = false +save-exact = true +scripts-prepend-node-path = true diff --git a/dapps/creators/react/README.md b/dapps/creators/react/README.md new file mode 100644 index 000000000..764e2c225 --- /dev/null +++ b/dapps/creators/react/README.md @@ -0,0 +1,6 @@ +# `embark-create-react-dapp` + +> DApp creator utility that uses Create React App + +Visit [framework.embarklabs.io](https://framework.embarklabs.io/) to get started with +[Embark](https://github.com/embarklabs/embark). diff --git a/dapps/creators/react/bin/create b/dapps/creators/react/bin/create new file mode 100755 index 000000000..5d42c467f --- /dev/null +++ b/dapps/creators/react/bin/create @@ -0,0 +1,13 @@ +#!/usr/bin/env node + +/* global process require */ + +require('source-map-support/register'); +require('..').main() + .then(code => { + process.exit(code ? code : 0); + }) + .catch(error => { + console.error(error.stack); + process.exit(1); + }); diff --git a/dapps/creators/react/package.json b/dapps/creators/react/package.json new file mode 100644 index 000000000..4121528f3 --- /dev/null +++ b/dapps/creators/react/package.json @@ -0,0 +1,70 @@ +{ + "name": "@embarklabs/create-react-dapp", + "version": "5.2.1", + "author": "Michael Bradley (https://github.com/michaelsbradleyjr/)", + "description": "DApp creator utility that uses Create React App", + "homepage": "https://github.com/embarklabs/embark/tree/master/dapps/creators/react#readme", + "repository": { + "directory": "dapps/creators/react", + "type": "git", + "url": "https://github.com/embarklabs/embark.git" + }, + "keywords": [ + "blockchain", + "create-react-app", + "dapps", + "ethereum", + "ipfs", + "react", + "serverless", + "solc", + "solidity" + ], + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "bin": "./bin/create", + "main": "./dist/index.js", + "files": [ + "dist" + ], + "embark-collective": { + "build:node": true + }, + "scripts": { + "_build": "npm run solo -- build", + "ci": "npm run qa", + "clean": "npm run reset", + "lint": "eslint bin/create src/", + "qa": "npm-run-all lint _build", + "reset": "npx rimraf dist embark-*.tgz package", + "solo": "embark-solo" + }, + "dependencies": { + "embark-i18n": "^5.1.1", + "embark-init": "^5.2.1", + "lodash.clonedeep": "4.5.0", + "source-map-support": "0.5.13" + }, + "eslintConfig": { + "extends": "../../../.eslintrc.json" + }, + "devDependencies": { + "@babel/runtime-corejs3": "7.7.4", + "embark-solo": "^5.1.1", + "eslint": "5.7.0", + "npm-run-all": "4.1.5", + "rimraf": "3.0.0" + }, + "indirect": { + "dependencies": { + "create-react-app": "^3.3.1" + } + }, + "engines": { + "node": ">=10.17.0", + "npm": ">=6.11.3", + "yarn": ">=1.19.1" + } +} diff --git a/dapps/creators/react/src/index.js b/dapps/creators/react/src/index.js new file mode 100644 index 000000000..cbc4f8f0d --- /dev/null +++ b/dapps/creators/react/src/index.js @@ -0,0 +1,41 @@ +/* global __dirname require */ + +import { __ } from 'embark-i18n'; +import { creatorDefaults, makeCreatorMain } from 'embark-init'; +import cloneDeep from 'lodash.clonedeep'; +import { join } from 'path'; + +const pkgJsonPath = join(__dirname, "..", "package.json"); +const pkgJson = require(pkgJsonPath); +const version = pkgJson.version; + +const creator = cloneDeep(creatorDefaults); +creator.extraHelp = () => { + console.log(''); + console.log('EXTRA HELP SECTION FOR react-dapp'); +}; +creator.version = version; + +delete creator.init.options.contractsOnly; +delete creator.init.options.simple; +delete creator.init.presets.contractsOnly; +const defaultPreset = '@embarklabs/dapps-presets-react-boilerplate'; +creator.init.presets.default = `${defaultPreset}@${pkgJson.devDependencies[defaultPreset]}`; +const demoPreset = '@embarklabs/dapps-presets-react-demo'; +creator.init.presets.demo = `${demoPreset}@${pkgJson.devDependencies[demoPreset]}`; + +const subcreator = 'create-react-app'; +creator.subcreator.abbrev = 'cra'; +creator.subcreator.command = ['{{subcreator.package}}', [2], {abc:123}]; +creator.subcreator.name = subcreator; +creator.subcreator.package = '{{subcreator.name}}@{{subcreator.version}}'; +creator.subcreator.version = pkgJson.indirect.dependencies[subcreator]; + +// const subcreator = '@angular/cli'; +// creator.subcreator.abbrev = 'ng'; +// creator.subcreator.command = ['-p', '{{subcreator.package}}', 'ng', 'new']; +// creator.subcreator.name = subcreator; +// creator.subcreator.package = '{{subcreator.name}}@{{subcreator.version}}'; +// creator.subcreator.version = pkgJson.indirect.dependencies[subcreator]; + +export const main = makeCreatorMain(creator); diff --git a/package.json b/package.json index 3a461cb70..8d5883eb2 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ }, "workspaces": { "packages": [ + "dapps/creators/*", "dapps/templates/*", "dapps/tests/*", "packages/*", diff --git a/packages/embark/package.json b/packages/embark/package.json index 16ac0a76e..dafd48648 100644 --- a/packages/embark/package.json +++ b/packages/embark/package.json @@ -82,6 +82,7 @@ "embark-engine": "^5.2.2", "embark-graph": "^5.2.0", "embark-i18n": "^5.1.1", + "embark-init": "^5.2.0", "embark-logger": "^5.2.0", "embark-reset": "^5.1.1", "embark-suggestions": "^5.2.2", diff --git a/packages/embark/src/bin/embark.js b/packages/embark/src/bin/embark.js index 217a6d843..50bd26b1c 100755 --- a/packages/embark/src/bin/embark.js +++ b/packages/embark/src/bin/embark.js @@ -992,6 +992,7 @@ function isDappCmd(cmd) { '--help', 'new', 'demo', + 'init', 'version', 'help' ].indexOf(cmd) === -1; diff --git a/packages/embark/src/cmd/cmd.js b/packages/embark/src/cmd/cmd.js index 07b83d211..f7ce7cf62 100644 --- a/packages/embark/src/cmd/cmd.js +++ b/packages/embark/src/cmd/cmd.js @@ -1,4 +1,5 @@ import { __, setOrDetectLocale } from 'embark-i18n'; +import { cli as initCli } from 'embark-init'; import { diagramPath } from 'embark-utils'; const program = require('commander'); const EmbarkController = require('./cmd_controller.js'); @@ -13,6 +14,7 @@ class Cmd { process(args) { this.newApp(); this.demo(); + this.init(); this.build(); this.run(); this.exec(); @@ -439,6 +441,19 @@ class Cmd { }); } + async init() { + let reject, resolve; + let promise = new Promise((res, rej) => { resolve = res; reject = rej; }); + initCli(program.command('init'), { reject, resolve }); + try { + const code = await promise; + process.exit(code ?? 0); + } catch (error) { + console.error(error.stack); + process.exit(1); + } + } + helpCmd() { program .command('help') diff --git a/packages/utils/init/.npmrc b/packages/utils/init/.npmrc new file mode 100644 index 000000000..e031d3432 --- /dev/null +++ b/packages/utils/init/.npmrc @@ -0,0 +1,4 @@ +engine-strict = true +package-lock = false +save-exact = true +scripts-prepend-node-path = true diff --git a/packages/utils/init/README.md b/packages/utils/init/README.md new file mode 100644 index 000000000..ddf0c78f1 --- /dev/null +++ b/packages/utils/init/README.md @@ -0,0 +1,6 @@ +# `embark-init` + +> DApp initializer utility + +Visit [framework.embarklabs.io](https://framework.embarklabs.io/) to get started with +[Embark](https://github.com/embarklabs/embark). diff --git a/packages/utils/init/bin/init b/packages/utils/init/bin/init new file mode 100755 index 000000000..5d42c467f --- /dev/null +++ b/packages/utils/init/bin/init @@ -0,0 +1,13 @@ +#!/usr/bin/env node + +/* global process require */ + +require('source-map-support/register'); +require('..').main() + .then(code => { + process.exit(code ? code : 0); + }) + .catch(error => { + console.error(error.stack); + process.exit(1); + }); diff --git a/packages/utils/init/package.json b/packages/utils/init/package.json new file mode 100644 index 000000000..b537def3d --- /dev/null +++ b/packages/utils/init/package.json @@ -0,0 +1,63 @@ +{ + "name": "embark-init", + "version": "5.2.1", + "author": "Michael Bradley (https://github.com/michaelsbradleyjr/)", + "description": "DApp initializer utility", + "homepage": "https://github.com/embarklabs/embark/tree/master/packages/utils/init#readme", + "repository": { + "directory": "packages/utils/init", + "type": "git", + "url": "https://github.com/embarklabs/embark.git" + }, + "keywords": [ + "blockchain", + "dapps", + "ethereum", + "ipfs", + "serverless", + "solc", + "solidity" + ], + "license": "MIT", + "bin": "./bin/init", + "main": "./dist/index.js", + "files": [ + "dist" + ], + "embark-collective": { + "build:node": true + }, + "scripts": { + "_build": "npm run solo -- build", + "ci": "npm run qa", + "clean": "npm run reset", + "lint": "eslint bin/init src/", + "qa": "npm-run-all lint _build", + "reset": "npx rimraf dist embark-*.tgz package", + "solo": "embark-solo" + }, + "dependencies": { + "chalk": "2.4.2", + "commander": "2.20.3", + "embark-i18n": "^5.1.1", + "lodash.clonedeep": "4.5.0", + "lodash.isplainobject": "4.0.6", + "minimist": "1.2.0", + "source-map-support": "0.5.13" + }, + "eslintConfig": { + "extends": "../../../.eslintrc.json" + }, + "devDependencies": { + "@babel/runtime-corejs3": "7.7.4", + "embark-solo": "^5.1.1", + "eslint": "5.7.0", + "npm-run-all": "4.1.5", + "rimraf": "3.0.0" + }, + "engines": { + "node": ">=10.17.0", + "npm": ">=6.11.3", + "yarn": ">=1.19.1" + } +} diff --git a/packages/utils/init/src/creator.js b/packages/utils/init/src/creator.js new file mode 100644 index 000000000..93406a8fd --- /dev/null +++ b/packages/utils/init/src/creator.js @@ -0,0 +1,136 @@ +/* global process */ + +import { spawn } from 'child_process'; +import { Command } from 'commander'; +import { __, setOrDetectLocale } from 'embark-i18n'; +import minimist from 'minimist'; + +import { creatorDefaults } from './defaults'; +import { main as initMain } from './index'; +import { makeCreatorCommanderOptions, replaceTokens, runCommand } from './util'; + +export { creatorDefaults }; + +const procArgv = process.argv.slice(); + +export function makeCreatorMain(creator = creatorDefaults) { + if (!Array.isArray(creator.subcreator.command)) { + creator.subcreator.command = [creator.subcreator.command]; + } + + async function handleCliOptions( + cliOptions, + { argv, promise, reject, resolve } = {} + ) { + setOrDetectLocale(cliOptions.locale); + + const npxCmd = process.platform === 'win32' ? 'npx.cmd': 'npx'; + + console.log('hi from handleCliOptions in creator.js'); + + // let error = new Error('bad stuff happened in creator.js'); + // if (reject) return reject(error); + // throw error; + if (resolve) return resolve(19); + return 19; + + // should display command/s that will be run, similar to what the release + // script does, so it's easier to figure out what's happening and how + // options are combining + + // are there prompts? + + // if so, and --yes was NOT spec'd then run the prompts and upate options + // with answers + + // --yes should apply to creator and init, but not to subcreator; if + // subcreator has a --yes option then it should be sepc'd with `-- --yes` + // ...could support ---yes + + // support a --cra-version options + + // support all options embark-init supports and forward them to embark-init + // if a project name isn't specified then pass '.' to create-react-app the + // embark-init options should be an export from embark-init, that way the + // creator doesn't have to "know about" specifics of embark-init + + // if `--` was spec'd then ignore subcreator.options, consider it an + // implicit --yes to prompts and supply options that followed `--` + + // ^ should probably have an --init-prompts for the creator that can + // combine with `--yes` and `--` + + // let subp = spawn(npxCmd, [ + // ...creator.subcreator.command, + // ...creator.subcreator.options + // ], { + // stdio: 'inherit' + // }); + + // let _reject, _resolve; + // let promise = new Promise((res, rej) => { _resolve = res; _reject = rej; }); + + // subp.on('error', error => { _reject(error); }); + // subp.on('exit', code => { _resolve(code); }); + + // try { + // const code = await promise; + // if (code) { + // if (resolve) return resolve(code); + // return code; + // } + // } catch (error) { + // if (reject) return reject(error); + // throw error; + // } + + // process init.options and build an array that combines/overrides re: + // matching options spec'd in the creator's cli + const initCliArgs = []; + + // should report what final options get forwarded to embark-init + return initMain(null, initCliArgs, {promise, reject, resolve}); + } + + function cli( + program, + { + argv = [], + promise, + reject, + resolve + } = {} + ) { + if (!program) { + program = new Command(); + program.version(creator.version); + } + program.description( + `Creates a new Embark dapp using ${creator.subcreator.name}` + ); + program.usage(`[options] [-- [${creator.subcreator.abbrev}-options]]`); + // console.log('before replace:', require('util').inspect(creator, {depth: null})); + creator = replaceTokens(creator); + // console.log('after replace:', require('util').inspect(creator, {depth: null})); + makeCreatorCommanderOptions(creator).forEach(opt => { + program.option(...opt); + }); + program.action((...args) => handleCliOptions( + args, { argv, promise, reject, resolve } + )); + return program; + } + + return function creatorMain( + program, + argv = procArgv, + { promise, reject, resolve } = {} + ) { + if (!(promise && reject && resolve)) { + promise = new Promise((res, rej) => { resolve = res; reject = rej; }); + } + program = cli(program, { argv: argv.slice(), promise, reject, resolve }); + program.parse(argv); + return promise; + }; +} diff --git a/packages/utils/init/src/defaults.js b/packages/utils/init/src/defaults.js new file mode 100644 index 000000000..baf8e7d23 --- /dev/null +++ b/packages/utils/init/src/defaults.js @@ -0,0 +1,143 @@ +import { __ } from 'embark-i18n'; +import { join } from 'path'; + +const pkgJsonPath = join(__dirname, "..", "package.json"); +const pkgJson = require(pkgJsonPath); + +const contractsOnlyPreset = '@embarklabs/dapps-presets-init-boilerplate'; +const defaultPreset = '@embarklabs/dapps-presets-init-boilerplate'; + +const defaultInitContractsOnlyPreset = `${contractsOnlyPreset}@${pkgJson.devDependencies[contractsOnlyPreset]}`; +const defaultInitDefaultPreset = `${defaultPreset}@${pkgJson.devDependencies[defaultPreset]}`; + +const defaultInitOptions = { + contractsOnly: { + description: [ + __('create a barebones project meant only for contract development'), + ',', + '\n ', + __('alias for %s', '--preset {{init.presets.contractsOnly}}') + ].join(''), + long: 'contracts-only' + }, + + force: { + description: __('overwrite existing files'), + long: 'force', + short: 'f' + }, + + locale: { + default: 'en', + description: __('language to use'), + long: 'locale [locale]' + }, + + preset: { + default: `{{init.presets.default}}`, + description: [ + __('preset to use'), + ', ', + __('can be any valid package specifier'), + '\n ' + ].join(''), + long: 'preset [pkg]', + short: 'p' + }, + + simple: { + description: __('alias for %s', '--contracts-only'), + long: 'simple' + }, + + template: { + description: __('alias for %s', '--preset [pkg]'), + long: 'template [pkg]' + }, + + yarn: { + description: __('use yarn instead of npm'), + long: 'yarn' + }, + + yes: { + description: __('skip prompts, accept defaults for unspecified options'), + long: 'yes', + short: 'y' + } +}; + +export const initDefaults = { + options: defaultInitOptions, + presets: { + contractsOnly: defaultInitContractsOnlyPreset, + default: defaultInitDefaultPreset + } +}; + +const defaultCreatorDemoPreset = null; +const defaultCreatorExtraHelp = null; + +const defaultCreatorOptions = { + demo: { + description: [ + __('create a working dapp with a SimpleStorage contract'), + ', ', + __('alias for %s', '--preset {{init.presets.demo}}') + ].join(''), + long: 'demo' + }, + + noInit: { + description: [ + __('only run %s', '{{subcreator.name}}'), + ', ', + __('do not initialize embark-related files') + ].join(''), + long: 'no-init' + }, + + overrideSubcreator: { + default: '{{subcreator.package}}', + description: [ + __('specify the %s', '{{subcreator.name}}'), + ' ', + __('package'), + ', ', + __('can be any valid package specifier'), + ', e.g. ', + __('to use a different version or fork') + ].join(''), + long: '{{subcreator.abbrev}}-package [pkg]' + } +}; + +const defaultSubcreatorAbbrev = null; +const defaultSubcreatorCommand = []; +const defaultSubcreatorName = null; +const defaultSubcreatorOptions = []; +const defaultSubcreatorPackage = null; +const defaultSubcreatorVersion = null; + +const subcreatorDefaults = { + abbrev: defaultSubcreatorAbbrev, + command: defaultSubcreatorCommand, + name: defaultSubcreatorName, + options: defaultSubcreatorOptions, + package: defaultSubcreatorPackage, + version: defaultSubcreatorVersion +}; + +export const creatorDefaults = { + extraHelp: defaultCreatorExtraHelp, + init: { + options: initDefaults.options, + presets: { + demo: defaultCreatorDemoPreset, + ...initDefaults.presets + } + }, + options: defaultCreatorOptions, + subcreator: subcreatorDefaults, + version: null +}; diff --git a/packages/utils/init/src/index.js b/packages/utils/init/src/index.js new file mode 100644 index 000000000..ddf692d38 --- /dev/null +++ b/packages/utils/init/src/index.js @@ -0,0 +1,153 @@ +/* global __dirname process require */ + +import { spawn } from 'child_process'; +import { Command } from 'commander'; +import { __, setOrDetectLocale } from 'embark-i18n'; +import minimist from 'minimist'; +import { join } from 'path'; + +import { initDefaults } from './defaults'; +import { makeInitCommanderOptions, replaceTokens, runCommand } from './util'; + +export { initDefaults }; +export * from './creator'; + +const pkgJsonPath = join(__dirname, "..", "package.json"); +const pkgJson = require(pkgJsonPath); +const version = pkgJson.version; + +async function handleCliOptions( + cliOptions, + { argv, init, promise, reject, resolve } = {} +) { + setOrDetectLocale(cliOptions.locale); + + const npxCmd = process.platform === 'win32' ? 'npx.cmd': 'npx'; + + console.log('hi from handleCliOptions in embark-init'); + + // let error = new Error('bad stuff happened in embark-init'); + // if (reject) return reject(error); + // throw error; + // if (resolve) return resolve(19); + // return 19; + + // should display command/s that will be run, similar to what the release + // script does, so it's easier to figure out what's happening and how options + // are combining + + // was a packge-command given as a positional? + + // if so, ignore LHS options and: + // `npx [package-command] [--RHS-opts] -- [--CREATOR-opts]` + // BUT first resolve [package-command]: + // first choice: embark-create-[package-command] + // fallback: create-[package-command] + + // ^ embark-init should check if it is in the monorepo and attempt to find + // the creator package inside the monorepo and invoke its bin directly + // instead of using npx, though npx can be a fallback + + // if not... + // are there prompts? + + // if so, and --yes was NOT spec'd then run the prompts and upate options + // with answers + + // presets should (somehow) be able to contain metadata that will affect + // creator and subcreator options, e.g. `--cra-version` and `-- --typescript` + + // hydrate the preset, which may involve additional prompts + // ?? maybe all init prompts should be in presets w/ no preliminary prompts + // spec'able to main ?? + + // the idea is to attempt to reuse Vue's presets system as much as possible, + // if necessary repurposing the source code as sources in embark-init, but + // hopefully it's flexible enough to be leveraged with copying/modifying the + // source code +} + +export function cli( + program, + { + argv = [], + init = initDefaults, + promise, + reject, + resolve + } = {} +) { + if (!program) { + program = new Command(); + program.version(version); + } + program.description('Initializes a project as an Embark dapp'); + program.usage('[options] [creator] [creator-options] [-- [extra-options]]'); + ({init} = replaceTokens({init})); + makeInitCommanderOptions(init).forEach(opt => { program.option(...opt); }); + program.action((...args) => handleCliOptions( + args, { argv, init, promise, reject, resolve } + )); + program.on('--help', () => { + let embarkInit = 'embark-init'; + if (program.parent) embarkInit = 'embark init'; + + console.log(''); + console.log('SIMPLE USAGE:'); + console.log(''); + console.log(' ', `${embarkInit} [options]`); + console.log(''); + console.log( + 'Initialization is performed in the current working directory;', + 'will first run \`npm init\` if no package.json is present.' + ); + + console.log(''); + console.log('WITH CREATOR:'); + console.log(''); + console.log(' ', `${embarkInit} [creator] [options] [-- [extra-options]]`); + console.log(''); + console.log( + 'A creator is a name such as "react-dapp".', + 'Options to the left of the creator are ignored.', + 'Any options to the right of "--" are passed to the underlying tool,', + 'e.g. create-react-app, and will override conflicting options computed by the creator.' + ); + console.log(''); + console.log( + 'Creator names are resolved to packages by prepending "@embarklabs/create-", then trying with "embark-", and finally using the bare name.' + ); + console.log(''); + console.log( + 'Example: "react-dapp" resolves to "@embarklabs/create-react-dapp", and the following are equivalent:' + ); + console.log(''); + console.log(' ', `${embarkInit} react-dapp mydapp [options] [-- [extra-options]]`); + console.log(''); + console.log(' ', `npx @embarklabs/crete-react-dapp mydapp [options] [-- [extra-options]]`); + console.log(''); + console.log('To see the help output of a creator do:'); + console.log(''); + console.log(' ', `${embarkInit} [creator] --help`); + console.log(''); + console.log('To see the help output of a creator\'s underlying tool do:'); + console.log(''); + console.log(' ', `${embarkInit} [creator] -- --help`); + }); + return program; +} + +const procArgv = process.argv.slice(); + +export function main( + program, + argv = procArgv, + { promise, reject, resolve } = {} +) { + if (!(promise && reject && resolve)) { + promise = new Promise((res, rej) => { resolve = res; reject = rej; }); + } + program = cli(program, { argv: argv.slice(), promise, reject, resolve }); + program.parse(argv); + return promise; +} diff --git a/packages/utils/init/src/util.js b/packages/utils/init/src/util.js new file mode 100644 index 000000000..2f95004ef --- /dev/null +++ b/packages/utils/init/src/util.js @@ -0,0 +1,183 @@ +import chalk from 'chalk'; +import cloneDeep from 'lodash.clonedeep'; +import isPlainObject from 'lodash.isplainobject'; + +import { creatorDefaults } from './defaults'; + +const cyan = (str) => chalk.cyan(str); +const log = (mark, str, which = 'log') => console[which]( + mark, str.filter(s => !!s).join(` `) +); +export const logError = (...str) => log(chalk.red(`✘`), str, 'error'); +export const logInfo = (...str) => log(chalk.blue(`ℹ`), str); +export const logSuccess = (...str) => log(chalk.green(`✔`), str); +export const logWarning = (...str) => log(chalk.yellow('‼︎'), str); + +export function makeInitCommanderOptions({options}) { + return Object.values(options) + .sort(({long: aLong}, {long: bLong}) => { + if (aLong < bLong) return -1; + if (aLong > bLong) return 1; + return 0; + }) + .reduce((acc, {default: def, description, long, short}) => { + const copt = []; + if (short) { + copt.push(`-${short}, --${long}`); + } else { + copt.push(`--${long}`); + } + copt.push(description); + if (def) copt.push(def); + return acc.concat([copt]); + }, []); +} + +export function makeCreatorCommanderOptions(creator) { + return makeInitCommanderOptions({ + options: { + ...creator.init.options, + ...creator.options + } + }); +} + +// !!! needs refactor because intend to use spawn +export function runCommand(cmd, inherit = true, display) { + logInfo(`Running command ${cyan(display || cmd)}.`); + let out; + if (inherit) { + // execSyncInherit(cmd); + } else { + // out = execSync(cmd); + } + return out; +} + +// NOTE: does not detect/handle recursive references, e.g. `{{foo}}` refers to +// `{{bar}}` and `{{bar}}` refers to `{{foo}}`, but assume for now that won't +// be a common pitfall +export function replaceTokens(obj) { + obj = cloneDeep(obj); + let dict = {}; + + let entries = Object.entries(obj).map( + ([key, value]) => [key, value, '', 'object'] + ); + while (entries.length) { + let [key, value, level, kind] = entries.shift(); + let nextLevel; + if (kind === 'object') { + nextLevel = `${level}${level ? '.' : ''}${key}`; + } else if (kind === 'array') { + nextLevel = `${level}[${key}]`; + } + if (typeof value === 'number') value = value.toString(); + if (typeof value !== 'string') { + if (isPlainObject(value)) { + const nextKind = 'object'; + entries.unshift(...(Object.entries(value).map( + ([key, value]) => [key, value, nextLevel, nextKind] + ))); + } else if (Array.isArray(value)) { + const nextKind = 'array'; + entries.unshift(...(value.map( + (value, index) => [index, value, nextLevel, nextKind] + ))); + } + continue; + } + const token = nextLevel; + dict[token] = value; + } + + const unknowns = []; + Object.values(dict).forEach(value => { + const matches = value.match(/\{\{([^}]+)\}\}/g); + if (!matches) return; + unknowns.push(...matches.map( + curly => curly.match(/\{\{\s*(\S+)\s*\}\}/)[1] + ).filter( + token => !dict.hasOwnProperty(token) + )); + }); + + if (unknowns.length) { + throw new Error( + `Unknown token${unknowns.length > 1 ? 's' : ''} ${unknowns.join(', ')}` + ); + } + + const keys = Object.keys(dict); + const dependencies = {}; + const dependents = {}; + while (keys.length) { + const key = keys.shift(); + if (!dependencies[key]) dependencies[key] = new Set(); + if (!dependents[key]) dependents[key] = new Set(); + let unresolvedDep; + const matches = dict[key].match(/\{\{([^}]+)\}\}/g); + if (matches) { + matches + .map(curly => curly.match(/\{\{\s*(\S+)\s*\}\}/)[1]) + .forEach(token => { + if (!dependents[token]) dependents[token] = new Set(); + dependents[token].add(key); + dependencies[key].add(token); + }); + [...dependencies[key]].forEach(dependency => { + if (dependents[dependency] && dependents[dependency].has(key)) { + throw new Error( + `circular dependency between tokens ${key} and ${dependency}` + ); + } + if (dict[dependency].match(/\{\{([^}]+)\}\}/g)) { + if (!unresolvedDep) { + unresolvedDep = true; + keys.push(key); + } + } else { + const token = `{{${dependency}}}`; + let value = dict[key]; + let replaced; + while (true) { + replaced = value.replace(token, dict[dependency]); + if (replaced === value) break; + value = replaced; + } + dict[key] = value; + } + }); + } + } + + entries = Object.entries(obj).map(([key, value]) => [key, value, obj]); + while (entries.length) { + let [key, value, context] = entries.shift(); + const nextContext = value; + if (isPlainObject(value)) { + entries.unshift(...(Object.entries(value).map( + ([key, value]) => [key, value, nextContext] + ))); + continue; + } else if (Array.isArray(value)) { + entries.unshift(...(value.map( + (value, index) => [index, value, nextContext] + ))); + continue; + } else if (typeof value === 'string') { + Object.entries(dict).forEach(([token, tokenVal]) => { + token = `{{${token}}}`; + let replaced; + while (true) { + replaced = value.replace(token, tokenVal); + if (replaced === value) break; + value = replaced; + } + context[key] = value; + }); + } + } + + return obj; +} diff --git a/scripts/globalize.js b/scripts/globalize.js index c0d5aa5e6..eaa1de28a 100644 --- a/scripts/globalize.js +++ b/scripts/globalize.js @@ -10,6 +10,11 @@ const embarkBinPath = path.resolve( ); if (!fs.existsSync(embarkBinPath)) process.exit(1); +const embarkInitBinPath = path.resolve( + path.join(__dirname, '../packages/utils/init/bin/init') +); +if (!fs.existsSync(embarkInitBinPath)) process.exit(1); + const getStdout = (cmd) => { let out; try { @@ -30,8 +35,16 @@ if (process.platform === 'win32') { path.join(npmGlobalBin, 'embark.cmd'), `@node "${embarkBinPath}" %*${EOL}` ); + fs.writeFileSync( + path.join(npmGlobalBin, 'embark-init.cmd'), + `@node "${embarkInitBinPath}" %*${EOL}` + ); } else { - const linkPath = path.join(npmGlobalBin, 'embark'); + let linkPath = path.join(npmGlobalBin, 'embark'); if (fs.existsSync(linkPath)) fs.unlinkSync(linkPath); fs.symlinkSync(embarkBinPath, linkPath); + + linkPath = path.join(npmGlobalBin, 'embark-init'); + if (fs.existsSync(linkPath)) fs.unlinkSync(linkPath); + fs.symlinkSync(embarkInitBinPath, linkPath); } diff --git a/yarn.lock b/yarn.lock index e8c632728..8eceadb17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7511,16 +7511,16 @@ commander@2.18.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970" integrity sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ== +commander@2.20.3, commander@^2.11.0, commander@^2.12.1, commander@^2.13.0, commander@^2.15.0, commander@^2.19.0, commander@^2.20.0, commander@^2.9.0, commander@~2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + commander@3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== -commander@^2.11.0, commander@^2.12.1, commander@^2.13.0, commander@^2.15.0, commander@^2.19.0, commander@^2.20.0, commander@^2.9.0, commander@~2.20.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - commander@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/commander/-/commander-4.0.1.tgz#b67622721785993182e807f4883633e6401ba53c"