diff --git a/base/BaseInitHybridGenerator.js b/base/BaseInitHybridGenerator.js new file mode 100644 index 00000000..a164d5ea --- /dev/null +++ b/base/BaseInitHybridGenerator.js @@ -0,0 +1,25 @@ +'use strict'; +const BaseInitPluginGenerator = require('./BaseInitPluginGenerator'); +const {basetype} = require('./config'); + +class BaseInitHybridGenerator extends BaseInitPluginGenerator { + + constructor(args, options) { + super(args, options, basetype.HYBRID); + } + + initializing() { + // since just last in the hierarchy used, need to do super calls + super.initializing(); + } + + default() { + return super.default(); + } + + writing() { + return super.writing(); + } +} + +module.exports = BaseInitHybridGenerator; \ No newline at end of file diff --git a/base/BaseInitPluginGenerator.js b/base/BaseInitPluginGenerator.js new file mode 100644 index 00000000..794bd20e --- /dev/null +++ b/base/BaseInitPluginGenerator.js @@ -0,0 +1,95 @@ +'use strict'; +const path = require('path'); +const fs = require('fs-extra'); +const chalk = require('chalk'); +const GeneratorUtils = require('../utils/GeneratorUtils'); +const BasePhoveaGenerator = require('../base/BasePhoveaGenerator'); +const config = require('./config'); + +class BaseInitPluginGenerator extends BasePhoveaGenerator { + + constructor(args, options, basetype) { + super(args, options); + this.type = path.basename(path.dirname(this.resolved)).substring(5); // init-web ... web + this.basetype = basetype || config.basetype.WEB; + // Make options available + this.option('skipInstall'); + this.option('noSamples'); + this.option('useDefaults'); + this.cwd = ''; + } + + initializing() { + if (this._isInvalidWorkspace()) { + throw new Error(chalk.red('Execution failed, because a ".yo-rc.json" and ".yo-rc-workspace.json" file was found. If this directory is a workspace, please remove the ".yo-rc.json" file and try again.\n')); + } + + this.composeWith(['phovea:_check-own-version', 'phovea:check-node-version']); + + this.config.defaults({ + type: this.type + }); + } + + _isWorkspace() { + return fs.existsSync(this.destinationPath('.yo-rc-workspace.json')); + } + + _hasConfigFile() { + return fs.existsSync(this.destinationPath('.yo-rc.json')); + } + + /** + * If there is both a `.yo-rc-workspace.json` and `.yo-rc.json` file in the current directory + * the workspace is invalid and the generator cannot function properly. + */ + _isInvalidWorkspace() { + return this._isWorkspace() && this._hasConfigFile(); + } + + /** + * Create a subdirectory in the current directory. + * Initialize the property cwd. + * @param {string} dir Directory name. + */ + async _createSubDir(dir) { + if (this._isWorkspace() && this.cwd !== dir + '/') { + this.cwd = dir + '/'; + return GeneratorUtils.mkdir(dir); + } + return Promise.resolve(); + } + + readmeAddon() { + const f = this.templatePath('README.partial.md'); + if (fs.existsSync(f)) { + return this.fs.read(f); + } + return ''; + } + + default() { + this.composeWith('phovea:_init-' + this.basetype, { + options: Object.assign({ + readme: this.readmeAddon() + (this.options.readme ? `\n\n${this.options.readme}` : '') + }, this.options), + isWorkspace: this._isWorkspace() // inform the sub generator that the cwd is the workspace to avoid reading prompt default values from the workspace package.json + }); + } + + async writing() { + const config = this.config.getAll(); + await this._createSubDir(config.app || config.serviceName || config.name); + if (fs.existsSync(this.templatePath('package.tmpl.json'))) { + this._patchPackageJSON(config, null, null, null, this.cwd); + } + if (fs.existsSync(this.templatePath('_gitignore'))) { + this.fs.copy(this.templatePath('_gitignore'), this.destinationPath(this.cwd + '.gitignore')); + } + + this._writeTemplates(config, !this.options.noSamples, this.cwd); + + } +} + +module.exports = BaseInitPluginGenerator; \ No newline at end of file diff --git a/base/BaseInitServerGenerator.js b/base/BaseInitServerGenerator.js new file mode 100644 index 00000000..f384884e --- /dev/null +++ b/base/BaseInitServerGenerator.js @@ -0,0 +1,26 @@ +'use strict'; + +const BaseInitPluginGenerator = require('./BaseInitPluginGenerator'); +const {basetype} = require('./config'); + +class BaseInitServerGenerator extends BaseInitPluginGenerator { + + constructor(args, options) { + super(args, options, basetype.PYTHON); + } + + initializing() { + // since just last in the hierarchy used, need to do super calls + super.initializing(); + } + + default() { + return super.default(); + } + + writing() { + return super.writing(); + } +} + +module.exports = BaseInitServerGenerator; \ No newline at end of file diff --git a/base/BasePhoveaGenerator.js b/base/BasePhoveaGenerator.js new file mode 100644 index 00000000..fac15df6 --- /dev/null +++ b/base/BasePhoveaGenerator.js @@ -0,0 +1,98 @@ +'use strict'; +const Generator = require('yeoman-generator'); +const {merge, template} = require('lodash'); +const path = require('path'); +const glob = require('glob').sync; +const fs = require('fs-extra'); +const GeneratorUtils = require('../utils/GeneratorUtils'); + +class BasePhoveaGenerator extends Generator { + + /** + * Modify package.json by passing the configuration. + * + * @param {object} config Current configuration + * @param {*} unset + * @param {*} extra + * @param {*} replaceExtra + * @param {string} cwd The directory from which the generator is being called, i.e., `tdp_core/`. + * If cwd is provided than the `package.json` is going to be written to that subdirectory, otherwise to the current directory. + */ + _patchPackageJSON(config, unset, extra, replaceExtra, cwd = '') { + const pkg = this.fs.readJSON(this.destinationPath(cwd + 'package.json'), {}); + let pkgPatch; + if (fs.existsSync(this.templatePath('package.tmpl.json'))) { + pkgPatch = JSON.parse(template(this.fs.read(this.templatePath('package.tmpl.json')))(config)); + } else { + pkgPatch = {}; + } + merge(pkg, pkgPatch); + if (replaceExtra && extra) { + Object.assign(pkg, extra); + } else { + merge(pkg, extra || {}); + } + + (unset || []).forEach((d) => delete pkg[d]); + + this.fs.writeJSON(this.destinationPath(cwd + 'package.json'), pkg); + } + + /** + * Copies the template files to the current directory or to a subdirectory if `cwd` is provided. + * @param {object} config Current configuration + * @param {*} withSamples + * @param {string} cwd The directory from which the generator is being called, i.e., `tdp_core/`. + * If `cwd` is provided than the `package.json` is going to be written to that subdirectory, otherwise to the current directory. + */ + _writeTemplates(config, withSamples, cwd = '') { + const includeDot = { + globOptions: { + dot: true + } + }; + + const pattern = GeneratorUtils.stringifyAble(config); + + const copyTpl = (base, dbase, initialize_once) => { + // see https://github.com/SBoudrias/mem-fs-editor/issues/25 + // copyTpl doesn't support glob options + const f = glob(base + '/**/*', { + dot: true, + nodir: true + }); + f.forEach((fi) => { + const rel = path.relative(base, fi); + if (!initialize_once || !fs.existsSync(this.destinationPath(cwd + dbase + rel))) { + this.fs.copyTpl(fi, this.destinationPath(cwd + dbase + rel), pattern); + } + }); + }; + + const copy = (prefix) => { + if (fs.existsSync(this.templatePath(prefix + 'plain'))) { + this.fs.copy(this.templatePath(prefix + 'plain/**/*'), this.destinationPath(cwd), includeDot); + } + + const plainTemplatePath = this.templatePath(prefix + 'plain_initialize_once'); + if (fs.existsSync(plainTemplatePath)) { + copyTpl(plainTemplatePath, '', true); + } + + copyTpl(this.templatePath(prefix + 'processed'), '', false); + + if (config.name) { + if (fs.existsSync(this.templatePath(prefix + 'pluginname_plain'))) { + this.fs.copy(this.templatePath(prefix + 'pluginname_plain/**/*'), this.destinationPath(cwd + config.name.toLowerCase() + '/'), includeDot); + } + copyTpl(this.templatePath(prefix + 'pluginname_processed'), config.name.toLowerCase() + '/', false); + } + }; + copy(''); + if (withSamples) { + copy('sample_'); + } + } +} + +module.exports = BasePhoveaGenerator; \ No newline at end of file diff --git a/base/config.js b/base/config.js new file mode 100644 index 00000000..e74de0dc --- /dev/null +++ b/base/config.js @@ -0,0 +1,13 @@ + +/** + * Base classes types each plugin type can belong to. + */ +const basetype = { + PYTHON: 'python', + WEB: 'web', + HYBRID: 'hybrid' +}; + +module.exports = { + basetype +}; diff --git a/generators/_init-hybrid/index.js b/generators/_init-hybrid/index.js index 9209b476..6194e2ef 100644 --- a/generators/_init-hybrid/index.js +++ b/generators/_init-hybrid/index.js @@ -1,9 +1,9 @@ 'use strict'; -const Base = require('../../utils').Base; const known = () => require('../../utils/known'); -const {toLibraryAliasMap, toLibraryExternals} = require('../_init-web'); +const BaseInitPluginGenerator = require('../../base/BaseInitPluginGenerator'); +const RepoUtils = require('../../utils/RepoUtils'); -class Generator extends Base { +class Generator extends BaseInitPluginGenerator { initializing() { this.config.defaults({ @@ -31,8 +31,8 @@ class Generator extends Base { this.config.set('modules', props.modules); this.config.set('libraries', props.libraries); } - this.config.set('libraryAliases', toLibraryAliasMap.call(this, this.config.get('modules'), this.config.get('libraries'))); - this.config.set('libraryExternals', toLibraryExternals.call(this, this.config.get('modules'), this.config.get('libraries'))); + this.config.set('libraryAliases', RepoUtils.toLibraryAliasMap(this.config.get('modules'), this.config.get('libraries'))); + this.config.set('libraryExternals', RepoUtils.toLibraryExternals(this.config.get('modules'), this.config.get('libraries'))); }); } diff --git a/generators/_init-python/index.js b/generators/_init-python/index.js index c5be5940..b0de8053 100644 --- a/generators/_init-python/index.js +++ b/generators/_init-python/index.js @@ -1,13 +1,14 @@ 'use strict'; const _ = require('lodash'); -const Base = require('yeoman-generator'); -const {writeTemplates, patchPackageJSON, stringifyAble, useDevVersion} = require('../../utils'); -const {parseRequirements} = require('../../utils/pip'); +const PipUtils = require('../../utils/PipUtils'); +const NpmUtils = require('../../utils/NpmUtils'); +const GeneratorUtils = require('../../utils/GeneratorUtils'); const fs = require('fs'); +const BasePhoveaGenerator = require('../../base/BasePhoveaGenerator'); const known = () => require('../../utils/known'); -class Generator extends Base { +class Generator extends BasePhoveaGenerator { constructor(args, options) { super(args, options); @@ -66,8 +67,8 @@ class Generator extends Base { } _generateDependencies(useDevelopDependencies, cwd) { - let requirements = parseRequirements(this.fs.read(this.destinationPath(cwd + 'requirements.txt'), {defaults: ''})); - const dockerPackages = parseRequirements(this.fs.read(this.destinationPath(cwd + 'docker_packages.txt'), {defaults: ''})); + let requirements = PipUtils.parseRequirements(this.fs.read(this.destinationPath(cwd + 'requirements.txt'), {defaults: ''})); + const dockerPackages = PipUtils.parseRequirements(this.fs.read(this.destinationPath(cwd + 'docker_packages.txt'), {defaults: ''})); const concat = (p) => Object.keys(p).map((pi) => pi + p[pi]); // merge dependencies @@ -78,9 +79,9 @@ class Generator extends Base { modules.filter(known().plugin.isTypeServer).forEach((m) => { const p = known().plugin.byName(m); - // avoid having a requirement twice in two different formats that occurs when in the requirements.txt a requirement is written - // in the format -e git+https://github.com/phovea/phovea_server.git@v2.2.0#egg=phovea_server - // and the incoming format is phovea_server>=5.0.1,<6.0.0 + // avoid having a requirement twice in two different formats that occurs when in the requirements.txt a requirement is written + // in the format -e git+https://github.com/phovea/phovea_server.git@v2.2.0#egg=phovea_server + // and the incoming format is phovea_server>=5.0.1,<6.0.0 if (!useDevelopDependencies) { const devRequirement = Object.keys(p.develop.requirements)[0]; const masterRequirment = Object.keys(p.requirements)[0]; @@ -105,18 +106,19 @@ class Generator extends Base { } writing() { const config = this.config.getAll(); - this.cwd = this.options.isWorkspace ? (config.cwd || config.name) + '/' : ''; - const deps = this._generateDependencies(useDevVersion.call(this, this.cwd), this.cwd); + this.cwd = this.options.isWorkspace ? (config.app || config.serviceName || config.name) + '/' : ''; + const {version} = this.fs.readJSON(this.destinationPath(this.cwd + 'package.json'), {version: '1.0.0'}); + const deps = this._generateDependencies(NpmUtils.useDevVersion(version), this.cwd); - patchPackageJSON.call(this, config, ['devDependencies'], null, null, this.cwd); - writeTemplates.call(this, config, !this.options.noSamples, this.cwd); + this._patchPackageJSON(config, ['devDependencies'], null, null, this.cwd); + this._writeTemplates.call(this, config, !this.options.noSamples, this.cwd); this.fs.write(this.destinationPath(this.cwd + 'requirements.txt'), deps.requirements.join('\n')); this.fs.write(this.destinationPath(this.cwd + 'docker_packages.txt'), deps.dockerPackages.join('\n')); // don't overwrite existing registry file if (!fs.existsSync(this.destinationPath(this.cwd + config.name.toLowerCase() + '/__init__.py'))) { - this.fs.copyTpl(this.templatePath('__init__.tmpl.py'), this.destinationPath(this.cwd + config.name.toLowerCase() + '/__init__.py'), stringifyAble(config)); + this.fs.copyTpl(this.templatePath('__init__.tmpl.py'), this.destinationPath(this.cwd + config.name.toLowerCase() + '/__init__.py'), GeneratorUtils.stringifyAble(config)); } this.fs.copy(this.templatePath('_gitignore'), this.destinationPath(this.cwd + '.gitignore')); this.fs.copy(this.templatePath('docs_gitignore'), this.destinationPath(this.cwd + 'docs/.gitignore')); diff --git a/generators/_init-web/index.js b/generators/_init-web/index.js index f89113f9..9c9ed19a 100644 --- a/generators/_init-web/index.js +++ b/generators/_init-web/index.js @@ -1,57 +1,15 @@ 'use strict'; const _ = require('lodash'); const chalk = require('chalk'); -const Base = require('yeoman-generator'); -const {writeTemplates, patchPackageJSON, stringifyAble, useDevVersion} = require('../../utils'); const fs = require('fs'); - +const NpmUtils = require('../../utils/NpmUtils'); +const SpawnUtils = require('../../utils/SpawnUtils'); +const RepoUtils = require('../../utils/RepoUtils'); +const GeneratorUtils = require('../../utils/GeneratorUtils'); +const BasePhoveaGenerator = require('../../base/BasePhoveaGenerator'); const known = () => require('../../utils/known'); -function toLibraryAliasMap(moduleNames = [], libraryNames = []) { - let r = {}; - moduleNames.forEach((m) => { - const plugin = known().plugin.byName(m); - if (!plugin) { - this.log('cant find plugin: ', m); - return; - } - libraryNames.push(...(plugin.libraries || [])); - }); - libraryNames.forEach((l) => { - const lib = known().lib.byName(l); - if (!lib) { - this.log('cant find library: ', l); - return; - } - _.merge(r, lib.aliases); - }); - return r; -} - -function toLibraryExternals(moduleNames = [], libraryNames = []) { - let r = []; - moduleNames.forEach((m) => { - const plugin = known().plugin.byName(m); - if (!plugin) { - this.log('cant find plugin: ', m); - return; - } - r.push(...(plugin.externals || [])); - libraryNames.push(...(plugin.libraries || [])); - }); - libraryNames.forEach((l) => { - const lib = known().lib.byName(l); - if (!lib) { - this.log('cant find library: ', l); - return; - } - r.push(lib.name); - r.push(...(lib.externals || [])); - }); - return Array.from(new Set(r)); -} - -class Generator extends Base { +class Generator extends BasePhoveaGenerator { constructor(args, options) { super(args, options); @@ -99,8 +57,8 @@ class Generator extends Base { this.config.set('modules', props.modules); this.config.set('libraries', props.libraries); } - this.config.set('libraryAliases', toLibraryAliasMap.call(this, this.config.get('modules'), this.config.get('libraries'))); - this.config.set('libraryExternals', toLibraryExternals.call(this, this.config.get('modules'), this.config.get('libraries'))); + this.config.set('libraryAliases', RepoUtils.toLibraryAliasMap(this.config.get('modules'), this.config.get('libraries'))); + this.config.set('libraryExternals', RepoUtils.toLibraryExternals(this.config.get('modules'), this.config.get('libraries'))); }); } @@ -129,23 +87,24 @@ class Generator extends Base { writing() { const config = this.config.getAll(); - this.cwd = this.options.isWorkspace ? (config.app || config.name) + '/' : ''; - patchPackageJSON.call(this, config, [], { - dependencies: this._generateDependencies(useDevVersion.call(this, this.cwd)) + this.cwd = this.options.isWorkspace ? (config.app || config.serviceName|| config.name) + '/' : ''; + const {version} = this.fs.readJSON(this.destinationPath(this.cwd + 'package.json'), {version: '1.0.0'}); + + this._patchPackageJSON(config, [], { + dependencies: this._generateDependencies(NpmUtils.useDevVersion(version)) }, null, this.cwd); this.fs.copy(this.templatePath('_gitignore'), this.destinationPath(this.cwd + '.gitignore')); - writeTemplates.call(this, config, null, this.cwd); + this._writeTemplates.call(this, config, null, this.cwd); // do not overwrite existing registry file if (!fs.existsSync(this.destinationPath(this.cwd + 'src/phovea.ts'))) { - this.fs.copyTpl(this.templatePath('phovea.tmpl.ts'), this.destinationPath(this.cwd + 'src/phovea.ts'), stringifyAble(config)); + this.fs.copyTpl(this.templatePath('phovea.tmpl.ts'), this.destinationPath(this.cwd + 'src/phovea.ts'), GeneratorUtils.stringifyAble(config)); } } install() { if (this.options.options.install) { - const options = this.cwd ? {cwd: this.cwd} : {}; - this.spawnCommand("npm", ["install"], options); + SpawnUtils.spawnSync('npm', 'install', this.cwd, true); } } @@ -153,13 +112,11 @@ class Generator extends Base { if (fs.existsSync(this.destinationPath(this.cwd + 'phovea.js'))) { this.log('\r\n'); this.log(chalk.red(`ACTION REQUIRED!`)); - this.log(chalk.default(`Please migrate the content of`), chalk.yellow(`phovea.js`), chalk.default(`to`), chalk.yellow(`/src/phovea.ts`) + chalk.default(` now!`)); - this.log(chalk.default(`Afterwards you can remove the`), chalk.yellow(`phovea.js`), chalk.default(`file from this plugin repository.`)); - this.log(chalk.default(`If you do not migrate the content the registered extension points will be unavailable.`)); + this.log(chalk.white(`Please migrate the content of`), chalk.yellow(`phovea.js`), chalk.white(`to`), chalk.yellow(`/src/phovea.ts`) + chalk.white(` now!`)); + this.log(chalk.white(`Afterwards you can remove the`), chalk.yellow(`phovea.js`), chalk.white(`file from this plugin repository.`)); + this.log(chalk.white(`If you do not migrate the content the registered extension points will be unavailable.`)); } } } module.exports = Generator; -module.exports.toLibraryAliasMap = toLibraryAliasMap; -module.exports.toLibraryExternals = toLibraryExternals; diff --git a/generators/_init-web/templates/plain/buildInfo.js b/generators/_init-web/templates/plain/buildInfo.js deleted file mode 100644 index b9bb0042..00000000 --- a/generators/_init-web/templates/plain/buildInfo.js +++ /dev/null @@ -1,170 +0,0 @@ -/** - * Created by sam on 13.11.2016. - */ - -const spawnSync = require('child_process').spawnSync; -const path = require('path'); -const resolve = path.resolve; -const fs = require('fs'); - -function dependencyGraph(cwd) { - const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'; - const r = spawnSync(npm, ['ls', '--prod', '--json'], { - cwd: cwd - }); - if (!r.stdout) { - console.error(cwd, r.error); - return {}; - } - return JSON.parse(r.stdout.toString()); -} - -function gitHead(cwd) { - if (!fs.existsSync(cwd + '/.git')) { - return null; - } - const r = spawnSync('git', ['rev-parse', '--verify', 'HEAD'], { - cwd: cwd - }); - if (!r.stdout) { - console.error(cwd, r.error); - return {}; - } - return r.stdout.toString().trim(); -} - -function resolveModules() { - const reg = fs.readFileSync('../phovea_registry.js').toString(); - const regex = /^import '(.*)\/phovea_registry.js'/gm; - const modules = []; - let r; - while ((r = regex.exec(reg)) !== null) { - modules.push(r[1]); - } - return modules; -} - -function cleanupDependency(d) { - return d; -} - -function resolveWorkspace() { - const workspaceDeps = dependencyGraph('..').dependencies; - const modules = new Set(resolveModules()); - - let deps = null; - const resolveModule = (m) => { - const pkg = JSON.parse(fs.readFileSync(`../${m}/package.json`).toString()); - const head = gitHead('../' + m); - const repo = pkg.repository.url; - return { - name: pkg.name, - version: pkg.version, - resolved: head ? `${repo.endsWith('.git') ? repo.slice(0, repo.length - 4) : repo}/commit/${head}` : pkg.version, - dependencies: deps(pkg.dependencies) - }; - }; - deps = (deps) => { - const r = {}; - Object.keys(deps).forEach((d) => { - if (d in workspaceDeps) { - r[d] = cleanupDependency(workspaceDeps[d]); - delete workspaceDeps[d]; - } else if (modules.has(d)) { - modules.delete(d); - r[d] = resolveModule(d); - } else { - r[d] = '-> link'; - } - }); - return r; - }; - - // self = - const root = path.basename(process.cwd()); - modules.delete(root); - const base = resolveModule(root); - base.extraDependencies = {}; - while (modules.size > 0) { - let m = Array.from(modules.keys())[0]; - modules.delete(m); - base.extraDependencies[m] = resolveModule(m); - } - return base; -} - -function resolveSingle() { - const self = dependencyGraph('.'); - const pkg = require(`./package.json`); - const head = gitHead('.'); - const deps = {}; - Object.keys(self.dependencies || {}).forEach((d) => { - deps[d] = cleanupDependency(self.dependencies[d]); - }); - return { - name: self.name, - version: pkg.version, - resolved: head ? `${pkg.repository.url}#${head}` : pkg.version, - dependencies: deps, - extraDependencies: {} - }; -} - -function generate() { - const isWorkspaceContext = fs.existsSync('../phovea_registry.js'); - if (isWorkspaceContext) { - return resolveWorkspace(); - } - return resolveSingle(); -} - -const IS_WINDOWS = process.platform === 'win32'; - -function tmpdir() { - if (IS_WINDOWS) { - return process.env.TEMP || process.env.TMP || - (process.env.SystemRoot || process.env.windir) + '\\temp'; - } - return process.env.TMPDIR || process.env.TMP || process.env.TEMP || '/tmp'; -} - -function resolveScreenshot() { - const f = resolve(__dirname, 'media/screenshot.png'); - if (!fs.existsSync(f)) { - return null; - } - const buffer = Buffer.from(fs.readFileSync(f)).toString('base64'); - return `data:image/png;base64,${buffer}`; -} - -function metaData(pkg) { - pkg = pkg || require(`./package.json`); - return { - name: pkg.name, - displayName: pkg.displayName, - version: pkg.version, - repository: pkg.repository.url, - homepage: pkg.homepage, - description: pkg.description, - screenshot: resolveScreenshot() - }; -} - -module.exports.metaData = metaData; -module.exports.metaDataTmpFile = function (pkg) { - const s = metaData(pkg); - const file = `${tmpdir()}/metaData${Math.random().toString(36).slice(-8)}.txt`; - fs.writeFileSync(file, JSON.stringify(s, null, ' ')); - return file; -}; -module.exports.generate = generate; -module.exports.tmpFile = function () { - const s = generate(); - const file = `${tmpdir()}/buildInfo${Math.random().toString(36).slice(-8)}.txt`; - fs.writeFileSync(file, JSON.stringify(s, null, ' ')); - return file; -}; - -if (require.main === module) { - fs.writeFile('deps.json', JSON.stringify(generate(), null, ' ')); -} diff --git a/generators/_init-web/templates/plain/tsconfig.json b/generators/_init-web/templates/plain/tsconfig.json index c1be4463..e1cadbab 100644 --- a/generators/_init-web/templates/plain/tsconfig.json +++ b/generators/_init-web/templates/plain/tsconfig.json @@ -18,7 +18,7 @@ "esModuleInterop": false, "resolveJsonModule": true, "allowSyntheticDefaultImports": true, - "preserveWatchOutput":true + "preserveWatchOutput": true }, "include": [ "src/**/*.ts", diff --git a/generators/_node/index.js b/generators/_node/index.js index 84f6ca61..a5192294 100644 --- a/generators/_node/index.js +++ b/generators/_node/index.js @@ -1,14 +1,13 @@ 'use strict'; const _ = require('lodash'); const parseAuthor = require('parse-author'); -const Base = require('yeoman-generator'); -const patchPackageJSON = require('../../utils').patchPackageJSON; const originUrl = require('git-remote-origin-url'); const fs = require('fs-extra'); +const BasePhoveaGenerator = require('../../base/BasePhoveaGenerator'); // based on https://github.com/yeoman/generator-node/blob/master/generators/app/index.js -class PackageJSONGenerator extends Base { +class PackageJSONGenerator extends BasePhoveaGenerator { constructor(args, options) { super(args, options); @@ -135,13 +134,13 @@ class PackageJSONGenerator extends Base { writing() { const config = _.extend({}, this.props, this.config.getAll()); - this.cwd = this.isWorkspace ? (config.cwd || config.name) + '/' : ''; // use config.cwd for init-app or init-service generators and config.name for the rest. + this.cwd = this.isWorkspace ? (config.app || config.serviceName || config.name) + '/' : ''; if (this.originUrl) { config.repository = this.originUrl; } else { config.repository = `https://github.com/${config.githubAccount}/${config.name}.git`; } - patchPackageJSON.call(this, config, null, null, null, this.cwd); + this._patchPackageJSON(config, null, null, null, this.cwd); config.content = this.options.readme || ''; config.longDescription = this.options.longDescription || this.props.description || ''; diff --git a/generators/add-extension/index.js b/generators/add-extension/index.js index d9270cd2..e4ddbde4 100644 --- a/generators/add-extension/index.js +++ b/generators/add-extension/index.js @@ -2,39 +2,11 @@ const Base = require('yeoman-generator'); const plugins = require('../../utils/types').plugin; -const stringifyAble = require('../../utils').stringifyAble; const path = require('path'); const fs = require('fs'); const glob = require('glob').sync; const chalk = require('chalk'); - -function toJSONFromText(text) { - const r = {}; - text.split('\n').forEach((line) => { - const trimmedLine = line.trim(); - if (trimmedLine.length === 0) { // ignore empty lines (e.g. new line added by editor) - return; - } - - const splitPoint = trimmedLine.indexOf('='); - const key = trimmedLine.slice(0, splitPoint); - let value = trimmedLine.slice(splitPoint + 1); - value = value.trim(); - if (!isNaN(parseFloat(value))) { - value = parseFloat(value); - } - let obj = r; - const keys = key.trim().split('.'); - keys.slice(0, keys.length - 1).forEach((k) => { - if (!(k in obj)) { - obj[k] = {}; - } - obj = obj[k]; - }); - obj[keys[keys.length - 1]] = value; - }); - return r; -} +const GeneratorUtils = require('../../utils/GeneratorUtils'); /** * The outside key in the `.yo-rc.json` file where the configuration is saved. @@ -170,7 +142,7 @@ class Generator extends Base { type: props.type, id: props.id, module: props.module, - extras: toJSONFromText(props.extras) + extras: GeneratorUtils.toJSONFromText(props.extras) }; }); } @@ -182,7 +154,7 @@ class Generator extends Base { this._writeConfig(this.cwd, basekey, arr); // inject new extension - const d = stringifyAble(this.new_); + const d = GeneratorUtils.stringifyAble(this.new_); if (this.basetype === 'web') { this._injectWebExtension(d, this.cwd); @@ -215,7 +187,7 @@ class Generator extends Base { if (fs.existsSync(cwd + 'src/phovea.ts')) { absFile = d.module.startsWith('~') ? d.module.slice(1) : `./${d.module.includes('.') ? d.module.slice(0, d.module.lastIndexOf('.')) : d.module}`; - importFunction = `() => System.import('${absFile}')`; // TODO remove System.import for Typescript case when switching to Webpack 4 (see https://github.com/phovea/generator-phovea/issues/286#issuecomment-566553497) + importFunction = `() => import('${absFile}')`; } else { absFile = d.module.startsWith('~') ? d.module.slice(1) : `./src/${d.module.includes('.') ? d.module.slice(0, d.module.lastIndexOf('.')) : d.module}`; importFunction = `function() { return import('${absFile}'); }`; diff --git a/generators/clone-repo/index.js b/generators/clone-repo/index.js index ae89f042..ab76af82 100644 --- a/generators/clone-repo/index.js +++ b/generators/clone-repo/index.js @@ -1,14 +1,9 @@ 'use strict'; const Base = require('yeoman-generator'); const chalk = require('chalk'); -const { - simplifyRepoUrl -} = require('../../utils/repo'); -const version = require('../../utils/version'); - -function failed(spawnResult) { - return spawnResult.status !== 0; -} +const RepoUtils = require('../../utils/RepoUtils'); +const NpmUtils = require('../../utils/NpmUtils'); +const SpawnUtils = require('../../utils/SpawnUtils'); /** * Clone a given repository and supports version ranges for git tags. @@ -105,30 +100,30 @@ class Generator extends Base { } _cloneRepo(repoUrl, branch, extras, cloneDirName) { - if (!version.isGitCommit(branch)) { + if (!NpmUtils.isGitCommit(branch)) { // modify branch name, if it is an advance version tag // otherwise just use the branch name as it is - if (version.isAdvancedVersionTag(branch)) { + if (NpmUtils.isAdvancedVersionTag(branch)) { this.log(chalk.white(`found branch with version range`), chalk.green(branch), chalk.white(`for`), chalk.green(repoUrl)); const line = `ls-remote --tags ${repoUrl}`; this.log(chalk.white(`fetching possible version tags:`), `git ${line}`); - const r = this._spawn('git', line.split(/ +/)); + const r = SpawnUtils.spawnSync('git', line.split(/ +/), this.cwd); - if (failed(r)) { + if (SpawnUtils.failed(r)) { this.log(chalk.red(`failed to fetch list of tags from git repository`), `status code: ${r.status}`); this.log(r.stderr.toString()); - return this._abort(`failed to fetch list of tags from git repository - status code: ${r.status}`); + return SpawnUtils.abort(`failed to fetch list of tags from git repository - status code: ${r.status}`); } const gitLog = r.stdout.toString(); - const gitVersions = version.extractVersionsFromGitLog(gitLog); + const gitVersions = NpmUtils.extractVersionsFromGitLog(gitLog); this.log(chalk.white(`found the following version tags: `), gitVersions); - const highestVersion = version.findHighestVersion(gitVersions, branch); + const highestVersion = NpmUtils.findHighestVersion(gitVersions, branch); if (!highestVersion) { this.log(chalk.red(`failed to find git version tag for given version range`)); - return this._abort(`failed to find git version tag for given version range`); + return SpawnUtils.abort(`failed to find git version tag for given version range`); } this.log(chalk.white(`use version tag`), chalk.green(highestVersion), chalk.white(`as branch name`)); @@ -137,43 +132,23 @@ class Generator extends Base { const line = `clone -b ${branch}${extras || ''} ${repoUrl}${cloneDirName}`; this.log(chalk.white(`clone repository:`), `git ${line}`); - return this._spawnOrAbort('git', line.split(/ +/)); + return SpawnUtils.spawnOrAbort('git', line.split(/ +/), this.cwd); } + // clone a specific commit const line = `clone ${extras || ''} ${repoUrl}${cloneDirName}`; this.log(chalk.white(`clone repository:`), `git ${line}`); - return this._spawnOrAbort('git', line.split(/ +/)).then(() => { + + return SpawnUtils.spawnOrAbort('git', line.split(/ +/), this.cwd).then(() => { const line = `checkout ${branch}`; this.log(chalk.white(`checkout commit:`), `git ${line}`); - let repoName = simplifyRepoUrl(repoUrl); + let repoName = RepoUtils.simplifyRepoUrl(repoUrl); repoName = repoName.slice(repoName.lastIndexOf('/') + 1); - return this._spawnOrAbort('git', line.split(/ +/), { - cwd: `${this.cwd}/${repoName}` - }); + const cwd = this.cwd ? `${this.cwd}/${repoName}` : repoName; + return SpawnUtils.spawnOrAbort('git', line.split(/ +/), cwd); }); } - _abort(msg) { - return Promise.reject(msg ? msg : 'Step Failed: Aborting'); - } - - _spawn(cmd, argline, cwd) { - const options = cwd === false ? {} : Object.assign({ - cwd: this.cwd, - stdio: ['inherit', 'pipe', 'pipe'] // pipe `stdout` and `stderr` to host process - }, cwd || {}); - return this.spawnCommandSync(cmd, Array.isArray(argline) ? argline : argline.split(' '), options); - } - - _spawnOrAbort(cmd, argline, cwd) { - const r = this._spawn(cmd, argline, cwd); - if (failed(r)) { - this.log(r.stderr.toString()); - return this._abort(`failed: "${cmd} ${Array.isArray(argline) ? argline.join(' ') : argline}" - status code: ${r.status}`); - } - return Promise.resolve(r); - } - } module.exports = Generator; diff --git a/generators/clone/index.js b/generators/clone/index.js index cedcc7ce..39378526 100644 --- a/generators/clone/index.js +++ b/generators/clone/index.js @@ -1,54 +1,9 @@ 'use strict'; const Base = require('yeoman-generator'); -const path = require('path'); -const glob = require('glob').sync; const known = require('../../utils/known'); -const {toHTTPRepoUrl, toSSHRepoUrl} = require('../../utils/repo'); -function toRepository(plugin, useSSH) { - const p = known.plugin.byName(plugin); - return useSSH ? toSSHRepoUrl(p.repository) : toHTTPRepoUrl(p.repository); -} - -function resolveNeighbors(plugins, useSSH, types, shallow) { - let missing = []; - const addMissing = (p) => { - this.log(this.destinationPath(p + '/.yo-rc.json')); - const config = this.fs.readJSON(this.destinationPath(p + '/.yo-rc.json'), {'generator-phovea': {}})['generator-phovea']; - let modules = [].concat(config.modules || [], config.smodules || []); - this.log(`${p} => ${modules.join(' ')}`); - if (types && types !== 'both') { - // filter to just certain sub types - const filter = types === 'web' ? known.plugin.isTypeWeb : known.plugin.isTypeServer; - modules = modules.filter((m) => known.plugin.isTypeHybrid(m) || filter(m)); - } - missing.push(...modules.filter((m) => plugins.indexOf(m) < 0)); - }; - - plugins.forEach(addMissing); - - while (missing.length > 0) { - let next = missing.shift(); - let repo = toRepository(next, useSSH); - let args = ['clone', repo]; - if (shallow) { - args.splice(1, 0, '--depth', '1'); - } - this.log(`git clone ${args.join(' ')}`); - this.spawnCommandSync('git', args, { - cwd: this.destinationPath() - }); - plugins.push(next); - addMissing(next); - } -} +const RepoUtils = require('../../utils/RepoUtils'); +const WorkspaceUtils = require('../../utils/WorkspaceUtils'); -function resolveAllNeighbors(useSSH, types) { - const files = glob('*/.yo-rc.json', { - cwd: this.destinationPath() - }); - const plugins = files.map(path.dirname); - return resolveNeighbors.call(this, plugins, useSSH, types); -} class Generator extends Base { constructor(args, options) { @@ -130,7 +85,7 @@ class Generator extends Base { } writing() { - const repos = this.props.plugins.map((d) => toRepository(d, this.props.cloneSSH)); + const repos = this.props.plugins.map((d) => RepoUtils.toRepository(d, this.props.cloneSSH)); repos.forEach((repo) => { this.log(`git clone ${repo}`); @@ -139,11 +94,9 @@ class Generator extends Base { }); }); if (this.props.resolve) { - resolveNeighbors.call(this, this.props.plugins, this.props.cloneSSH); + WorkspaceUtils.resolveNeighbors(this.props.plugins, this.props.cloneSSH, null, null, this.destinationPath()); } } } -module.exports = Generator; -module.exports.resolveNeighbors = resolveNeighbors; -module.exports.resolveAllNeighbors = resolveAllNeighbors; +module.exports = Generator; \ No newline at end of file diff --git a/generators/init-app-slib/index.js b/generators/init-app-slib/index.js index e4737495..2868d0d6 100644 --- a/generators/init-app-slib/index.js +++ b/generators/init-app-slib/index.js @@ -1,7 +1,8 @@ 'use strict'; -const BaseHybrid = require('../../utils').BaseHybrid; -class PluginGenerator extends BaseHybrid { +const BaseInitHybridGenerator = require('../init-lib-service'); + +class PluginGenerator extends BaseInitHybridGenerator { initializing() { super.initializing(); diff --git a/generators/init-app/index.js b/generators/init-app/index.js index c83cc486..5a6b53dd 100644 --- a/generators/init-app/index.js +++ b/generators/init-app/index.js @@ -1,8 +1,8 @@ 'use strict'; -const BasePluginGenerator = require('../../utils').Base; +const BaseInitPluginGenerator = require('../../base/BaseInitPluginGenerator'); const chalk = require('chalk'); -class PluginGenerator extends BasePluginGenerator { +class PluginGenerator extends BaseInitPluginGenerator { initializing() { super.initializing(); @@ -39,7 +39,6 @@ class PluginGenerator extends BasePluginGenerator { }]).then((props) => { this.config.set('app', props.app); this.config.set('clientOnly', props.clientOnly); - this.config.set('cwd', props.app); }); } @@ -47,10 +46,10 @@ class PluginGenerator extends BasePluginGenerator { super.default(); } - writing() { + async writing() { const config = this.config.getAll(); - this._mkdir(config.cwd); - this._patchPackageJSON(config, ['main'], null, this.cwd); + await this._createSubDir(config.app); + this._patchPackageJSON(config, ['main'], null, null, this.cwd); this._writeTemplates(config, !this.options.noSamples, this.cwd); } diff --git a/generators/init-bundle/index.js b/generators/init-bundle/index.js index 70fc429d..8a3100af 100644 --- a/generators/init-bundle/index.js +++ b/generators/init-bundle/index.js @@ -1,4 +1,4 @@ 'use strict'; -var BasePluginGenerator = require('../../utils').Base; +const BaseInitPluginGenerator = require('../../base/BaseInitPluginGenerator'); -module.exports = BasePluginGenerator; +module.exports = BaseInitPluginGenerator; diff --git a/generators/init-lib-service/index.js b/generators/init-lib-service/index.js index 1d3a52a1..c7ee5dfa 100644 --- a/generators/init-lib-service/index.js +++ b/generators/init-lib-service/index.js @@ -1,4 +1,4 @@ 'use strict'; -var BaseHybrid = require('../../utils').BaseHybrid; +const BaseInitHybridGenerator = require('../../base/BaseInitHybridGenerator'); -module.exports = BaseHybrid; +module.exports = BaseInitHybridGenerator; diff --git a/generators/init-lib-slib/index.js b/generators/init-lib-slib/index.js index 1d3a52a1..c7ee5dfa 100644 --- a/generators/init-lib-slib/index.js +++ b/generators/init-lib-slib/index.js @@ -1,4 +1,4 @@ 'use strict'; -var BaseHybrid = require('../../utils').BaseHybrid; +const BaseInitHybridGenerator = require('../../base/BaseInitHybridGenerator'); -module.exports = BaseHybrid; +module.exports = BaseInitHybridGenerator; diff --git a/generators/init-lib/index.js b/generators/init-lib/index.js index 4d653310..643c6e0a 100644 --- a/generators/init-lib/index.js +++ b/generators/init-lib/index.js @@ -1,7 +1,7 @@ 'use strict'; -const BasePluginGenerator = require('../../utils').Base; const chalk = require('chalk'); -class PluginGenerator extends BasePluginGenerator { +const BaseInitPluginGenerator = require('../../base/BaseInitPluginGenerator'); +class PluginGenerator extends BaseInitPluginGenerator { initializing() { return super.initializing(); diff --git a/generators/init-product/index.js b/generators/init-product/index.js index 659b0551..a29c0c1e 100644 --- a/generators/init-product/index.js +++ b/generators/init-product/index.js @@ -1,26 +1,14 @@ /** * Created by Samuel Gratzl on 28.11.2016. */ -const Base = require('yeoman-generator'); -const {writeTemplates, patchPackageJSON} = require('../../utils'); -const {simplifyRepoUrl} = require('../../utils/repo'); +const WorkspaceUtils = require('../../utils/WorkspaceUtils'); const chalk = require('chalk'); const fs = require('fs'); +const BasePhoveaGenerator = require('../../base/BasePhoveaGenerator'); const isRequired = (v) => v.toString().length > 0; -function buildPossibleAdditionalPlugins(type) { - const toDescription = (d) => ({ - value: {name: d.name, repo: simplifyRepoUrl(d.repository)}, - name: `${d.name}: ${d.description}`, - short: d.name - }); - - const plugins = require('../../utils/known').plugin; - return ((type === 'web' || type === 'static') ? plugins.listWeb : plugins.listServer).map(toDescription); -} - -class Generator extends Base { +class Generator extends BasePhoveaGenerator { initializing() { if (fs.existsSync(this.destinationPath('.yo-rc-workspace.json'))) { @@ -95,7 +83,7 @@ class Generator extends Base { name: 'additional', type: 'checkbox', message: 'additional plugins: ', - choices: (act) => buildPossibleAdditionalPlugins(act.type) + choices: (act) => WorkspaceUtils.buildPossibleAdditionalPlugins(act.type) }, { name: 'custom', type: 'confirm', @@ -123,8 +111,8 @@ class Generator extends Base { writing() { const config = this.config.getAll(); - patchPackageJSON.call(this, config); - writeTemplates.call(this, config); + this._patchPackageJSON.call(this, config); + this._writeTemplates.call(this, config); this.fs.copy(this.templatePath('_gitignore'), this.destinationPath('.gitignore')); // don't overwrite existing registry file if (!fs.existsSync(this.destinationPath('phovea_product.json'))) { diff --git a/generators/init-product/templates/plain/Jenkinsfile b/generators/init-product/templates/plain/Jenkinsfile deleted file mode 100644 index 44e97420..00000000 --- a/generators/init-product/templates/plain/Jenkinsfile +++ /dev/null @@ -1,46 +0,0 @@ -node { - stage('Checkout') { - checkout scm - } - - stage('Before Install') { - def nodeHome = tool 'node-v7' - env.PATH="${env.PATH}:${nodeHome}/bin" - def dockerHome = tool 'docker' - env.PATH="${env.PATH}:${dockerHome}/bin" - } - - stage('Install') { - sh 'node -v' - sh 'npm --version' - sh 'docker --version' - sh 'npm install' - } - - stage('Build') { - try { - withCredentials([usernameColonPassword(credentialsId: 'PHOVEA_GITHUB_CREDENTIALS', variable: 'PHOVEA_GITHUB_CREDENTIALS')]) { - docker.withRegistry("https://922145058410.dkr.ecr.eu-central-1.amazonaws.com", "ecr:eu-central-1:PHOVEA_AWS_CREDENTIALS") { - docker.withRegistry("", "PHOVEA_DOCKER_HUB_CREDENTIALS") { - wrap([$class: 'Xvfb']) { - sh 'node build.js --skipTests --skipSaveImage --noDefaultTags --pushExtra=latest --pushTo=922145058410.dkr.ecr.eu-central-1.amazonaws.com/caleydo' - } - } - } - } - currentBuild.result = "SUCCESS" - } catch (e) { - // if any exception occurs, mark the build as failed - currentBuild.result = 'FAILURE' - throw e - } finally { - // always clean up - sh 'npm prune' - sh 'rm node_modules -rf' - } - } - - stage('Post Build') { - archiveArtifacts artifacts: 'build/*' - } -} diff --git a/generators/init-service/index.js b/generators/init-service/index.js index 58c969d1..82ac211d 100644 --- a/generators/init-service/index.js +++ b/generators/init-service/index.js @@ -1,7 +1,8 @@ 'use strict'; -const BasePluginGenerator = require('../../utils').BasePython; -class Generator extends BasePluginGenerator { +const BaseInitServerGenerator = require('../../base/BaseInitServerGenerator'); + +class Generator extends BaseInitServerGenerator { initializing() { super.initializing(); this.config.defaults({ @@ -20,7 +21,6 @@ class Generator extends BasePluginGenerator { default: this.config.get('serviceName') }]).then(({serviceName}) => { this.config.set('serviceName', serviceName); - this.config.set('cwd', serviceName); }); } diff --git a/generators/init-slib/index.js b/generators/init-slib/index.js index dde05c85..529d768d 100644 --- a/generators/init-slib/index.js +++ b/generators/init-slib/index.js @@ -1,8 +1,8 @@ 'use strict'; -const BasePluginGenerator = require('../../utils').BasePython; const fs = require('fs'); +const BaseInitServerGenerator = require('../../base/BaseInitServerGenerator'); -class PluginGenerator extends BasePluginGenerator { +class PluginGenerator extends BaseInitServerGenerator { initializing() { super.initializing(); @@ -12,10 +12,13 @@ class PluginGenerator extends BasePluginGenerator { return super.default(); } - writing() { - super.writing(); - if (!fs.existsSync(this.destinationPath(this.config.get('name') + '/config.json'))) { - this.fs.writeJSON(this.destinationPath(this.config.get('name') + '/config.json'), {}); + async writing() { + await super.writing(); + const config = this.config.getAll(); + const cwd = this.destinationPath(this._isWorkspace() ? (config.app || config.serviceName || config.name) + '/' + config.name.toLowerCase() : config.name); + if (!fs.existsSync(cwd + '/config.json')) { + await this._createSubDir(cwd); + this.fs.writeJSON(cwd + '/config.json', {}); } } } diff --git a/generators/install/index.js b/generators/install/index.js index 8f059219..973a7ecb 100644 --- a/generators/install/index.js +++ b/generators/install/index.js @@ -2,6 +2,7 @@ const Base = require('yeoman-generator'); const path = require('path'); const fs = require('fs'); +const GeneratorUtils = require('../../utils/GeneratorUtils'); function toPluginRepo(url, useSSH) { const match = url.match(/(github:)?([\w\d-_]+\/)?([\w\d-_]+)(#.+)?/); @@ -50,27 +51,6 @@ class Generator extends Base { this.composeWith(['phovea:_check-own-version', 'phovea:check-node-version']); } - _yo(generator, options) { - const yeoman = require('yeoman-environment'); - // call yo internally - const env = yeoman.createEnv([], { - cwd: this.cwd - }, this.env.adapter); - env.register(require.resolve('../' + generator), 'phovea:' + generator); - return new Promise((resolve, reject) => { - try { - this.log('running yo phovea:' + generator); - env.run('phovea:' + generator, options || {}, () => { - // wait a second after running yo to commit the files correctly - setTimeout(() => resolve(), 500); - }); - } catch (e) { - console.error('error', e, e.stack); - reject(e); - } - }); - } - default() { this.plugin = this.options.for; @@ -101,7 +81,7 @@ class Generator extends Base { return p; }).filter((n) => Boolean(n)); this.log('updating workspace'); - return this._yo('workspace').then(() => { + return GeneratorUtils.yo('workspace').then(() => { this.log('running npm install'); this.npmInstall(); }); diff --git a/generators/prepare-release/index.js b/generators/prepare-release/index.js index ec144065..94faea28 100644 --- a/generators/prepare-release/index.js +++ b/generators/prepare-release/index.js @@ -2,8 +2,8 @@ const Base = require('yeoman-generator'); const chalk = require('chalk'); const fs = require('fs-extra'); -const {parseRequirements} = require('../../utils/pip'); -const {toHTTPRepoUrl, toSSHRepoUrl, simplifyRepoUrl} = require('../../utils/repo'); +const PipUtils = require('../../utils/PipUtils'); +const RepoUtils = require('../../utils/RepoUtils'); function toBaseName(name) { if (name.includes('/')) { @@ -137,7 +137,7 @@ class Generator extends Base { } _cloneRepo(repo, branch, extras) { - const repoUrl = this.cloneSSH ? toSSHRepoUrl(repo) : toHTTPRepoUrl(repo); + const repoUrl = this.cloneSSH ? RepoUtils.toSSHRepoUrl(repo) : RepoUtils.toHTTPRepoUrl(repo); const line = `clone -b ${branch}${extras || ''} ${repoUrl}`; this.log(chalk.blue(`clone repository:`), `git ${line}`); return this._spawnOrAbort('git', line.split(' ')); @@ -222,7 +222,7 @@ class Generator extends Base { if (fs.existsSync(`${ctx.cwd}/requirements.txt`)) { ctx.requirements = {}; - const req = parseRequirements(this.fs.read(`${ctx.cwd}/requirements.txt`)); + const req = PipUtils.parseRequirements(this.fs.read(`${ctx.cwd}/requirements.txt`)); p = Promise.all(Object.keys(req).map((dep) => { if (dependenciesToIgnores.some((d) => dep.includes(d))) { return null; @@ -294,7 +294,7 @@ class Generator extends Base { }); this.fs.writeJSON(`${ctx.cwd}/package.json`, pkg); if (ctx.requirements) { - const req = parseRequirements(this.fs.read(`${ctx.cwd}/requirements.txt`)); + const req = PipUtils.parseRequirements(this.fs.read(`${ctx.cwd}/requirements.txt`)); Object.keys(ctx.requirements).forEach((dep) => { const depVersion = req[dep]; if (depVersion && depVersion !== '') { @@ -332,7 +332,7 @@ class Generator extends Base { _createPullRequest(ctx) { const open = require('open'); - const base = simplifyRepoUrl(ctx.repo); + const base = RepoUtils.simplifyRepoUrl(ctx.repo); const url = `https://github.com/${base}/compare/release_${ctx.version}?expand=1`; return open(url, { wait: false @@ -362,7 +362,7 @@ class Generator extends Base { _openReleasePage(ctx) { const open = require('open'); - const base = simplifyRepoUrl(ctx.repo); + const base = RepoUtils.simplifyRepoUrl(ctx.repo); const url = `https://github.com/${base}/releases/tag/v${ctx.version}`; return open(url, { wait: false diff --git a/generators/resolve/index.js b/generators/resolve/index.js index f40de609..222e225d 100644 --- a/generators/resolve/index.js +++ b/generators/resolve/index.js @@ -67,7 +67,7 @@ class Generator extends Base { } writing() { - resolveAllNeighbors.call(this, this.props.cloneSSH, this.props.type, this.options.shallow); + resolveAllNeighbors(this.props.cloneSSH, this.props.type, this.options.shallow, this.destinationPath()); } } diff --git a/generators/setup-workspace/index.js b/generators/setup-workspace/index.js index 7ef8c40d..7272aa17 100644 --- a/generators/setup-workspace/index.js +++ b/generators/setup-workspace/index.js @@ -3,43 +3,11 @@ const Base = require('yeoman-generator'); const chalk = require('chalk'); const fs = require('fs-extra'); const path = require('path'); -const yeoman = require('yeoman-environment'); -const { - toHTTPRepoUrl, - toSSHRepoUrl, - simplifyRepoUrl -} = require('../../utils/repo'); - -function toBaseName(name) { - if (name.includes('/')) { - return name; - } - return `Caleydo/${name}`; -} - -function failed(spawnResult) { - return spawnResult.status !== 0; -} - -function toCWD(basename) { - let match = basename.match(/.*\/(.*)/)[1]; - if (match.endsWith('_product')) { - match = match.slice(0, -8); - } - return match; -} - -function findDefaultApp(product) { - if (!product) { - return null; - } - for (let p of product) { - if (p.type === 'web') { - return p.repo.slice(p.repo.lastIndexOf('/') + 1).replace('.git', ''); - } - } - return null; -} +const RepoUtils = require('../../utils/RepoUtils'); +const SpawnUtils = require('../../utils/SpawnUtils'); +const WorkspaceUtils = require('../../utils/WorkspaceUtils'); +const GeneratorUtils = require('../../utils/GeneratorUtils'); +const {findDefaultApp} = require('../../utils/WorkspaceUtils'); function downloadFileImpl(url, dest) { const http = require(url.startsWith('https') ? 'https' : 'http'); @@ -75,7 +43,6 @@ function toDownloadName(url) { } return url.substring(url.lastIndexOf('/') + 1); } - class Generator extends Base { constructor(args, options) { @@ -123,137 +90,87 @@ class Generator extends Base { default: this.options.ssh, when: !this.options.ssh }]).then((props) => { - this.productName = toBaseName(props.productName || this.args[0]); - this.cwd = toCWD(this.productName); + this.productName = RepoUtils.toBaseName(props.productName || this.args[0]); + this.cwd = RepoUtils.toCWD(this.productName); this.cloneSSH = props.cloneSSH || this.options.ssh; }); } - _yo(generator, options, args) { - // call yo internally - const env = yeoman.createEnv([], { - cwd: this.cwd - }, this.env.adapter); - const _args = Array.isArray(args) ? args.join(' ') : args || ''; - return new Promise((resolve, reject) => { - try { - this.log(`Running: yo phovea:${generator} ${_args}`); - env.lookup(() => { - env.run(`phovea:${generator} ${_args}`, options || {}, () => { - // wait a second after running yo to commit the files correctly - setTimeout(() => resolve(), 500); - }); - }); - } catch (e) { - console.error('Error', e, e.stack); - reject(e); - } - }); - } - - _abort(msg) { - return Promise.reject(msg ? msg : 'Step Failed: Aborting'); - } - - _spawn(cmd, argline, cwd) { - const options = cwd === false ? {} : Object.assign({ - cwd: this.cwd, - stdio: 'inherit' // log output and error of spawned process to host process - }, cwd || {}); - - this.log(`\nRunning: ${cmd} ${argline}\n`); - return this.spawnCommandSync(cmd, Array.isArray(argline) ? argline : argline.split(' '), options); - } - - _spawnOrAbort(cmd, argline, cwd) { - const r = this._spawn(cmd, argline, cwd); - if (failed(r)) { - this.log(r.stderr.toString()); - return this._abort(`Failed: "${cmd} ${Array.isArray(argline) ? argline.join(' ') : argline}" - status code: ${r.status}`); - } else if(r.stdout) { - this.log(r.stdout.toString()); + /** + * Removes/renames files of the cloned product that conflict with the workspace files. + */ + _removeUnnecessaryProductFiles() { + if (fs.existsSync(this.cwd + '/.yo-rc.json')) { + fs.unlinkSync(this.cwd + '/.yo-rc.json'); } - return Promise.resolve(cmd); - } - - _cloneRepo(repo, branch, extras, dir = '') { - const repoUrl = this.cloneSSH ? toSSHRepoUrl(repo) : toHTTPRepoUrl(repo); - return this._yo(`clone-repo`, { - branch, - extras: extras || '', - dir, - cwd: this.cwd - }, repoUrl); // repository URL as argument - } - - _removeUnnecessaryProductFiles() { - fs.unlinkSync(this.cwd + '/.yo-rc.json'); - fs.rmdirSync(this.cwd + '/.git', {recursive: true}); // TODO look into git submodules - fs.renameSync(this.cwd + '/package.json', this.cwd + '/package_product.json'); + fs.rmdirSync(this.cwd + '/.git', { recursive: true }); // TODO look into git submodules + fs.renameSync(this.cwd + '/package.json', this.cwd + '/package_product.json'); } /** - * Copies the template files of the product in the workspace - * @param {string} templatePath + * Copies/merges the template files of the product into the workspace. + * @param {string} templatePath Path to the template directory of the product. */ _copyProductTemplates(templatePath) { const dirs = fs.readdirSync(templatePath).filter(f => fs.statSync(path.join(templatePath, f)).isDirectory()); - dirs.forEach((dir) => fs.copySync(templatePath +'/' + dir, this.destinationPath(this.cwd))); + dirs.forEach((dir) => fs.copySync(templatePath + '/' + dir, this.destinationPath(this.cwd))); } - + /** + * Clones the product into the workspace and generates the `yo-rc-workspace.json` file. + * @returns The parsed phovea_product.json file. + */ _getProduct() { - return this._cloneRepo(this.productName, this.options.branch || 'master', null, '.') + return WorkspaceUtils.cloneRepo(this.productName, this.options.branch || 'master', null, '.', this.cwd, this.cloneSSH) .then(() => { this._removeUnnecessaryProductFiles(); - const phoveaProductJSON = `${this.cwd}/phovea_product.json`; if (!fs.existsSync(phoveaProductJSON)) { throw new Error('No phovea_product.json file found! Did you enter a valid phovea product repository?'); } this.product = fs.readJSONSync(phoveaProductJSON); + const defaultApp = findDefaultApp(this.product); + this.defaultApp = defaultApp.name; - const defaultApp = this.product.find((v) => v.type === 'web'); - if (defaultApp) { - const baseRepo = simplifyRepoUrl(defaultApp.repo); - const defaultAppName = baseRepo.slice(baseRepo.lastIndexOf('/') + 1); - this.defaultApp = defaultAppName; - const yoWorkspacePath = this.destinationPath(`${this.cwd}/.yo-rc-workspace.json`); - if(!fs.existsSync(yoWorkspacePath)) { - fs.writeJsonSync(yoWorkspacePath, { - modules: [], - defaultApp: defaultAppName, - frontendRepos: defaultApp.additional.map((repo) => repo.name), - devRepos: [defaultAppName] - }, {spaces: 2}); - } - } - - return this.product; + this._createYoRcWorkspace(defaultApp); + return RepoUtils.parsePhoveaProduct(this.product); }); } - _mkdir(dir) { - dir = dir || this.cwd; - this.log('Create directory: ' + dir); - return new Promise((resolve) => fs.ensureDir(dir, resolve)); + /** + * Generates `yo-rc-workspace.json` file + * @param {string} defaultApp + */ + _createYoRcWorkspace(defaultApp) { + const yoWorkspacePath = this.destinationPath(`${this.cwd}/.yo-rc-workspace.json`); + if (!fs.existsSync(yoWorkspacePath && this.defaultApp)) { + const frontendRepos = defaultApp.additional; + fs.writeJsonSync(yoWorkspacePath, { + modules: [], + defaultApp: this.defaultApp, + frontendRepos, + devRepos: [this.defaultApp] + }, {spaces: 2}); + } } + /** + * Copies the generator's and the product's template files into the workspace. + */ _customizeWorkspace() { - const defaultApp = findDefaultApp(this.product); - if (defaultApp) { - this.fs.copyTpl(this.templatePath('start_defaultapp.tmpl.xml'), this.destinationPath(`${this.cwd}/.idea/runConfigurations/start_${defaultApp}.xml`), { - defaultApp: defaultApp + if (this.defaultApp) { + this.fs.copyTpl(this.templatePath('start_defaultapp.tmpl.xml'), this.destinationPath(`${this.cwd}/.idea/runConfigurations/start_${this.defaultApp}.xml`), { + defaultApp: this.defaultApp }); - this.fs.copyTpl(this.templatePath('lint_defaultapp.tmpl.xml'), this.destinationPath(`${this.cwd}/.idea/runConfigurations/lint_${defaultApp}.xml`), { - defaultApp: defaultApp + this.fs.copyTpl(this.templatePath('lint_defaultapp.tmpl.xml'), this.destinationPath(`${this.cwd}/.idea/runConfigurations/lint_${this.defaultApp}.xml`), { + defaultApp: this.defaultApp }); } const productTemplatesPath = this.destinationPath(`${this.cwd}/templates`); - if (fs.existsSync(productTemplatesPath)){ + if (fs.existsSync(productTemplatesPath)) { this._copyProductTemplates(productTemplatesPath); } } @@ -300,7 +217,7 @@ class Generator extends Base { if (data.length === 0) { return Promise.resolve(null); } - return this._mkdir(this.cwd + '/_data') + return GeneratorUtils.mkdir(this.cwd + '/_data') .then(() => Promise.all(data.map((d) => this._downloadDataFile(d, this.cwd + '/_data')))); } @@ -314,11 +231,17 @@ class Generator extends Base { if (data.length === 0) { return Promise.resolve(null); } - return this._mkdir(this.cwd + '/_backup') + return GeneratorUtils.mkdir(this.cwd + '/_backup') .then(() => Promise.all(data.map((d) => this._downloadBackupFile(d, this.cwd + '/_backup')))) - .then(this._ifExecutable.bind(this, 'docker-compose', this._spawnOrAbort.bind(this, './docker-backup', 'restore'), 'please execute: "./docker-backup restore" manually')); + .then(this._ifExecutable.bind(this, 'docker-compose', () => SpawnUtils.spawnOrAbort('./docker-backup', 'restore', this.cwd, true), 'please execute: "./docker-backup restore" manually')); } + /** + * Checks whether a command/cli tool is installed in the current system and executes provided command. + * @param {string} cmd Cmd i.e, `docker-compose`. + * @param {Function} ifExists Spawn this command if the cmd is executable. + * @param {string} extraMessage Message to log if the cmd was not found. + */ _ifExecutable(cmd, ifExists, extraMessage = '') { const paths = process.env.PATH.split(path.delimiter); const pathExt = (process.env.PATHEXT || '').split(path.delimiter); @@ -343,55 +266,32 @@ class Generator extends Base { return ifExists(); } + /** + * Runs `docker-compose build` if `docker-compose.yml` exists in the workspace and user has installed `dokcer-compose` cli. + */ + _buildDockerCompose() { + const file = this.fs.read(this.destinationPath(`${this.cwd}/docker-compose.yml`), {defaults: ''}); + const isNotEmpty = file.trim().length > 0; + if (isNotEmpty && !this.options.skip.includes('build')) { + return this._ifExecutable('docker-compose', () => SpawnUtils.spawnOrAbort('docker-compose', 'build', this.cwd, true), ' please run "docker-compose build" manually"'); + } + + return null; + } + writing() { this.hasErrors = false; return Promise.resolve(1) - .then(this._mkdir.bind(this, null)) + .then(GeneratorUtils.mkdir(this.cwd)) .then(this._getProduct.bind(this)) - .then((product) => { - const names = new Set(); - const repos = []; - product.forEach((p) => { - const repo = p.repo || 'phovea/' + p.name; - if (!names.has(repo)) { - names.add(repo); - repos.push({ - repo, - branch: p.branch || 'master' - }); - } - (p.additional || []).forEach((pi) => { - const repo = pi.repo || 'phovea/' + pi.name; - if (!names.has(repo)) { - names.add(repo); - repos.push({ - repo, - branch: pi.branch || 'master' - }); - } - }); - }); - return repos; - }) - .then((repos) => Promise.all(repos.map((r) => this._cloneRepo(r.repo, r.branch)))) - .then(this._yo.bind(this, 'workspace', { - defaultApp: findDefaultApp(), - skipNextStepsLog: true // skip "next steps" logs from yo phovea:workspace - })) + .then((repos) => Promise.all(repos.map((r) => WorkspaceUtils.cloneRepo(r.repo, r.branch, null, '', this.cwd, this.cloneSSH)))) + .then(() => GeneratorUtils.yo('workspace', {defaultApp: this.defaultApp, skipNextStepsLog: true}, null, this.cwd, this.env.adapter)) .then(this._customizeWorkspace.bind(this)) .then(this._downloadDataFiles.bind(this)) - .then(() => this.options.skip.includes('install') ? null : this._spawnOrAbort('npm', 'install')) + .then(() => this.options.skip.includes('install') ? null : SpawnUtils.spawnOrAbort('npm', 'install', this.cwd, true)) .then(this._downloadBackupFiles.bind(this)) - .then(() => { - const l = this.fs.read(this.destinationPath(`${this.cwd}/docker-compose.yml`), { - defaults: '' - }); - if (l.trim().length > 0 && !this.options.skip.includes('build')) { - return this._ifExecutable('docker-compose', this._spawnOrAbort.bind(this, 'docker-compose', 'build'), ' please run "docker-compose build" manually"'); - } - return null; - }) + .then(this._buildDockerCompose.bind(this)) .catch((msg) => { this.log('\r\n'); this.log(chalk.red(msg)); @@ -400,7 +300,7 @@ class Generator extends Base { } end() { - if(this.hasErrors) { + if (this.hasErrors) { return; // skip next steps on errors } diff --git a/generators/workspace/index.js b/generators/workspace/index.js index 8b77f6c1..8050db6b 100644 --- a/generators/workspace/index.js +++ b/generators/workspace/index.js @@ -1,5 +1,4 @@ 'use strict'; -const Base = require('yeoman-generator'); const path = require('path'); const glob = require('glob').sync; const chalk = require('chalk'); @@ -8,10 +7,9 @@ const _ = require('lodash'); const fs = require('fs'); const known = () => require('../../utils/known'); -const writeTemplates = require('../../utils').writeTemplates; -const patchPackageJSON = require('../../utils').patchPackageJSON; -const {mergeVersions, mergePipVersions} = require('../../utils/version'); -const {parseRequirements} = require('../../utils/pip'); +const NpmUtils = require('../../utils/NpmUtils'); +const PipUtils = require('../../utils/PipUtils'); +const BasePhoveaGenerator = require('../../base/BasePhoveaGenerator'); function mergeWith(target, source) { const mergeArrayUnion = (a, b) => Array.isArray(a) ? _.union(a, b) : undefined; @@ -42,7 +40,7 @@ function rewriteDockerCompose(compose) { return compose; } -class Generator extends Base { +class Generator extends BasePhoveaGenerator { constructor(args, options) { super(args, options); @@ -99,7 +97,7 @@ class Generator extends Base { default: defaultConfig.addWorkspaceRepos, description: 'States whether workspace repos should be part of the dependencies. Set to `true` for local development setup. Otherwise `false` for CI build process.' }); - + } initializing() { @@ -158,14 +156,14 @@ class Generator extends Base { plugins.map((plugin) => { return `./${plugin}/src`; }), - 'extensions': 'html,scss,css', - 'quiet': false, - 'legacyWatch': true, - 'delay': 2500, - 'runOnChangeOnly': true + 'extensions': 'html,scss,css', + 'quiet': false, + 'legacyWatch': true, + 'delay': 2500, + 'runOnChangeOnly': true } }; - const repoDependencies = Object.assign({},...plugins.map((plugin) => ({[plugin]: `./${plugin}`}))); + const repoDependencies = Object.assign({}, ...plugins.map((plugin) => ({[plugin]: `./${plugin}`}))); const integrateMulti = (target, source) => { Object.keys(source || {}).forEach((key) => { @@ -205,7 +203,7 @@ class Generator extends Base { if (this.props.defaultApp) { const workspaceFile = this.fs.readJSON(this.destinationPath('.yo-rc-workspace.json')); devRepos = workspaceFile && workspaceFile.devRepos ? workspaceFile.devRepos : [this.props.defaultApp]; - if(devRepos.indexOf(this.props.defaultApp)<0) devRepos.push(this.props.defaultApp); + if (devRepos.indexOf(this.props.defaultApp) < 0) devRepos.push(this.props.defaultApp); devRepos = devRepos.filter((plugin) => plugins.indexOf(plugin) >= 0); //add dev-repos scripts scripts['dev-repos:compile'] = devRepos.map((repo) => `npm run compile:${repo}`).join(' & '); @@ -215,11 +213,11 @@ class Generator extends Base { //add watch watch['dev-repos:copy'] = { 'patterns': devRepos.map((repo) => `./${repo}/src`), - 'extensions': 'html,scss,css', - 'quiet': false, - 'legacyWatch': true, - 'delay': 2500, - 'runOnChangeOnly': true + 'extensions': 'html,scss,css', + 'quiet': false, + 'legacyWatch': true, + 'delay': 2500, + 'runOnChangeOnly': true }; // enforce that the dependencies of the default app are the last one to have a setup suitable for the default app thus more predictable const pkg = this.fs.readJSON(this.destinationPath(this.props.defaultApp + '/package.json')); @@ -240,10 +238,10 @@ class Generator extends Base { }); Object.keys(dependencies).forEach((key) => { - dependencies[key] = mergeVersions(key, dependencies[key]); + dependencies[key] = NpmUtils.mergeVersions(key, dependencies[key]); }); Object.keys(devDependencies).forEach((key) => { - devDependencies[key] = mergeVersions(key, devDependencies[key]); + devDependencies[key] = NpmUtils.mergeVersions(key, devDependencies[key]); }); // dependencies from package.tmpl.json @@ -253,7 +251,7 @@ class Generator extends Base { // scripts from package.tmpl.json const extraScripts = this.fs.readJSON(this.templatePath('package.tmpl.json')).scripts; - return {plugins, dependencies: Object.assign(Object.assign(dependencies, extraDependencies), this.options.addWorkspaceRepos ? repoDependencies : {}), devDependencies: Object.assign(devDependencies, extraDevDependencies), scripts: Object.assign(scripts, extraScripts), watch, devRepos}; + return {plugins, dependencies: Object.assign(Object.assign(dependencies, extraDependencies), this.options.addWorkspaceRepos ? repoDependencies : {}), devDependencies: Object.assign(devDependencies, extraDevDependencies), scripts: Object.assign(scripts, extraScripts), watch, devRepos}; } _generateServerDependencies(additionalPlugins) { @@ -289,8 +287,8 @@ class Generator extends Base { set.add(ri.trim()); }); }; - integrateMulti(requirements, parseRequirements(this.fs.read(this.destinationPath(`${p}/requirements.txt`), {defaults: ''}))); - integrateMulti(devRequirements, parseRequirements(this.fs.read(this.destinationPath(`${p}/requirements_dev.txt`), {defaults: ''}))); + integrateMulti(requirements, PipUtils.parseRequirements(this.fs.read(this.destinationPath(`${p}/requirements.txt`), {defaults: ''}))); + integrateMulti(devRequirements, PipUtils.parseRequirements(this.fs.read(this.destinationPath(`${p}/requirements_dev.txt`), {defaults: ''}))); addAll('docker_packages.txt', dockerPackages); const script = this.fs.read(this.destinationPath(`${p}/docker_script.sh`), {defaults: ''}); @@ -397,10 +395,10 @@ class Generator extends Base { }); Object.keys(requirements).forEach((key) => { - requirements[key] = mergePipVersions(key, requirements[key]); + requirements[key] = PipUtils.mergePipVersions(key, requirements[key]); }); Object.keys(devRequirements).forEach((key) => { - devRequirements[key] = mergePipVersions(key, devRequirements[key]); + devRequirements[key] = PipUtils.mergePipVersions(key, devRequirements[key]); }); return { @@ -451,17 +449,17 @@ class Generator extends Base { const {plugins, dependencies, devDependencies, scripts, watch, devRepos} = this._generateWebDependencies(this.props.modules); const sdeps = this._generateServerDependencies(this.props.modules); const dockerWebHint = - ' # Uncomment the following lines for testing the web production build\n' + - ' # web:\n' + - ' # build:\n' + - ' # context: ./deploy/web\n' + - ' # dockerfile: deploy/web/Dockerfile\n' + - ' # ports:\n' + - ' # - \'8090:80\'\n' + - ' # volumes:\n' + - ' # - \'./bundles:/usr/share/nginx/html\'\n' + - ' # depends_on:\n' + - ' # - api\n'; + ' # Uncomment the following lines for testing the web production build\n' + + ' # web:\n' + + ' # build:\n' + + ' # context: ./deploy/web\n' + + ' # dockerfile: deploy/web/Dockerfile\n' + + ' # ports:\n' + + ' # - \'8090:80\'\n' + + ' # volumes:\n' + + ' # - \'./bundles:/usr/share/nginx/html\'\n' + + ' # depends_on:\n' + + ' # - api\n'; // save config this.fs.extendJSON(this.destinationPath('.yo-rc-workspace.json'), { @@ -485,8 +483,8 @@ class Generator extends Base { this._patchDockerImages(patch, sdeps.dockerCompose); } { - this.fs.write(this.destinationPath('docker-compose.yml'), yaml.stringify(sdeps.dockerCompose, 100, 2) ); - this.fs.write(this.destinationPath('docker-compose-debug.yml'), yaml.stringify(sdeps.dockerComposeDebug, 100, 2)+ dockerWebHint); + this.fs.write(this.destinationPath('docker-compose.yml'), yaml.stringify(sdeps.dockerCompose, 100, 2)); + this.fs.write(this.destinationPath('docker-compose-debug.yml'), yaml.stringify(sdeps.dockerComposeDebug, 100, 2) + dockerWebHint); } const config = {}; @@ -498,9 +496,9 @@ class Generator extends Base { config.wsDescription = this.options.wsDescription; config.wsVersion = this.options.wsVersion; - writeTemplates.call(this, config, false); + this._writeTemplates(config, false); // replace the added entries - patchPackageJSON.call(this, config, [], {devDependencies, dependencies, scripts, watch}, true); + this._patchPackageJSON(config, [], {devDependencies, dependencies, scripts, watch}, true); if (!fs.existsSync(this.destinationPath('config.json'))) { this.fs.copy(this.templatePath('config.tmpl.json'), this.destinationPath('config.json')); diff --git a/package.json b/package.json index 080fdfd7..e8014f93 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ }, "main": "generators/app/index.js", "files": [ + "base/", "generators/", "utils/", "knownPhoveaPlugins.json", diff --git a/test/add-extension.test.js b/test/add-extension.test.js new file mode 100644 index 00000000..7f5305cd --- /dev/null +++ b/test/add-extension.test.js @@ -0,0 +1,185 @@ + +'use strict'; +const path = require('path'); +const assert = require('yeoman-assert'); +const helpers = require('yeoman-test'); +const fse = require('fs-extra'); +const TestUtils = require('./test-utils/TestUtils'); +const {template} = require('lodash'); +const dependencies = require('./test-utils/generator-dependencies'); +const {basetype} = require('../base/config'); + +describe('add a web extension to a web library', () => { + const tdpViewTmpl = template(fse.readFileSync(TestUtils.templatePath('add-extension', 'tdpView.tmpl.ts')))({moduleName: 'CustomView'}); + + const prompts = { + type: 'tdpView', + id: 'view_id', + module: 'CustomView', + extras: ` + idType=MYIDTYPE + load=true + config.database=MyDB + ` + }; + + beforeAll(async () => { + let workingDirectory; + // initialize a dummy web library + await helpers + .run(path.join(__dirname, '../generators/init-lib')) + .withGenerators(dependencies.INIT_LIB) + .inTmpDir((dir) => { + workingDirectory = dir; + }); + + // run yo phovea:add-extension in the same directory + await helpers + .run(path.join(__dirname, '../generators/add-extension')) + .withGenerators(dependencies.COMMON) + .inTmpDir(() => { + process.chdir(workingDirectory); + }) + .withPrompts(prompts); + }); + + it('generates module', () => { + assert.file('src/CustomView.ts'); + }); + + it('generated module uses the correct template', () => { + assert.fileContent('src/CustomView.ts', tdpViewTmpl); + }); + + it('registers the view in phovea.ts', () => { + const config = `registry.push('tdpView', 'view_id', () => import('./CustomView'), {\n 'idType': 'MYIDTYPE',\n 'load': true,\n 'config': {\n 'database': 'MyDB'\n }\n });`; + assert.fileContent('src/phovea.ts', config); + }); +}); + +describe('add a python extension to a python plugin', () => { + const prompts = { + type: 'tdp-sql-definition', + id: 'my_id', + module: 'custom', + extras: ` + autoUpgrade=false + ` + }; + beforeAll(async () => { + let workingDirectory; + // Initialize a dummy python library + await helpers + .run(path.join(__dirname, '../generators/init-slib')) + .withGenerators(dependencies.INIT_SLIB) + .inTmpDir((dir) => { + workingDirectory = dir; + }) + .withPrompts({name: 'server_plugin'}); + + // Run yo phovea:add-extension in the same directory + await helpers + .run(path.join(__dirname, '../generators/add-extension')) + .withGenerators(dependencies.COMMON) + .inTmpDir(() => { + process.chdir(workingDirectory); + }) + .withPrompts(prompts); + }); + + it('generates module', () => { + assert.file('server_plugin/custom.py'); + }); + + it('registers extension in __init__.py', () => { + const config = `registry.append('tdp-sql-definition', 'my_id', 'server_plugin.custom', {\n 'autoUpgrade': False\n })`; + assert.fileContent('server_plugin/__init__.py', config); + }); +}); + +describe('add a web extension to a hybrid plugin', () => { + const prompts = { + basetype: basetype.WEB, + type: 'tdpScore', + id: 'score_id', + module: 'SingleScore', + }; + beforeAll(async () => { + let workingDirectory; + // Initialize a dummy hybrid plugin + await helpers + .run(path.join(__dirname, '../generators/init-lib-slib')) + .withGenerators(dependencies.INIT_LIB_SLIB) + .inTmpDir((dir) => { + workingDirectory = dir; + }) + .withPrompts({name: 'hybrid_plugin'}); + + // Run yo phovea:add-extension in the same directory + await helpers + .run(path.join(__dirname, '../generators/add-extension')) + .withGenerators(dependencies.COMMON) + .inTmpDir(() => { + process.chdir(workingDirectory); + }) + .withPrompts(prompts); + }); + it('generates score module', () => { + assert.file('src/SingleScore.ts'); + }); + + it('registers the score in phovea.ts', () => { + const config = `registry.push('tdpScore', 'score_id', () => import('./SingleScore'), {});`; + assert.fileContent('src/phovea.ts', config); + }); +}); + +describe('add a web extension from the workspace', () => { + const cwd = process.cwd(); + const libPlugin = 'libPLugin'; + const prompts = { + basetype: basetype.WEB, + type: 'tdpScore', + id: 'score_id', + module: 'SingleScore', + plugin: libPlugin + }; + beforeAll(async () => { + let workingDirectory; + // Initialize a dummy lib plugin + await helpers + .run(path.join(__dirname, '../generators/init-lib')) + .withGenerators(dependencies.INIT_LIB) + .inTmpDir((dir) => { + workingDirectory = dir; + // simulate a workspace + fse.writeFileSync('.yo-rc-workspace.json', ''); + fse.mkdirSync(`./${libPlugin}`); + process.chdir(path.join(dir, libPlugin)); + }) + .withPrompts({name: 'plugin'}); + + // Run yo phovea:add-extension in the same directory + await helpers + .run(path.join(__dirname, '../generators/add-extension')) + .withGenerators(dependencies.COMMON) + .inTmpDir(() => { + process.chdir(workingDirectory); + }) + .withPrompts(prompts); + }); + + // Switch back the cwd + afterAll(() => { + process.chdir(cwd); + }); + + it('generates score module inside the plugin directory', () => { + assert.file(libPlugin + '/src/SingleScore.ts'); + }); + + it('registers the score in phovea.ts inside the plugin directory', () => { + const config = `registry.push('tdpScore', 'score_id', () => import('./SingleScore'), {});`; + assert.fileContent(libPlugin + '/src/phovea.ts', config); + }); +}); \ No newline at end of file diff --git a/test/check-node-version.test.js b/test/check-node-version.test.js new file mode 100644 index 00000000..aab9ad7a --- /dev/null +++ b/test/check-node-version.test.js @@ -0,0 +1,66 @@ + +'use strict'; +const path = require('path'); +const helpers = require('yeoman-test'); +const fs = require('fs'); +const installedVersions = require('../utils/installedVersions'); +const requiredNodeVersion = fs.readFileSync(path.resolve(__dirname, '../.nvmrc'), 'utf8'); +const requiredNpmVersion = fs.readFileSync(path.resolve(__dirname, '../.npm-version'), 'utf8'); + +jest.mock('check-node-version'); +const check = require('check-node-version'); + +installedVersions.checkRequiredVersion = jest.fn(); + +describe('check-node-version', () => { + const results = { + versions: { + node: { + version: { + version: 12 + }, + }, + npm: { + version: { + version: 18 + } + } + } + }; + + const versions = { + installed: { + node: results.versions.node.version.version, + npm: results.versions.npm.version.version + }, + required: { + node: requiredNodeVersion.replace('\n', ''), + npm: requiredNpmVersion.replace('\n', '') + } + }; + + check.mockImplementation((_, cb) => cb(false, results)); + + beforeAll(() => { + return helpers + .run(path.join(__dirname, '../generators/check-node-version')); + + }); + it('calls function check only once', () => { + expect(check.mock.calls.length).toBe(1); + }); + + it('calls function checkRequiredVersion once', () => { + expect(installedVersions.checkRequiredVersion.mock.calls.length).toBe(1); + }); + + it('calls checkRequiredVersion with correct argument', () => { + expect(installedVersions.checkRequiredVersion.mock.calls[0][0]).toMatchObject(versions); + }); + + // it('throws error if function check returns an error', () => { + // TODO do not terminate the node process in installedVersions since it also kills the test. Throw an error instead. + // check.mockImplementation((_, cb) => cb(true, null)); + // expect(installedVersions.checkRequiredVersion.mock.calls[0][0]).toMatchObject(versions); + // }); +}); diff --git a/test/clone-repo.test.js b/test/clone-repo.test.js new file mode 100644 index 00000000..b58df091 --- /dev/null +++ b/test/clone-repo.test.js @@ -0,0 +1,201 @@ + +'use strict'; +const path = require('path'); +const helpers = require('yeoman-test'); +const rimraf = require('rimraf'); +const SpawnUtils = require('../utils/SpawnUtils'); +const dependencies = require('./test-utils/generator-dependencies'); + +const repo = 'git@github.com:Caleydo/ordino.git'; +const target = '../cloned'; + +const cloneRepo = (options) => helpers + .run(path.join(__dirname, '../generators/clone-repo')) + .inDir(path.join(__dirname, target), () => null) + .withArguments([repo]) + .withOptions(options) + .withGenerators(dependencies.COMMON); + +describe('call clone-repo with branch develop', () => { + + beforeAll(() => { + SpawnUtils.spawnOrAbort = jest.fn(); + + return cloneRepo({branch: 'develop', extras: '--depth 1', dir: '.', cwd: 'ordino_workspace'}); + }); + + afterAll(() => { + rimraf.sync(path.join(__dirname, target)); + }); + + it('calls function spawnOrAbort() once with the correct argument', () => { + expect(SpawnUtils.spawnOrAbort.mock.calls.length).toBe(1); + const cmd = SpawnUtils.spawnOrAbort.mock.calls[0][0]; + expect(cmd).toBe('git'); + + const options = SpawnUtils.spawnOrAbort.mock.calls[0][1]; + expect(options).toStrictEqual(['clone', '-b', 'develop', '--depth', '1', repo, '.']); + + const cwd = SpawnUtils.spawnOrAbort.mock.calls[0][2]; + expect(cwd).toBe('ordino_workspace'); + }); +}); + +describe('call clone-repo with an exact version tag', () => { + + beforeAll(() => { + SpawnUtils.spawnOrAbort = jest.fn(); + return cloneRepo({branch: 'v2.0.0'}); + }); + + afterAll(() => { + rimraf.sync(path.join(__dirname, target)); + }); + + it('calls function spawnOrAbort() once with the correct arguments', () => { + expect(SpawnUtils.spawnOrAbort.mock.calls.length).toBe(1); + const cmd = SpawnUtils.spawnOrAbort.mock.calls[0][0]; + expect(cmd).toBe('git'); + + const options = SpawnUtils.spawnOrAbort.mock.calls[0][1]; + expect(options).toStrictEqual(['clone', '-b', 'v2.0.0', repo]); + + const cwd = SpawnUtils.spawnOrAbort.mock.calls[0][2]; + expect(cwd).toBe(undefined); + }); +}); + +describe('call clone-repo with an advanced version tag', () => { + + beforeAll(() => { + SpawnUtils.spawnOrAbort = jest.fn(); + SpawnUtils.spawnSync = jest.fn(); + SpawnUtils.spawnSync.mockImplementation(() => { + return { + status: 0, + stdout: ` + 336072e87ec8f6054cead9f64c6830897fb7f076 refs/tags/v2.0.0 + 8747a43780e4651542facd7b4feac7bcb8e3778d refs/tags/v2.0.1 + ` + }; + }); + return cloneRepo({branch: '^v2.0.0'}); + }); + + afterAll(() => { + rimraf.sync(path.join(__dirname, target)); + }); + + it('calls function spawnOrAbort() once with the the correctly resolved version tag', () => { + expect(SpawnUtils.spawnSync.mock.calls.length).toBe(1); + const cmd = SpawnUtils.spawnOrAbort.mock.calls[0][0]; + expect(cmd).toBe('git'); + + const options = SpawnUtils.spawnOrAbort.mock.calls[0][1]; + expect(options).toStrictEqual(['clone', '-b', 'v2.0.1', repo]); + + const cwd = SpawnUtils.spawnOrAbort.mock.calls[0][2]; + expect(cwd).toBe(undefined); + }); +}); + +describe('call clone-repo with an advanced version tag and no remote', () => { + + beforeAll(() => { + SpawnUtils.spawnOrAbort = jest.fn(); + SpawnUtils.abort = jest.fn(); + SpawnUtils.spawnSync = jest.fn(); + SpawnUtils.spawnSync.mockImplementation(() => { + return { + status: 128, + stderr: `some error` + }; + }); + return cloneRepo({branch: '^v2.0.0'}); + }); + + afterAll(() => { + rimraf.sync(path.join(__dirname, target)); + }); + + it('calls function abort() once and spawnOrAbort() never', () => { + expect(SpawnUtils.abort.mock.calls.length).toBe(1); + const msg = SpawnUtils.abort.mock.calls[0][0]; + expect(msg).toBe('failed to fetch list of tags from git repository - status code: 128'); + + expect(SpawnUtils.spawnOrAbort.mock.calls.length).toBe(0); + }); +}); + +describe('call clone-repo with an advanced version tag that does not resolve to an exact version tag', () => { + + beforeAll(() => { + SpawnUtils.spawnOrAbort = jest.fn(); + SpawnUtils.abort = jest.fn(); + SpawnUtils.spawnSync = jest.fn(); + SpawnUtils.spawnSync.mockImplementation(() => { + return { + status: 0, + stdout: ` + 336072e87ec8f6054cead9f64c6830897fb7f076 refs/tags/v2.0.0 + 8747a43780e4651542facd7b4feac7bcb8e3778d refs/tags/v2.0.1 + ` + }; + }); + return cloneRepo({branch: '^v3.0.0'}); + }); + + afterAll(() => { + rimraf.sync(path.join(__dirname, target)); + }); + + it('calls function abort() once and spawnOrAbort() never', () => { + expect(SpawnUtils.abort.mock.calls.length).toBe(1); + const msg = SpawnUtils.abort.mock.calls[0][0]; + expect(msg).toBe('failed to find git version tag for given version range'); + + expect(SpawnUtils.spawnOrAbort.mock.calls.length).toBe(0); + }); +}); + +describe('call clone-repo with a git commit', () => { + + + beforeAll(() => { + SpawnUtils.spawnOrAbort = jest.fn(); + SpawnUtils.spawnOrAbort.mockImplementation(() => Promise.resolve()); + return cloneRepo({branch: 'e7cfd95e0ff2188d006444f93ea2ed6aeac18864'}); + }); + + afterAll(() => { + rimraf.sync(path.join(__dirname, target)); + }); + + it('calls function spawnOrabort() twice', () => { + expect(SpawnUtils.spawnOrAbort.mock.calls.length).toBe(2); + + }); + + it('first it clones the the repo', () => { + const cmd = SpawnUtils.spawnOrAbort.mock.calls[0][0]; + expect(cmd).toBe('git'); + + const options = SpawnUtils.spawnOrAbort.mock.calls[0][1]; + expect(options).toStrictEqual(['clone', repo]); + + const cwd = SpawnUtils.spawnOrAbort.mock.calls[0][2]; + expect(cwd).toBe(undefined); + + }); + + it('then it checks out the git commit', () => { + const cmd = SpawnUtils.spawnOrAbort.mock.calls[1][0]; + expect(cmd).toBe('git'); + + const options = SpawnUtils.spawnOrAbort.mock.calls[1][1]; + expect(options).toStrictEqual(['checkout', 'e7cfd95e0ff2188d006444f93ea2ed6aeac18864']); + + const cwd = SpawnUtils.spawnOrAbort.mock.calls[1][2]; + expect(cwd).toBe('ordino'); + }); +}); \ No newline at end of file diff --git a/test/init-app-slib.test.js b/test/init-app-slib.test.js index aefddef5..20422af2 100644 --- a/test/init-app-slib.test.js +++ b/test/init-app-slib.test.js @@ -5,26 +5,13 @@ const assert = require('yeoman-assert'); const helpers = require('yeoman-test'); const rimraf = require('rimraf'); const fse = require('fs-extra'); -const testUtils = require('./testUtils'); +const TestUtils = require('./test-utils/TestUtils'); +const dependencies = require('./test-utils/generator-dependencies'); /** * Directory name to run the generator */ const target = '../appslib'; -/** - * Subgenerators composed with the `init-app-slib` subgenerator. - */ -const GENERATOR_DEPENDENCIES = [ - '../generators/_init-hybrid', - '../generators/_node', - '../generators/init-app', - '../generators/_init-web', - '../generators/init-slib', - '../generators/_init-python', - '../generators/_check-own-version', - '../generators/check-node-version', -]; - const expectedFiles = [ 'tsd.d.ts', 'jest.config.js', @@ -42,12 +29,12 @@ describe('generate app-slib plugin with prompt `app: appName` and the rest defau /** * package.tmpl.json template of the _init-web subgenerator */ - const initWebPackage = fse.readJSONSync(testUtils.templatePath('_init-web', 'package.tmpl.json')); + const initWebPackage = fse.readJSONSync(TestUtils.templatePath('_init-web', 'package.tmpl.json')); /** * tsconfig.json template of the _init-web subgenerator */ - const initWebTsConfig = fse.readJSONSync(testUtils.templatePath('_init-web', 'tsconfig.json', 'plain')); + const initWebTsConfig = fse.readJSONSync(TestUtils.templatePath('_init-web', 'tsconfig.json', 'plain')); beforeAll(() => { return helpers @@ -56,7 +43,7 @@ describe('generate app-slib plugin with prompt `app: appName` and the rest defau .withPrompts({ app: 'appName' }) - .withGenerators(GENERATOR_DEPENDENCIES); + .withGenerators(dependencies.INIT_APP_SLIB); }); afterAll(() => { @@ -84,6 +71,10 @@ describe('generate app-slib plugin with prompt `app: appName` and the rest defau assert.jsonFileContent('tsconfig.json', initWebTsConfig); }); + it('generates `.yo-rc.json` with correct type', () => { + assert.jsonFileContent('.yo-rc.json', {"generator-phovea": {type: 'app-slib'}}); + }); + it('generates no `tsconfig_dev.json`', () => { assert.noFile('tsconfig_dev.json'); }); diff --git a/test/init-app.test.js b/test/init-app.test.js index 849a8486..0aaa33dd 100644 --- a/test/init-app.test.js +++ b/test/init-app.test.js @@ -1,28 +1,17 @@ - 'use strict'; const path = require('path'); const assert = require('yeoman-assert'); const helpers = require('yeoman-test'); const rimraf = require('rimraf'); const fse = require('fs-extra'); -const testUtils = require('./testUtils'); +const TestUtils = require('./test-utils/TestUtils'); +const dependencies = require('./test-utils/generator-dependencies'); /** * Directory name to run the generator */ const target = '../app'; -/** - * Subgenerators composed with the `init-app` subgenerator. - */ -const GENERATOR_DEPENDENCIES = [ - '../generators/_node', - '../generators/init-lib', - '../generators/_init-web', - '../generators/_check-own-version', - '../generators/check-node-version', -]; - const expectedFiles = [ 'tsd.d.ts', 'jest.config.js', @@ -31,7 +20,8 @@ const expectedFiles = [ const unExpectedFiles = [ 'webpack.config.js', - 'tests.webpack.js' + 'tests.webpack.js', + 'src/index.template' ]; describe('generate app plugin with prompt `app: appName` and the rest default prompt values', () => { @@ -39,11 +29,11 @@ describe('generate app plugin with prompt `app: appName` and the rest default pr /** * package.tmpl.json template of the _init-web subgenerator */ - const initWebPackage = fse.readJSONSync(testUtils.templatePath('_init-web', 'package.tmpl.json')); + const initWebPackage = fse.readJSONSync(TestUtils.templatePath('_init-web', 'package.tmpl.json')); /** * tsconfig.json template of the _init-web subgenerator */ - const initWebTsConfig = fse.readJSONSync(testUtils.templatePath('_init-web', 'tsconfig.json', 'plain')); + const initWebTsConfig = fse.readJSONSync(TestUtils.templatePath('_init-web', 'tsconfig.json', 'plain')); beforeAll(() => { return helpers @@ -52,7 +42,7 @@ describe('generate app plugin with prompt `app: appName` and the rest default pr .withPrompts({ app: 'appName' }) - .withGenerators(GENERATOR_DEPENDENCIES); + .withGenerators(dependencies.INIT_APP); }); afterAll(() => { @@ -79,6 +69,10 @@ describe('generate app plugin with prompt `app: appName` and the rest default pr assert.jsonFileContent('tsconfig.json', initWebTsConfig); }); + it('generates `.yo-rc.json` with correct type', () => { + assert.jsonFileContent('.yo-rc.json', {"generator-phovea": {type: 'app'}}); + }); + it('generates no `tsconfig_dev.json`', () => { assert.noFile('tsconfig_dev.json'); }); diff --git a/test/init-lib-service.test.js b/test/init-lib-service.test.js index bae80e66..d6d114a7 100644 --- a/test/init-lib-service.test.js +++ b/test/init-lib-service.test.js @@ -5,26 +5,14 @@ const assert = require('yeoman-assert'); const helpers = require('yeoman-test'); const rimraf = require('rimraf'); const fse = require('fs-extra'); -const testUtils = require('./testUtils'); +const TestUtils = require('./test-utils/TestUtils'); +const dependencies = require('./test-utils/generator-dependencies'); + /** * Directory name to run the generator */ const target = '../libservice'; -/** - * Subgenerators composed with the `init-lib-service` subgenerator. - */ -const GENERATOR_DEPENDENCIES = [ - '../generators/_node', - '../generators/_init-hybrid', - '../generators/init-lib', - '../generators/_init-web', - '../generators/init-service', - '../generators/_init-python', - '../generators/_check-own-version', - '../generators/check-node-version', -]; - const expectedFiles = [ 'tsd.d.ts', 'jest.config.js' @@ -41,18 +29,18 @@ describe('generate lib-service plugin with default prompt values', () => { /** * package.tmpl.json template of the _init-web subgenerator */ - const initWebPackage = fse.readJSONSync(testUtils.templatePath('_init-web', 'package.tmpl.json')); + const initWebPackage = fse.readJSONSync(TestUtils.templatePath('_init-web', 'package.tmpl.json')); /** * tsconfig.json template of the _init-web subgenerator */ - const initWebTsConfig = fse.readJSONSync(testUtils.templatePath('_init-web', 'tsconfig.json', 'plain')); + const initWebTsConfig = fse.readJSONSync(TestUtils.templatePath('_init-web', 'tsconfig.json', 'plain')); beforeAll(() => { return helpers .run(path.join(__dirname, '../generators/init-lib-service')) .inDir(path.join(__dirname, target), () => null) - .withGenerators(GENERATOR_DEPENDENCIES); + .withGenerators(dependencies.INIT_LIB_SERVICE); }); afterAll(() => { @@ -79,6 +67,10 @@ describe('generate lib-service plugin with default prompt values', () => { assert.jsonFileContent('tsconfig.json', initWebTsConfig); }); + it('generates `.yo-rc.json` with correct type', () => { + assert.jsonFileContent('.yo-rc.json', {"generator-phovea": {type: 'lib-service'}}); + }); + it('generates no `tsconfig_dev.json`', () => { assert.noFile('tsconfig_dev.json'); }); diff --git a/test/init-lib-slib.test.js b/test/init-lib-slib.test.js index 8d0986cc..1b2dce33 100644 --- a/test/init-lib-slib.test.js +++ b/test/init-lib-slib.test.js @@ -5,26 +5,13 @@ const assert = require('yeoman-assert'); const helpers = require('yeoman-test'); const rimraf = require('rimraf'); const fse = require('fs-extra'); -const testUtils = require('./testUtils'); +const TestUtils = require('./test-utils/TestUtils'); +const dependencies = require('./test-utils/generator-dependencies'); /** * Directory name to run the generator */ const target = '../libslib'; -/** - * Subgenerators composed with the `init-lib-slib` subgenerator. - */ -const GENERATOR_DEPENDENCIES = [ - '../generators/_node', - '../generators/_init-hybrid', - '../generators/init-lib', - '../generators/_init-web', - '../generators/init-slib', - '../generators/_init-python', - '../generators/_check-own-version', - '../generators/check-node-version', -]; - const expectedFiles = [ 'tsd.d.ts', 'jest.config.js' @@ -41,18 +28,18 @@ describe('generate lib-slib plugin with default prompt values', () => { /** * package.tmpl.json template of the _init-web subgenerator */ - const initWebPackage = fse.readJSONSync(testUtils.templatePath('_init-web', 'package.tmpl.json')); + const initWebPackage = fse.readJSONSync(TestUtils.templatePath('_init-web', 'package.tmpl.json')); /** * tsconfig.json template of the _init-web subgenerator */ - const initWebTsConfig = fse.readJSONSync(testUtils.templatePath('_init-web', 'tsconfig.json', 'plain')); + const initWebTsConfig = fse.readJSONSync(TestUtils.templatePath('_init-web', 'tsconfig.json', 'plain')); beforeAll(() => { return helpers .run(path.join(__dirname, '../generators/init-lib-slib')) .inDir(path.join(__dirname, target), () => null) - .withGenerators(GENERATOR_DEPENDENCIES); + .withGenerators(dependencies.INIT_LIB_SLIB); }); afterAll(() => { @@ -79,6 +66,10 @@ describe('generate lib-slib plugin with default prompt values', () => { assert.jsonFileContent('tsconfig.json', initWebTsConfig); }); + it('generates `.yo-rc.json` with correct type', () => { + assert.jsonFileContent('.yo-rc.json', {"generator-phovea": {type: 'lib-slib'}}); + }); + it('generates no `tsconfig_dev.json`', () => { assert.noFile('tsconfig_dev.json'); }); diff --git a/test/init-lib.test.js b/test/init-lib.test.js index a8e5ad0a..59943a91 100644 --- a/test/init-lib.test.js +++ b/test/init-lib.test.js @@ -5,26 +5,16 @@ const assert = require('yeoman-assert'); const helpers = require('yeoman-test'); const rimraf = require('rimraf'); const fse = require('fs-extra'); -const testUtils = require('./testUtils'); +const TestUtils = require('./test-utils/TestUtils'); const {template} = require('lodash'); +const SpawnUtils = require('../utils/SpawnUtils'); +const dependencies = require('./test-utils/generator-dependencies'); /** * Directory name to run the generator */ const target = '../lib'; -/** - * Subgenerators composed with the `init-lib` subgenerator. - */ -const GENERATOR_DEPENDENCIES = [ - '../generators/_node', - '../generators/init-lib', - '../generators/_init-web', - '../generators/_check-own-version', - '../generators/check-node-version', -]; - - const expectedFiles = [ 'tsd.d.ts', 'jest.config.js' @@ -36,24 +26,29 @@ const unExpectedFiles = [ 'index.js' ]; +/** + * Run yo phovea:init-lib in dir + */ +const runInitLib = () => helpers + .run(path.join(__dirname, '../generators/init-lib')) + .withGenerators(dependencies.INIT_LIB) + .inDir(path.join(__dirname, target), () => null); + -describe('generate lib plugin with default prompt values', () => { +describe('Generate lib plugin with default prompt values', () => { /** * package.tmpl.json template of the _init-web subgenerator */ - const initWebPackage = fse.readJSONSync(testUtils.templatePath('_init-web', 'package.tmpl.json')); + const initWebPackage = fse.readJSONSync(TestUtils.templatePath('_init-web', 'package.tmpl.json')); /** * tsconfig.json template of the _init-web subgenerator */ - const initWebTsConfig = fse.readJSONSync(testUtils.templatePath('_init-web', 'tsconfig.json', 'plain')); + const initWebTsConfig = fse.readJSONSync(TestUtils.templatePath('_init-web', 'tsconfig.json', 'plain')); beforeAll(() => { - return helpers - .run(path.join(__dirname, '../generators/init-lib')) - .inDir(path.join(__dirname, target), () => null) - .withGenerators(GENERATOR_DEPENDENCIES); + return runInitLib(); }); afterAll(() => { @@ -80,6 +75,10 @@ describe('generate lib plugin with default prompt values', () => { assert.jsonFileContent('tsconfig.json', initWebTsConfig); }); + it('generates `.yo-rc.json` with correct type', () => { + assert.jsonFileContent('.yo-rc.json', {"generator-phovea": {type: 'lib'}}); + }); + it('generates no `tsconfig_dev.json`', () => { assert.noFile('tsconfig_dev.json'); }); @@ -100,11 +99,7 @@ describe('Generate plugin with name `phovea_core`', () => { }; beforeAll(() => { - return helpers - .run(path.join(__dirname, '../generators/init-lib')) - .inDir(path.join(__dirname, target), () => null) - .withPrompts(prompts) - .withGenerators(GENERATOR_DEPENDENCIES); + return runInitLib().withPrompts(prompts); }); afterAll(() => { @@ -112,7 +107,35 @@ describe('Generate plugin with name `phovea_core`', () => { }); it('generates `phovea_registry.js` with import statement adapted for `phovea_core`', () => { - const phoveaRegistryTmpl = template(fse.readFileSync(testUtils.templatePath('_init-web', 'phovea_registry.js', 'processed')))({name: prompts.name, modules: [], isWeb: () => null}); + const phoveaRegistryTmpl = template(fse.readFileSync(TestUtils.templatePath('_init-web', 'phovea_registry.js', 'processed')))({name: prompts.name, modules: [], isWeb: () => null}); assert.fileContent('phovea_registry.js', phoveaRegistryTmpl); }); }); + +describe('Test options of yo phovea:init-lib', () => { + + const prompts = { + name: 'phovea_core' + }; + + const options = { + install: true + }; + + beforeAll(() => { + SpawnUtils.spawnSync = jest.fn(); + return runInitLib() + .withPrompts(prompts) + .withOptions(options); + }); + + afterAll(() => { + rimraf.sync(path.join(__dirname, target)); + }); + + it('runs npm install', () => { + expect(SpawnUtils.spawnSync.mock.calls.length).toBe(1); + const [cmd, args, cwd, verbose] = SpawnUtils.spawnSync.mock.calls[0]; + expect([cmd, args, cwd, verbose]).toStrictEqual(['npm', 'install', '', true]); + }); +}); \ No newline at end of file diff --git a/test/init-product.test.js b/test/init-product.test.js new file mode 100644 index 00000000..c794355d --- /dev/null +++ b/test/init-product.test.js @@ -0,0 +1,83 @@ + +'use strict'; +const path = require('path'); +const assert = require('yeoman-assert'); +const helpers = require('yeoman-test'); +const rimraf = require('rimraf'); +const fse = require('fs-extra'); +const TestUtils = require('./test-utils/TestUtils'); +const {template} = require('lodash'); +const dependencies = require('./test-utils/generator-dependencies'); + +/** + * Directory name to run the generator + */ +const target = '../product'; + + +const expectedFiles = [ + '.gitattributes', + 'package.json', + 'build.js', + 'phovea_product.schema.json', + '.circleci/config.yml', + '.gitignore', + 'README.md', + '.editorconfig', + 'ISSUE_TEMPLATE.md', + 'LICENSE' +]; + +describe('generate a product with default prompt values', () => { + + const pluginName = 'wep_app'; + const pkg = JSON.parse(template(JSON.stringify(fse.readJSONSync(TestUtils.templatePath('init-product', 'package.tmpl.json'))))( + {name: pluginName})); + + const config = { + type: 'web', + label: 'Web App', + repo: 'phovea/wep_app', + branch: 'master', + additional: [{ + 'name': 'phovea_core', + 'repo': 'phovea/phovea_core', + }, { + 'name': 'phovea_ui', + 'repo': 'phovea/phovea_ui', + }] + }; + + beforeAll(() => { + return helpers + .run(path.join(__dirname, '../generators/init-product')) + .inDir(path.join(__dirname, target), () => null) + .withGenerators(dependencies.INIT_PRODUCT) + .withPrompts({ + ...config, + name: pluginName, + }); + }); + + afterAll(() => { + rimraf.sync(path.join(__dirname, target)); + }); + + it('generates all expected files', () => { + assert.file(expectedFiles); + }); + + it('generates correct `package.json`', () => { + assert.jsonFileContent('package.json', {dependencies: pkg.dependencies}); + assert.jsonFileContent('package.json', {name: 'wep_app'}); + assert.jsonFileContent('package.json', {scripts: pkg.scripts}); + }); + + it('generates `phovea_product.json` with the correct service', () => { + assert.jsonFileContent('phovea_product.json', [config]); + }); + + it('generates `.yo-rc.json` with correct type', () => { + assert.jsonFileContent('.yo-rc.json', {"generator-phovea": {type: 'product'}}); + }); +}); \ No newline at end of file diff --git a/test/init-service.test.js b/test/init-service.test.js index bba3c24a..fa203e34 100644 --- a/test/init-service.test.js +++ b/test/init-service.test.js @@ -4,28 +4,19 @@ const path = require('path'); const assert = require('yeoman-assert'); const helpers = require('yeoman-test'); const rimraf = require('rimraf'); +const dependencies = require('./test-utils/generator-dependencies'); /** * Directory name to run the generator */ const target = '../service'; -/** - * Subgenerators composed with the `init-slib` subgenerator. - */ -const GENERATOR_DEPENDENCIES = [ - '../generators/_node', - '../generators/_init-python', - '../generators/_check-own-version', - '../generators/check-node-version', -]; - describe('generate service plugin with default prompt values', () => { beforeAll(() => { return helpers .run(path.join(__dirname, '../generators/init-service')) .inDir(path.join(__dirname, target), () => null) - .withGenerators(GENERATOR_DEPENDENCIES); + .withGenerators(dependencies.INIT_SERVICE); }); afterAll(() => { @@ -35,4 +26,8 @@ describe('generate service plugin with default prompt values', () => { it('generates `package.json` with no devDependencies', () => { assert.jsonFileContent('package.json', {devDependencies: undefined}); }); + + it('generates `.yo-rc.json` with correct type', () => { + assert.jsonFileContent('.yo-rc.json', {"generator-phovea": {type: 'service'}}); + }); }); diff --git a/test/init-slib.test.js b/test/init-slib.test.js index d0ae5ed2..00c4995b 100644 --- a/test/init-slib.test.js +++ b/test/init-slib.test.js @@ -4,21 +4,12 @@ const path = require('path'); const assert = require('yeoman-assert'); const helpers = require('yeoman-test'); const rimraf = require('rimraf'); +const dependencies = require('./test-utils/generator-dependencies'); /** * Directory name to run the generator */ const target = '../slib'; -/** - * Subgenerators composed with the `init-slib` subgenerator. - */ -const GENERATOR_DEPENDENCIES = [ - '../generators/_node', - '../generators/_init-python', - '../generators/_check-own-version', - '../generators/check-node-version', -]; - describe('generate slib plugin with default prompt values', () => { @@ -26,7 +17,7 @@ describe('generate slib plugin with default prompt values', () => { return helpers .run(path.join(__dirname, '../generators/init-slib')) .inDir(path.join(__dirname, target), () => null) - .withGenerators(GENERATOR_DEPENDENCIES); + .withGenerators(dependencies.INIT_SLIB); }); afterAll(() => { @@ -44,4 +35,8 @@ describe('generate slib plugin with default prompt values', () => { it('generates `package.json` with a no `types`', () => { assert.jsonFileContent('package.json', {types: undefined}); }); + + it('generates `.yo-rc.json` with correct type', () => { + assert.jsonFileContent('.yo-rc.json', {"generator-phovea": {type: 'slib'}}); + }); }); diff --git a/test/setup-workspace.test.js b/test/setup-workspace.test.js new file mode 100644 index 00000000..9ab45e65 --- /dev/null +++ b/test/setup-workspace.test.js @@ -0,0 +1,124 @@ + +'use strict'; +const path = require('path'); +const helpers = require('yeoman-test'); +const rimraf = require('rimraf'); +const GeneratorUtils = require('../utils/GeneratorUtils'); +const WorkspaceUtils = require('../utils/WorkspaceUtils'); +const assert = require('yeoman-assert'); +const fs = require('fs-extra'); +const RepoUtils = require('../utils/RepoUtils'); +const TestUtils = require('./test-utils/TestUtils'); +const SpawnUtils = require('../utils/SpawnUtils'); +const {template} = require('lodash'); +const dependencies = require('./test-utils/generator-dependencies'); + +/** + * Directory name to run the generator + */ +const target = '../phovea_workpsace'; + +const product = 'org/dummy_product'; + +const setupWorkspace = () => helpers + .run(path.join(__dirname, '../generators/setup-workspace')) + .inDir(path.join(__dirname, target), () => null) + .withArguments([product]) + .withOptions({ssh: true, branch: 'develop'}) + .withGenerators(dependencies.SETUP_WORKSPACE); + + +describe('generator setup-workspace', () => { + const phoveaProduct = require(`./test-utils/templates/phovea_product_dummy.json`); + beforeAll(() => { + // mock the clone-repo function + WorkspaceUtils.cloneRepo = jest.fn() + // first call + .mockImplementationOnce((repo, branch, extras, dir, cwd) => { + fs.mkdirSync('dummy/templates/api/deploy/api', {recursive: true}); + fs.writeFileSync('dummy/templates/api/deploy/api/Dockerfile', 'dummy_content'); + + fs.mkdirSync('dummy/templates/web/deploy/web', {recursive: true}); + fs.writeFileSync('dummy/templates/web/deploy/web/Dockerfile', 'dummy_content'); + fs.writeJSON(cwd + '/package.json', {}); + return fs.writeJSON(cwd + '/phovea_product.json', phoveaProduct); + }) + .mockImplementation(() => Promise.resolve(null)); // just resolve promise after the firts call + + GeneratorUtils.yo = jest.fn(); + SpawnUtils.spawnOrAbort = jest.fn(); + return setupWorkspace(); + }); + + afterAll(() => { + rimraf.sync(path.join(__dirname, target)); + }); + + it('calls WorkspaceUtils.cloneRepo(...args) 13 times (1 product + 12 plugins)', () => { + expect(WorkspaceUtils.cloneRepo.mock.calls.length).toBe(13); + }); + + it('clones product by calling `WorkspaceUtils.cloneRepo(...args)` with the correct args', () => { + const [productRepo, branch, extra, target, cwd, ssh] = WorkspaceUtils.cloneRepo.mock.calls[0]; + expect([productRepo, branch, extra, target, cwd, ssh]).toStrictEqual([product, 'develop', null, '.', 'dummy', true]); + }); + + it('clones all plugins by calling `WorkspaceUtils.cloneRepo(...args)` with the correct args', () => { + const cloneRepoArgs = WorkspaceUtils.cloneRepo.mock.calls; + RepoUtils.parsePhoveaProduct(phoveaProduct).forEach((plugin, i) => { + const index = i + 1; // skip the product cloning call + const [name, branch, extra, target, cwd, ssh] = cloneRepoArgs[index]; + expect([name, branch, extra, target, cwd, ssh]).toStrictEqual([plugin.repo, plugin.branch, null, '', 'dummy', true]); + }); + }); + + it('creates valid `.yo-rc-workspace.json`', () => { + const content = { + 'modules': [], + 'defaultApp': 'ordino_public', + 'frontendRepos': [ + 'phovea_core', + 'phovea_ui', + 'phovea_clue', + 'phovea_security_flask', + 'tdp_core', + 'ordino', + 'tdp_gene', + 'tdp_publicdb' + ], + 'devRepos': [ + 'ordino_public' + ] + }; + + assert.file('dummy/.yo-rc-workspace.json'); + assert.jsonFileContent('dummy/.yo-rc-workspace.json', content); + }); + + it('calls `yo phovea:workspace` with the correct arguments', () => { + const yoArguments = GeneratorUtils.yo.mock.calls; + expect(yoArguments.length).toBe(1); + const [generator, options, args, cwd] = yoArguments[0]; + expect([generator, options, args, cwd]).toStrictEqual(['workspace', {'defaultApp': 'ordino_public', 'skipNextStepsLog': true}, null, 'dummy']); + }); + + it('copies `.idea` template files', () => { + let fileContent = template(fs.readFileSync(TestUtils.templatePath('setup-workspace', 'start_defaultapp.tmpl.xml')))({defaultApp: 'ordino_public'}); + assert.file('dummy/.idea/runConfigurations/start_ordino_public.xml'); + assert.fileContent('dummy/.idea/runConfigurations/start_ordino_public.xml', fileContent); + + fileContent = template(fs.readFileSync(TestUtils.templatePath('setup-workspace', 'lint_defaultapp.tmpl.xml')))({defaultApp: 'ordino_public'}); + assert.file('dummy/.idea/runConfigurations/lint_ordino_public.xml'); + assert.fileContent('dummy/.idea/runConfigurations/lint_ordino_public.xml', fileContent); + }); + + it('copies products\' template files into the workspace', () => { + assert.file('dummy/deploy/api/Dockerfile'); + assert.file('dummy/deploy/web/Dockerfile'); + }); + + it('calls `SpawnUtils.spawnOrAbort(...args)` with correct args', () => { + const [cmd, args, cwd, verbose] = SpawnUtils.spawnOrAbort.mock.calls[0]; + expect([cmd, args, cwd, verbose]).toStrictEqual(['npm', 'install', 'dummy', true]); + }); +}); diff --git a/test/test-utils/TestUtils.js b/test/test-utils/TestUtils.js new file mode 100644 index 00000000..26c2bd2b --- /dev/null +++ b/test/test-utils/TestUtils.js @@ -0,0 +1,13 @@ +const path = require('path'); + +module.exports = class TestUtils { + /** + * Get the path to the templates of a specific subgenerator. + * @param {string} subgenerator + * @param {string} fileName + * @param {string} type Type of template, plain or processed + */ + static templatePath(subgenerator, fileName, type = '') { + return path.join(__dirname, `../../generators/${subgenerator}/templates/${type ? type + '/' : ''}${fileName}`); + } +}; \ No newline at end of file diff --git a/test/test-utils/generator-dependencies.js b/test/test-utils/generator-dependencies.js new file mode 100644 index 00000000..f2628f02 --- /dev/null +++ b/test/test-utils/generator-dependencies.js @@ -0,0 +1,84 @@ +'use-strict'; +const path = require('path'); + +const toPath = (generator) => path.join(__dirname, '../../generators/', generator); + +const COMMON = [ + toPath('_check-own-version'), + toPath('check-node-version'), +]; + +const INIT_LIB = [ + ...COMMON, + toPath('_node'), + toPath('init-lib'), + toPath('_init-web'), +]; + + +const INIT_SLIB = [ + ...COMMON, + toPath('_node'), + toPath('init-slib'), + toPath('_init-python'), +]; + +const INIT_SERVICE = [ + ...COMMON, + toPath('_node'), + toPath('init-service'), + toPath('_init-python'), +]; + + +const INIT_LIB_SLIB = Array.from(new Set([ + ...INIT_LIB, + ...INIT_SLIB, + toPath('_init-hybrid') +])); + +const INIT_LIB_SERVICE = Array.from(new Set([ + ...INIT_LIB, + ...INIT_SERVICE, + toPath('_init-hybrid') +])); + + +const INIT_APP = [ + ...COMMON, + toPath('_node'), + toPath('init-app'), + toPath('_init-web'), +]; + +const INIT_APP_SLIB = [ + ...INIT_APP, + ...INIT_SLIB, + toPath('_init-hybrid') +]; + +const INIT_PRODUCT = [ + ...COMMON, + toPath('_node'), +]; + +const SETUP_WORKSPACE=[ + ...COMMON, + toPath('workspace'), + toPath('clone-repo'), +]; + +const dependencies = { + COMMON, + INIT_LIB, + INIT_SLIB, + INIT_LIB_SLIB, + INIT_APP, + INIT_SERVICE, + INIT_APP_SLIB, + INIT_LIB_SERVICE, + INIT_PRODUCT, + SETUP_WORKSPACE +}; + +module.exports = dependencies; \ No newline at end of file diff --git a/test/test-utils/templates/phovea_product_dummy.json b/test/test-utils/templates/phovea_product_dummy.json new file mode 100644 index 00000000..fadc7e4c --- /dev/null +++ b/test/test-utils/templates/phovea_product_dummy.json @@ -0,0 +1,88 @@ +[ + { + "type": "web", + "label": "ordino", + "repo": "Caleydo/ordino_public", + "branch": "develop", + "additional": [ + { + "name": "phovea_core", + "repo": "phovea/phovea_core", + "branch": "develop" + }, + { + "name": "phovea_ui", + "repo": "phovea/phovea_ui", + "branch": "develop" + }, + { + "name": "phovea_clue", + "repo": "phovea/phovea_clue", + "branch": "develop" + }, + { + "name": "phovea_security_flask", + "repo": "phovea/phovea_security_flask", + "branch": "develop" + }, + { + "name": "tdp_core", + "repo": "datavisyn/tdp_core", + "branch": "develop" + }, + { + "name": "ordino", + "repo": "Caleydo/ordino", + "branch": "develop" + }, + { + "name": "tdp_gene", + "repo": "Caleydo/tdp_gene", + "branch": "develop" + }, + { + "name": "tdp_publicdb", + "repo": "Caleydo/tdp_publicdb", + "branch": "develop" + } + ] + }, + { + "type": "api", + "label": "ordino_server", + "repo": "phovea/phovea_server", + "branch": "develop", + "additional": [ + { + "name": "phovea_security_flask", + "repo": "phovea/phovea_security_flask", + "branch": "develop" + }, + { + "name": "phovea_data_redis", + "repo": "phovea/phovea_data_redis", + "branch": "develop" + }, + { + "name": "phovea_data_mongo", + "repo": "phovea/phovea_data_mongo", + "branch": "develop" + }, + { + "name": "phovea_clue", + "repo": "phovea/phovea_clue", + "branch": "develop" + }, + { + "name": "tdp_core", + "repo": "datavisyn/tdp_core", + "branch": "develop" + }, + { + "name": "tdp_publicdb", + "repo": "Caleydo/tdp_publicdb", + "branch": "develop" + } + ] + } + ] \ No newline at end of file diff --git a/test/testUtils.js b/test/testUtils.js deleted file mode 100644 index f331dfda..00000000 --- a/test/testUtils.js +++ /dev/null @@ -1,13 +0,0 @@ -const path = require('path'); - -/** - * Get the path to the templates of a specific subgenerator. - * @param {string} subgenerator - * @param {string} fileName - * @param {string} type Type of template, plain or processed - */ -const templatePath = (subgenerator, fileName, type = '') => path.join(__dirname, `../generators/${subgenerator}/templates/${type}/${fileName}`); - -module.exports = { - templatePath -}; diff --git a/test/utils-version.test.js b/test/utils-version.test.js deleted file mode 100644 index f7bebe44..00000000 --- a/test/utils-version.test.js +++ /dev/null @@ -1,397 +0,0 @@ -'use strict'; -const {intersect} = require('semver-intersect'); -const version = require('../utils/version'); - -describe('check isGitCommit()', () => { - it('check `451709494a48af64a8a4876063c244edf41d2643` === true', () => { - expect(version.isGitCommit('451709494a48af64a8a4876063c244edf41d2643')).toBeTruthy(); - }); - - it('check `8747a43780e4651542facd7b4feac7bcb8e3778d` === true', () => { - expect(version.isGitCommit('8747a43780e4651542facd7b4feac7bcb8e3778d')).toBeTruthy(); - }); - - it('check `develop` === false', () => { - expect(version.isGitCommit('develop')).toBeFalsy(); - }); - - it('check `v2.0.0` === false', () => { - expect(version.isGitCommit('v2.0.0')).toBeFalsy(); - }); - - it('check `~v2.0.0` === false', () => { - expect(version.isGitCommit('~v2.0.0')).toBeFalsy(); - }); - - it('check `^v2.0.0` === false', () => { - expect(version.isGitCommit('^v2.0.0')).toBeFalsy(); - }); - - it('check `2.0.0` === false', () => { - expect(version.isGitCommit('2.0.0')).toBeFalsy(); - }); - - it('check `~2.0.0` === false', () => { - expect(version.isGitCommit('~2.0.0')).toBeFalsy(); - }); - - it('check `^2.0.0` === false', () => { - expect(version.isGitCommit('^2.0.0')).toBeFalsy(); - }); -}); - -describe('check isExactVersionTag()', () => { - it('check `develop` === false', () => { - expect(version.isExactVersionTag('develop')).toBeFalsy(); - }); - - it('check `v2.0.0` === true', () => { - expect(version.isExactVersionTag('v2.0.0')).toBeTruthy(); - }); - - it('check `~v2.0.0` === false', () => { - expect(version.isExactVersionTag('~v2.0.0')).toBeFalsy(); - }); - - it('check `^v2.0.0` === false', () => { - expect(version.isExactVersionTag('^v2.0.0')).toBeFalsy(); - }); - - it('check `2.0.0` === false', () => { - expect(version.isExactVersionTag('2.0.0')).toBeFalsy(); - }); - - it('check `~2.0.0` === false', () => { - expect(version.isExactVersionTag('~2.0.0')).toBeFalsy(); - }); - - it('check `^2.0.0` === false', () => { - expect(version.isExactVersionTag('^2.0.0')).toBeFalsy(); - }); -}); - -describe('check isAdvancedVersionTag()', () => { - it('check `develop` === false', () => { - expect(version.isAdvancedVersionTag('develop')).toBeFalsy(); - }); - - it('check `v2.0.0` === false', () => { - expect(version.isAdvancedVersionTag('v2.0.0')).toBeFalsy(); - }); - - it('check `~v2.0.0` === true', () => { - expect(version.isAdvancedVersionTag('~v2.0.0')).toBeTruthy(); - }); - - it('check `^v2.0.0` === true', () => { - expect(version.isAdvancedVersionTag('^v2.0.0')).toBeTruthy(); - }); - - it('check `2.0.0` === false', () => { - expect(version.isAdvancedVersionTag('2.0.0')).toBeFalsy(); - }); - - it('check `~2.0.0` === false', () => { - expect(version.isAdvancedVersionTag('~2.0.0')).toBeFalsy(); - }); - - it('check `^2.0.0` === false', () => { - expect(version.isAdvancedVersionTag('^2.0.0')).toBeFalsy(); - }); -}); - -describe('transform git log to version tags list', () => { - const gitLog = `451709494a48af64a8a4876063c244edf41d2643 refs/tags/caleydo_web - a1d4f93a626d71937bc35b23d3715eaf34cce4a1 refs/tags/v0.0.5 - 921409de073f4329c09a4602622e87d816de266f refs/tags/v0.1.0 - 9815eb1163b212e06b7239a0bf6f97b0fbc2cf0c refs/tags/v1.0.0 - e61c59f8c09e51e0b25f2087ad92789c26d58c11 refs/tags/v1.0.0^{} - 336072e87ec8f6054cead9f64c6830897fb7f076 refs/tags/v2.0.0 - 8747a43780e4651542facd7b4feac7bcb8e3778d refs/tags/v2.0.1 - ebb538469a661dc4a8c5646ca1bb11259f2ba2bb refs/tags/v2.0.1^{} - c073da02dbb1c8185a5d50266f78b9688dd4403a refs/tags/v2.1.0 - 53f68e0768df23b173f59d22ea90f61c478b8450 refs/tags/v2.1.1 - `; - - const versionTags = version.extractVersionsFromGitLog(gitLog); - - it('check number of extracted versions', () => { - expect(versionTags.length).toBe(7); - }); - - it('check if tags without `v` are removed', () => { - expect(versionTags.indexOf('caleydo_web')).toBe(-1); - }); - - it('check if tags ending with `^{}` are removed', () => { - expect(versionTags.indexOf('v1.0.0^{}')).toBe(-1); - expect(versionTags.filter((v) => v === 'v1.0.0').length).toBe(1); - }); - - it('find specific version tag in list', () => { - expect(versionTags.indexOf('v0.0.5')).toBe(0); - }); -}); - -describe('check semver.satisfies', () => { - const semver = require('semver'); - - it('check against target version with caret operator', () => { - const targetVersion = '^v2.0.0'; - expect(semver.satisfies('v1.0.0', targetVersion)).toBeFalsy(); - expect(semver.satisfies('v2.0.0', targetVersion)).toBeTruthy(); - expect(semver.satisfies('v2.0.1', targetVersion)).toBeTruthy(); - expect(semver.satisfies('v2.0.2', targetVersion)).toBeTruthy(); - expect(semver.satisfies('v2.1.0', targetVersion)).toBeTruthy(); - expect(semver.satisfies('v2.1.1', targetVersion)).toBeTruthy(); - expect(semver.satisfies('v2.1.2', targetVersion)).toBeTruthy(); - expect(semver.satisfies('v2.2.0', targetVersion)).toBeTruthy(); - expect(semver.satisfies('v2.2.1', targetVersion)).toBeTruthy(); - expect(semver.satisfies('v2.2.2', targetVersion)).toBeTruthy(); - expect(semver.satisfies('v3.0.0', targetVersion)).toBeFalsy(); - expect(semver.satisfies('v3.1.0', targetVersion)).toBeFalsy(); - }); - - it('check target version with tilde operator', () => { - const targetVersion = '~v2.0.0'; - expect(semver.satisfies('v1.0.0', targetVersion)).toBeFalsy(); - expect(semver.satisfies('v2.0.0', targetVersion)).toBeTruthy(); - expect(semver.satisfies('v2.0.1', targetVersion)).toBeTruthy(); - expect(semver.satisfies('v2.0.2', targetVersion)).toBeTruthy(); - expect(semver.satisfies('v2.1.0', targetVersion)).toBeFalsy(); - expect(semver.satisfies('v2.1.1', targetVersion)).toBeFalsy(); - expect(semver.satisfies('v2.1.2', targetVersion)).toBeFalsy(); - expect(semver.satisfies('v2.2.0', targetVersion)).toBeFalsy(); - expect(semver.satisfies('v2.2.1', targetVersion)).toBeFalsy(); - expect(semver.satisfies('v2.2.2', targetVersion)).toBeFalsy(); - expect(semver.satisfies('v3.0.0', targetVersion)).toBeFalsy(); - expect(semver.satisfies('v3.1.0', targetVersion)).toBeFalsy(); - }); -}); - -describe('find highest version from list', () => { - const sourceVersions = [ - 'v0.0.5', - 'v0.1.0', - 'v1.0.0', - 'v2.1.0', // note: out of order to test sorting - 'v2.2.0', // note: out of order to test sorting - 'v2.0.0', - 'v2.0.1', - 'v2.0.2', - 'v2.1.1', - 'v2.1.2', - 'v2.2.1', - 'v2.2.2', - 'v3.0.0', - 'v3.1.0' - ]; - - it('find exact version `v2.0.0`', () => { - const targetVersion = 'v2.0.0'; - expect(version.findHighestVersion(sourceVersions, targetVersion)).toBe('v2.0.0'); - }); - - it('find patch version `~v2.0.0`', () => { - const targetVersion = '~v2.0.0'; - expect(version.findHighestVersion(sourceVersions, targetVersion)).toBe('v2.0.2'); - }); - - it('find minor version `^v2.0.0`', () => { - const targetVersion = '^v2.0.0'; - expect(version.findHighestVersion(sourceVersions, targetVersion)).toBe('v2.2.2'); - }); - - it('find non-existing `v4.0.0`', () => { - const targetVersion = 'v4.0.0'; - expect(version.findHighestVersion(sourceVersions, targetVersion)).toBeUndefined(); - }); - - it('find non-existing `^v3.2.0`', () => { - const targetVersion = '^v3.2.0'; - expect(version.findHighestVersion(sourceVersions, targetVersion)).toBeUndefined(); - }); -}); - -describe('find max version or range version from list', () => { - - it('works for simple versions arrays', () => { - const versions = ['0.0.5', '0.1.0', '1.0.0', '2.1.0', '2.2.0']; - expect(version.findMaxVersion(versions)).toBe('2.2.0'); - }); - - it('works for arrays with prerelease versions', () => { - const versions = ['4.2.0-alpha.1', '4.2.0-beta.0', '0.1.0', '1.0.0', '2.1.0', '2.2.0']; - expect(version.findMaxVersion(versions)).toBe('4.2.0-beta.0'); - }); - - it('works for arrays with prerelease and tilde ranges', () => { - const versions = ['4.2.0-alpha.1', '4.2.0-beta.0', '0.1.0', '~4.2.0', '2.1.0', '~2.2.0']; - expect(version.findMaxVersion(versions)).toBe('~4.2.0'); - }); - - it('works for arrays with prerelease, tilde and caret ranges', () => { - const versions = ['4.2.0-alpha.1', '4.2.0-beta.0', '^4.2.0', '~4.2.0', '2.1.0', '~2.2.0']; - expect(version.findMaxVersion(versions)).toBe('^4.2.0'); - }); - - it('works for `^4.2.0`', () => { - const versions = ['4.2.0-alpha.1', '4.2.0-beta.0', '^4.2.0', '~4.2.0', '2.1.0', '~2.2.0']; - expect(version.findMaxVersion(versions)).toBe('^4.2.0'); - }); - - it('works for caret prerelease ranges', () => { - const versions = ['^4.2.0-alpha.1', '^4.2.0-beta.0', '^4.1.0', '2.1.0', '~2.2.0']; - expect(version.findMaxVersion(versions)).toBe('^4.2.0-beta.0'); - }); - - it('works for tilde prerelease ranges', () => { - const versions = ['~4.2.0-alpha.1', '~4.2.0-beta.0', '~4.1.0', '2.1.0', '~2.2.0']; - expect(version.findMaxVersion(versions)).toBe('~4.2.0-beta.0'); - }); - - it('works for tilde and caret prerelease ranges', () => { - const versions = ['~4.2.0-alpha.1', '^4.2.0-rc.0', '~4.2.0-beta.1', '^4.1.0', '2.1.0', '~2.2.0']; - expect(version.findMaxVersion(versions)).toBe('^4.2.0-rc.0'); - }); - - it('works for versions that start with `v`', () => { - const versions = ['~4.2.0-alpha.1', '^4.2.0-rc.0', '~4.2.0-beta.1', '^4.1.0', 'v5.1.0', '~2.2.0']; - expect(version.findMaxVersion(versions)).toBe('5.1.0'); - }); - - it('works for versions of the format `5.1`', () => { - const versions = ['~4.2.0-alpha.1', '^4.2.0-rc.0', '~4.2.0-beta.1', '^4.1.0', '5.1', '~2.2.0']; - expect(version.findMaxVersion(versions)).toBe('5.1.0'); - }); - - it('works if versions are all ranges', () => { - const versions = ['^2.9.0', '~2.8.1']; - expect(version.findMaxVersion(versions)).toBe('^2.9.0'); - }); -}); - -describe('semver-intersect works for prerelease ranges', () => { - - it('finds intersection of an array of prerelease ranges', () => { - const versions = ['~4.2.0-alpha.1', '~4.2.0-beta.1',]; - expect(intersect(...versions)).toBe('~4.2.0-beta.1'); - }); -}); - -describe('find intersection or max version of github or gitlab version tags', () => { - - it('returns first correct gitlab tag', () => { - const name = 'target360'; - const versions = [ - 'git+ssh://git@gitlab.customer.com:Target360/plugins/target360#dv_develop', - 'git+ssh://git@gitlab.customer.com:Target360/plugins/target360#dv_develop', - '4.0.0' - ]; - expect(version.mergeVersions(name, versions)).toBe('git+ssh://git@gitlab.customer.com:Target360/plugins/target360#dv_develop'); - }); - - it('returns first correct githab tag', () => { - const name = 'phovea_core'; - const versions = [ - 'github:phovea/phovea_core#develop', - 'github:phovea/phovea_core#develop', - '5.0.0' - ]; - expect(version.mergeVersions(name, versions)).toBe('github:phovea/phovea_core#develop'); - }); - - it('throws error if versions point to different gitlab branches', () => { - const name = 'target360'; - const versions = [ - 'git+ssh://git@gitlab.customer.com:Target360/plugins/target360#dv_develop', - 'git+ssh://git@gitlab.customer.com:Target360/plugins/target360#master', - '4.0.0' - ]; - expect(() => version.mergeVersions(name, versions)).toThrow(); - }); - - it('returns correct intersection of github ranges', () => { - const name = 'phovea_core'; - const versions = [ - 'github:phovea/phovea_core#semver:^7.0.1', - 'github:phovea/phovea_core#semver:^7.0.0', - ]; - expect(version.mergeVersions(name, versions)).toBe('github:phovea/phovea_core#semver:^7.0.1'); - }); - - it('throws error if versions contain both github and gitlab', () => { - const name = 'phovea_core'; - const versions = [ - 'github:phovea/phovea_core#semver:^7.0.1', - 'git+ssh://git@gitlab.customer.com:Target360/plugins/target360#master', - ]; - expect(() => version.mergeVersions(name, versions)).toThrow(); - }); - - it('throws error if versions 2 different github versions', () => { - const name = 'phovea_core'; - const versions = [ - 'github:phovea/phovea_core#semver:^7.0.1', - 'github:phovea/phovea_core#develop', - ]; - expect(() => version.mergeVersions(name, versions)).toThrow(); - }); - - it('returns github version if one of the versions is a github semver version bigger than the rest', () => { - const name = 'phovea_core'; - const versions = [ - 'github:phovea/phovea_core#semver:^7.0.1', - '4.0.0', - ]; - expect(version.mergeVersions(name, versions)).toBe('github:phovea/phovea_core#semver:^7.0.1'); - }); - - it('returns correct max github version', () => { - const name = 'phovea_core'; - const versions = [ - 'github:phovea/phovea_core#semver:^7.0.1', - 'github:phovea/phovea_core#semver:^8.0.0' - ]; - expect(version.mergeVersions(name, versions)).toBe('github:phovea/phovea_core#semver:^8.0.0'); - }); - - it('compares github version with npm version', () => { - const name = 'phovea_core'; - const versions = [ - 'github:phovea/phovea_core#semver:^7.0.1', - '^8.0.0' - ]; - expect(version.mergeVersions(name, versions)).toBe('^8.0.0'); - }); - - it('compares an equal github version with an npm version', () => { - const name = 'phovea_core'; - const versions = [ - 'github:phovea/phovea_core#semver:^7.0.1', - '^7.0.1' - ]; - expect(version.mergeVersions(name, versions)).toBe('github:phovea/phovea_core#semver:^7.0.1'); - }); - - it('compares multiple github and npm versions', () => { - const name = 'phovea_core'; - const versions = [ - 'github:phovea/phovea_core#semver:^7.0.1', - 'github:phovea/phovea_core#semver:^6.0.1', - '^7.0.1', - '^7.0.2' - ]; - expect(version.mergeVersions(name, versions)).toBe('^7.0.2'); - }); - - it('finds the intersection of a git version and npm version', () => { - const name = 'phovea_core'; - const versions = [ - 'github:phovea/phovea_core#semver:~7.0.1', - 'github:phovea/phovea_core#semver:^6.0.1', - '^7.0.1', - ]; - expect(version.mergeVersions(name, versions)).toBe('github:phovea/phovea_core#semver:~7.0.1'); - }); -}); diff --git a/test/utils/GeneratorUtils.test.js b/test/utils/GeneratorUtils.test.js new file mode 100644 index 00000000..c8396f11 --- /dev/null +++ b/test/utils/GeneratorUtils.test.js @@ -0,0 +1,96 @@ +'use strict'; +const GeneratorUtils = require('../../utils/GeneratorUtils'); + + +describe('Test `stringifyInline()` correctly stringifies object', () => { + + it('adds 1 space', () => { + const obj = {key1: 'value1', key2: 'value2'}; + const space = ' '; + const result = "{\n 'key1': 'value1',\n 'key2': 'value2'\n }"; + expect(GeneratorUtils.stringifyInline(obj, space)).toBe(result); + }); + + it('adds 2 spaces', () => { + const obj = {key1: 'value1', key2: 'value2'}; + const space = ' '; + const result = "{\n 'key1': 'value1',\n 'key2': 'value2'\n }"; + expect(GeneratorUtils.stringifyInline(obj, space)).toBe(result); + }); + + it('removes double quotes', () => { + const obj = {key1: 'value1', key2: 'value2'}; + const space = ' '; + const result = '{\n "key1": "value1",\n "key2": "value2"\n }'; + expect(GeneratorUtils.stringifyInline(obj, space)).not.toBe(result); + }); +}); + +describe('Test `stringifyAble()`', () => { + const config = { + type: 'slib', + modules: ['phovea_server'], + libraries: [], + sextensions: [], + }; + it('isWeb returns false', () => { + expect(GeneratorUtils.stringifyAble(config).isWeb('phovea_server')).toBe(false); + }); + + it('formats boolean to python boolean', () => { + expect(GeneratorUtils.stringifyAble(config).stringifyPython({key1: true, key2: false}, ' ')).toBe("{\n 'key1': True,\n 'key2': False\n }"); + }); + + it('returns stringify function', () => { + expect(GeneratorUtils.stringifyAble(config).stringify).toBe(GeneratorUtils.stringifyInline); + }); + + it('returns the config', () => { + expect(GeneratorUtils.stringifyAble(config)).toMatchObject(config); + }); +}); + +describe('Test `toJSONFromText()`', () => { + it('parses string to object with the correct variable types ', () => { + + const extras = ` + type=lib + isBoolean=true + count=3 + weights=[1, 2, 3] + `; + + const extrasObject = { + type: 'lib', + isBoolean: true, + count: 3, + }; + expect(GeneratorUtils.toJSONFromText(extras)).toMatchObject(extrasObject); + }); + + it('parses dot notation to correct nested object', () => { + const extras = ` + config.name=ordino + config.isServer=false + config.number=5 + `; + + const extrasObject = { + config: { + name: 'ordino', + isServer: false, + number: 5 + } + }; + + expect(GeneratorUtils.toJSONFromText(extras)).toMatchObject(extrasObject); + }); + + it('returns an empty object for non string inputs', () => { + expect(GeneratorUtils.toJSONFromText(null)).toMatchObject({}); + expect(GeneratorUtils.toJSONFromText(undefined)).toMatchObject({}); + expect(GeneratorUtils.toJSONFromText('')).toMatchObject({}); + expect(GeneratorUtils.toJSONFromText(' ')).toMatchObject({}); + expect(GeneratorUtils.toJSONFromText(1)).toMatchObject({}); + }); +}); \ No newline at end of file diff --git a/test/utils/NpmUtils.test.js b/test/utils/NpmUtils.test.js new file mode 100644 index 00000000..77115171 --- /dev/null +++ b/test/utils/NpmUtils.test.js @@ -0,0 +1,428 @@ +'use strict'; +const NpmUtils = require('../../utils/NpmUtils'); + + +describe('mergeVersions list of versions with github or gitlab version tags', () => { + + it('returns first correct gitlab tag', () => { + const name = 'target360'; + const versions = [ + 'git+ssh://git@gitlab.customer.com:Target360/plugins/target360#dv_develop', + 'git+ssh://git@gitlab.customer.com:Target360/plugins/target360#dv_develop', + '4.0.0' + ]; + expect(NpmUtils.mergeVersions(name, versions)).toBe('git+ssh://git@gitlab.customer.com:Target360/plugins/target360#dv_develop'); + }); + + it('returns first correct github tag', () => { + const name = 'phovea_core'; + const versions = [ + 'github:phovea/phovea_core#develop', + 'github:phovea/phovea_core#develop', + '5.0.0' + ]; + expect(NpmUtils.mergeVersions(name, versions)).toBe('github:phovea/phovea_core#develop'); + }); + + it('throws error if versions point to different gitlab branches', () => { + const name = 'target360'; + const versions = [ + 'git+ssh://git@gitlab.customer.com:Target360/plugins/target360#dv_develop', + 'git+ssh://git@gitlab.customer.com:Target360/plugins/target360#master', + '4.0.0' + ]; + expect(() => NpmUtils.mergeVersions(name, versions)).toThrow(); + }); + + it('returns correct intersection of github ranges', () => { + const name = 'phovea_core'; + const versions = [ + 'github:phovea/phovea_core#semver:^7.0.1', + 'github:phovea/phovea_core#semver:^7.0.0', + ]; + expect(NpmUtils.mergeVersions(name, versions)).toBe('github:phovea/phovea_core#semver:^7.0.1'); + }); + + it('throws error if versions contain both github and gitlab', () => { + const name = 'phovea_core'; + const versions = [ + 'github:phovea/phovea_core#semver:^7.0.1', + 'git+ssh://git@gitlab.customer.com:Target360/plugins/target360#master', + ]; + expect(() => NpmUtils.mergeVersions(name, versions)).toThrow(); + }); + + it('throws error if versions are 2 different github versions', () => { + const name = 'phovea_core'; + const versions = [ + 'github:phovea/phovea_core#semver:^7.0.1', + 'github:phovea/phovea_core#develop', + ]; + expect(() => NpmUtils.mergeVersions(name, versions)).toThrow(); + }); + + it('returns github version if one of the versions is a github semver version bigger than the rest', () => { + const name = 'phovea_core'; + const versions = [ + 'github:phovea/phovea_core#semver:^7.0.1', + '4.0.0', + ]; + expect(NpmUtils.mergeVersions(name, versions)).toBe('github:phovea/phovea_core#semver:^7.0.1'); + }); + + it('returns correct max github version', () => { + const name = 'phovea_core'; + const versions = [ + 'github:phovea/phovea_core#semver:^7.0.1', + 'github:phovea/phovea_core#semver:^8.0.0' + ]; + expect(NpmUtils.mergeVersions(name, versions)).toBe('github:phovea/phovea_core#semver:^8.0.0'); + }); + + it('compares github version with npm version', () => { + const name = 'phovea_core'; + const versions = [ + 'github:phovea/phovea_core#semver:^7.0.1', + '^8.0.0' + ]; + expect(NpmUtils.mergeVersions(name, versions)).toBe('^8.0.0'); + }); + + it('compares an equal github version with an npm version', () => { + const name = 'phovea_core'; + const versions = [ + 'github:phovea/phovea_core#semver:^7.0.1', + '^7.0.1' + ]; + expect(NpmUtils.mergeVersions(name, versions)).toBe('github:phovea/phovea_core#semver:^7.0.1'); + }); + + it('compares multiple github and npm versions', () => { + const name = 'phovea_core'; + const versions = [ + 'github:phovea/phovea_core#semver:^7.0.1', + 'github:phovea/phovea_core#semver:^6.0.1', + '^7.0.1', + '^7.0.2' + ]; + expect(NpmUtils.mergeVersions(name, versions)).toBe('^7.0.2'); + }); + + it('finds the intersection of a git version and npm version', () => { + const name = 'phovea_core'; + const versions = [ + 'github:phovea/phovea_core#semver:~7.0.1', + 'github:phovea/phovea_core#semver:^6.0.1', + '^7.0.1', + ]; + expect(NpmUtils.mergeVersions(name, versions)).toBe('github:phovea/phovea_core#semver:~7.0.1'); + }); +}); + +describe('check isGitCommit()', () => { + it('check `451709494a48af64a8a4876063c244edf41d2643` === true', () => { + expect(NpmUtils.isGitCommit('451709494a48af64a8a4876063c244edf41d2643')).toBeTruthy(); + }); + + it('check `8747a43780e4651542facd7b4feac7bcb8e3778d` === true', () => { + expect(NpmUtils.isGitCommit('8747a43780e4651542facd7b4feac7bcb8e3778d')).toBeTruthy(); + }); + + it('check `develop` === false', () => { + expect(NpmUtils.isGitCommit('develop')).toBeFalsy(); + }); + + it('check `v2.0.0` === false', () => { + expect(NpmUtils.isGitCommit('v2.0.0')).toBeFalsy(); + }); + + it('check `~v2.0.0` === false', () => { + expect(NpmUtils.isGitCommit('~v2.0.0')).toBeFalsy(); + }); + + it('check `^v2.0.0` === false', () => { + expect(NpmUtils.isGitCommit('^v2.0.0')).toBeFalsy(); + }); + + it('check `2.0.0` === false', () => { + expect(NpmUtils.isGitCommit('2.0.0')).toBeFalsy(); + }); + + it('check `~2.0.0` === false', () => { + expect(NpmUtils.isGitCommit('~2.0.0')).toBeFalsy(); + }); + + it('check `^2.0.0` === false', () => { + expect(NpmUtils.isGitCommit('^2.0.0')).toBeFalsy(); + }); +}); + +describe('check isExactVersionTag()', () => { + it('check `develop` === false', () => { + expect(NpmUtils.isExactVersionTag('develop')).toBeFalsy(); + }); + + it('check `v2.0.0` === true', () => { + expect(NpmUtils.isExactVersionTag('v2.0.0')).toBeTruthy(); + }); + + it('check `~v2.0.0` === false', () => { + expect(NpmUtils.isExactVersionTag('~v2.0.0')).toBeFalsy(); + }); + + it('check `^v2.0.0` === false', () => { + expect(NpmUtils.isExactVersionTag('^v2.0.0')).toBeFalsy(); + }); + + it('check `2.0.0` === false', () => { + expect(NpmUtils.isExactVersionTag('2.0.0')).toBeFalsy(); + }); + + it('check `~2.0.0` === false', () => { + expect(NpmUtils.isExactVersionTag('~2.0.0')).toBeFalsy(); + }); + + it('check `^2.0.0` === false', () => { + expect(NpmUtils.isExactVersionTag('^2.0.0')).toBeFalsy(); + }); +}); + +describe('check isExactVersionTag()', () => { + it('check `develop` === false', () => { + expect(NpmUtils.isExactVersionTag('develop')).toBeFalsy(); + }); + + it('check `v2.0.0` === true', () => { + expect(NpmUtils.isExactVersionTag('v2.0.0')).toBeTruthy(); + }); + + it('check `~v2.0.0` === false', () => { + expect(NpmUtils.isExactVersionTag('~v2.0.0')).toBeFalsy(); + }); + + it('check `^v2.0.0` === false', () => { + expect(NpmUtils.isExactVersionTag('^v2.0.0')).toBeFalsy(); + }); + + it('check `2.0.0` === false', () => { + expect(NpmUtils.isExactVersionTag('2.0.0')).toBeFalsy(); + }); + + it('check `~2.0.0` === false', () => { + expect(NpmUtils.isExactVersionTag('~2.0.0')).toBeFalsy(); + }); + + it('check `^2.0.0` === false', () => { + expect(NpmUtils.isExactVersionTag('^2.0.0')).toBeFalsy(); + }); +}); + +describe('check isAdvancedVersionTag()', () => { + it('check `develop` === false', () => { + expect(NpmUtils.isAdvancedVersionTag('develop')).toBeFalsy(); + }); + + it('check `v2.0.0` === false', () => { + expect(NpmUtils.isAdvancedVersionTag('v2.0.0')).toBeFalsy(); + }); + + it('check `~v2.0.0` === true', () => { + expect(NpmUtils.isAdvancedVersionTag('~v2.0.0')).toBeTruthy(); + }); + + it('check `^v2.0.0` === true', () => { + expect(NpmUtils.isAdvancedVersionTag('^v2.0.0')).toBeTruthy(); + }); + + it('check `2.0.0` === false', () => { + expect(NpmUtils.isAdvancedVersionTag('2.0.0')).toBeFalsy(); + }); + + it('check `~2.0.0` === false', () => { + expect(NpmUtils.isAdvancedVersionTag('~2.0.0')).toBeFalsy(); + }); + + it('check `^2.0.0` === false', () => { + expect(NpmUtils.isAdvancedVersionTag('^2.0.0')).toBeFalsy(); + }); +}); + +describe('transform git log to version tags list', () => { + const gitLog = `451709494a48af64a8a4876063c244edf41d2643 refs/tags/caleydo_web + a1d4f93a626d71937bc35b23d3715eaf34cce4a1 refs/tags/v0.0.5 + 921409de073f4329c09a4602622e87d816de266f refs/tags/v0.1.0 + 9815eb1163b212e06b7239a0bf6f97b0fbc2cf0c refs/tags/v1.0.0 + e61c59f8c09e51e0b25f2087ad92789c26d58c11 refs/tags/v1.0.0^{} + 336072e87ec8f6054cead9f64c6830897fb7f076 refs/tags/v2.0.0 + 8747a43780e4651542facd7b4feac7bcb8e3778d refs/tags/v2.0.1 + ebb538469a661dc4a8c5646ca1bb11259f2ba2bb refs/tags/v2.0.1^{} + c073da02dbb1c8185a5d50266f78b9688dd4403a refs/tags/v2.1.0 + 53f68e0768df23b173f59d22ea90f61c478b8450 refs/tags/v2.1.1 + `; + + const versionTags = NpmUtils.extractVersionsFromGitLog(gitLog); + + it('check number of extracted versions', () => { + expect(versionTags.length).toBe(7); + }); + + it('check if tags without `v` are removed', () => { + expect(versionTags.indexOf('caleydo_web')).toBe(-1); + }); + + it('check if tags ending with `^{}` are removed', () => { + expect(versionTags.indexOf('v1.0.0^{}')).toBe(-1); + expect(versionTags.filter((v) => v === 'v1.0.0').length).toBe(1); + }); + + it('find specific version tag in list', () => { + expect(versionTags.indexOf('v0.0.5')).toBe(0); + }); +}); + +describe('check semver.satisfies', () => { + const semver = require('semver'); + + it('check against target version with caret operator', () => { + const targetVersion = '^v2.0.0'; + expect(semver.satisfies('v1.0.0', targetVersion)).toBeFalsy(); + expect(semver.satisfies('v2.0.0', targetVersion)).toBeTruthy(); + expect(semver.satisfies('v2.0.1', targetVersion)).toBeTruthy(); + expect(semver.satisfies('v2.0.2', targetVersion)).toBeTruthy(); + expect(semver.satisfies('v2.1.0', targetVersion)).toBeTruthy(); + expect(semver.satisfies('v2.1.1', targetVersion)).toBeTruthy(); + expect(semver.satisfies('v2.1.2', targetVersion)).toBeTruthy(); + expect(semver.satisfies('v2.2.0', targetVersion)).toBeTruthy(); + expect(semver.satisfies('v2.2.1', targetVersion)).toBeTruthy(); + expect(semver.satisfies('v2.2.2', targetVersion)).toBeTruthy(); + expect(semver.satisfies('v3.0.0', targetVersion)).toBeFalsy(); + expect(semver.satisfies('v3.1.0', targetVersion)).toBeFalsy(); + }); + + it('check target version with tilde operator', () => { + const targetVersion = '~v2.0.0'; + expect(semver.satisfies('v1.0.0', targetVersion)).toBeFalsy(); + expect(semver.satisfies('v2.0.0', targetVersion)).toBeTruthy(); + expect(semver.satisfies('v2.0.1', targetVersion)).toBeTruthy(); + expect(semver.satisfies('v2.0.2', targetVersion)).toBeTruthy(); + expect(semver.satisfies('v2.1.0', targetVersion)).toBeFalsy(); + expect(semver.satisfies('v2.1.1', targetVersion)).toBeFalsy(); + expect(semver.satisfies('v2.1.2', targetVersion)).toBeFalsy(); + expect(semver.satisfies('v2.2.0', targetVersion)).toBeFalsy(); + expect(semver.satisfies('v2.2.1', targetVersion)).toBeFalsy(); + expect(semver.satisfies('v2.2.2', targetVersion)).toBeFalsy(); + expect(semver.satisfies('v3.0.0', targetVersion)).toBeFalsy(); + expect(semver.satisfies('v3.1.0', targetVersion)).toBeFalsy(); + }); +}); + +describe('semver-intersect works for prerelease ranges', () => { + const {intersect} = require('semver-intersect'); + + it('finds intersection of an array of prerelease ranges', () => { + const versions = ['~4.2.0-alpha.1', '~4.2.0-beta.1',]; + expect(intersect(...versions)).toBe('~4.2.0-beta.1'); + }); +}); + +describe('find highest version from list', () => { + const sourceVersions = [ + 'v0.0.5', + 'v0.1.0', + 'v1.0.0', + 'v2.1.0', // note: out of order to test sorting + 'v2.2.0', // note: out of order to test sorting + 'v2.0.0', + 'v2.0.1', + 'v2.0.2', + 'v2.1.1', + 'v2.1.2', + 'v2.2.1', + 'v2.2.2', + 'v3.0.0', + 'v3.1.0' + ]; + + it('find exact version `v2.0.0`', () => { + const targetVersion = 'v2.0.0'; + expect(NpmUtils.findHighestVersion(sourceVersions, targetVersion)).toBe('v2.0.0'); + }); + + it('find patch version `~v2.0.0`', () => { + const targetVersion = '~v2.0.0'; + expect(NpmUtils.findHighestVersion(sourceVersions, targetVersion)).toBe('v2.0.2'); + }); + + it('find minor version `^v2.0.0`', () => { + const targetVersion = '^v2.0.0'; + expect(NpmUtils.findHighestVersion(sourceVersions, targetVersion)).toBe('v2.2.2'); + }); + + it('find non-existing `v4.0.0`', () => { + const targetVersion = 'v4.0.0'; + expect(NpmUtils.findHighestVersion(sourceVersions, targetVersion)).toBeUndefined(); + }); + + it('find non-existing `^v3.2.0`', () => { + const targetVersion = '^v3.2.0'; + expect(NpmUtils.findHighestVersion(sourceVersions, targetVersion)).toBeUndefined(); + }); +}); + +describe('find max version or range version from list', () => { + + it('works for simple versions arrays', () => { + const versions = ['0.0.5', '0.1.0', '1.0.0', '2.1.0', '2.2.0']; + expect(NpmUtils.findMaxVersion(versions)).toBe('2.2.0'); + }); + + it('works for arrays with prerelease versions', () => { + const versions = ['4.2.0-alpha.1', '4.2.0-beta.0', '0.1.0', '1.0.0', '2.1.0', '2.2.0']; + expect(NpmUtils.findMaxVersion(versions)).toBe('4.2.0-beta.0'); + }); + + it('works for arrays with prerelease and tilde ranges', () => { + const versions = ['4.2.0-alpha.1', '4.2.0-beta.0', '0.1.0', '~4.2.0', '2.1.0', '~2.2.0']; + expect(NpmUtils.findMaxVersion(versions)).toBe('~4.2.0'); + }); + + it('works for arrays with prerelease, tilde and caret ranges', () => { + const versions = ['4.2.0-alpha.1', '4.2.0-beta.0', '^4.2.0', '~4.2.0', '2.1.0', '~2.2.0']; + expect(NpmUtils.findMaxVersion(versions)).toBe('^4.2.0'); + }); + + it('works for `^4.2.0`', () => { + const versions = ['4.2.0-alpha.1', '4.2.0-beta.0', '^4.2.0', '~4.2.0', '2.1.0', '~2.2.0']; + expect(NpmUtils.findMaxVersion(versions)).toBe('^4.2.0'); + }); + + it('works for caret prerelease ranges', () => { + const versions = ['^4.2.0-alpha.1', '^4.2.0-beta.0', '^4.1.0', '2.1.0', '~2.2.0']; + expect(NpmUtils.findMaxVersion(versions)).toBe('^4.2.0-beta.0'); + }); + + it('works for tilde prerelease ranges', () => { + const versions = ['~4.2.0-alpha.1', '~4.2.0-beta.0', '~4.1.0', '2.1.0', '~2.2.0']; + expect(NpmUtils.findMaxVersion(versions)).toBe('~4.2.0-beta.0'); + }); + + it('works for tilde and caret prerelease ranges', () => { + const versions = ['~4.2.0-alpha.1', '^4.2.0-rc.0', '~4.2.0-beta.1', '^4.1.0', '2.1.0', '~2.2.0']; + expect(NpmUtils.findMaxVersion(versions)).toBe('^4.2.0-rc.0'); + }); + + it('works for versions that start with `v`', () => { + const versions = ['~4.2.0-alpha.1', '^4.2.0-rc.0', '~4.2.0-beta.1', '^4.1.0', 'v5.1.0', '~2.2.0']; + expect(NpmUtils.findMaxVersion(versions)).toBe('5.1.0'); + }); + + it('works for versions of the format `5.1`', () => { + const versions = ['~4.2.0-alpha.1', '^4.2.0-rc.0', '~4.2.0-beta.1', '^4.1.0', '5.1', '~2.2.0']; + expect(NpmUtils.findMaxVersion(versions)).toBe('5.1.0'); + }); + + it('works if versions are all ranges', () => { + const versions = ['^2.9.0', '~2.8.1']; + expect(NpmUtils.findMaxVersion(versions)).toBe('^2.9.0'); + }); +}); \ No newline at end of file diff --git a/test/utils/PipUtils.test.js b/test/utils/PipUtils.test.js new file mode 100644 index 00000000..698c1b70 --- /dev/null +++ b/test/utils/PipUtils.test.js @@ -0,0 +1,116 @@ +'use strict'; +const PipUtils = require('../../utils/PipUtils'); + +describe('find intersection list of pip versions', () => { + const pipPackage = 'alembic'; + it('check for empty list', () => { + const versions = []; + + expect(PipUtils.mergePipVersions(pipPackage, versions)).toBe(''); + }); + + it('check for single exact version', () => { + const versions = ['==3.10.1']; + + expect(PipUtils.mergePipVersions(pipPackage, versions)).toBe('==3.10.1'); + }); + + it('check for list of exact versions', () => { + const versions = ['==3.10.1', '==3.10.2']; + + expect(PipUtils.mergePipVersions(pipPackage, versions)).toBe('==3.10.2'); + }); + + it('check for list of ranged and exact versions', () => { + const versions = ['~=3.10.1', '^=3.10.2']; + + expect(PipUtils.mergePipVersions(pipPackage, versions)).toBe('~=3.10.2'); + }); + + // TODO Refactor to return max without removing the range tags + it('finds max exact version when no intersection exists', () => { + const versions = ['~=3.10.3', '^=3.10.2', '~=3.10.3', '^=4.10.2']; + + expect(PipUtils.mergePipVersions(pipPackage, versions)).toBe('==4.10.2'); + }); +}); + +describe('check PipUtils.toSemver', () => { + + it('version is an empty string', () => { + const version = ''; + expect(PipUtils.toSemVer(version)).toBe(''); + }); + + it('version has `==`', () => { + const version = '==3.4.0'; + expect(PipUtils.toSemVer(version)).toBe('3.4.0'); + }); + + it('version has `~=`', () => { + const version = '~=3.4.0'; + expect(PipUtils.toSemVer(version)).toBe('~3.4.0'); + }); + + it('version has `^=`', () => { + const version = '^=3.4.0'; + expect(PipUtils.toSemVer(version)).toBe('^3.4.0'); + }); +}); + +describe('check PipUtils.toPipVersion', () => { + + it('version is an empty string', () => { + const version = ''; + expect(PipUtils.toPipVersion(version)).toBe(null); + }); + + it('version has no range tag', () => { + const version = '3.4.0'; + expect(PipUtils.toPipVersion(version)).toBe('==3.4.0'); + }); + + it('version has a `~`', () => { + const version = '~3.4.0'; + expect(PipUtils.toPipVersion(version)).toBe('~=3.4.0'); + }); + + it('version has `^`', () => { + const version = '^3.4.0'; + expect(PipUtils.toPipVersion(version)).toBe('^=3.4.0'); + }); + +}); + +describe('parse requirements.txt into an object', () => { + + it('returns an empty object if requirements file is null', () => { + const file = null; + expect(PipUtils.parseRequirements(file)).toEqual({}); + }); + + it('returns an empty object if the requirements file contains an string of only whitespace', () => { + const file = ' '; + expect(PipUtils.parseRequirements(file)).toEqual({}); + }); + + it('returns the requirements object', () => { + const file = ` + flake8^=3.7.9 + pep8-naming~=0.9.1 + pytest==5.3.5 + -e git+https://github.com/phovea/phovea_server.git@develop#egg=phovea_server + -e git+https://github.com/datavisyn/tdp_core.git@develop#egg=tdp_core + + + `; + const result = { + 'flake8': '^=3.7.9', + 'pep8-naming': '~=0.9.1', + 'pytest': '==5.3.5', + '-e git+https://github.com/datavisyn/tdp_core.git': '@develop#egg=tdp_core', + '-e git+https://github.com/phovea/phovea_server.git': '@develop#egg=phovea_server' + }; + expect(PipUtils.parseRequirements(file)).toEqual(result); + }); +}); \ No newline at end of file diff --git a/test/utils/RepoUtils.test.js b/test/utils/RepoUtils.test.js new file mode 100644 index 00000000..9088129d --- /dev/null +++ b/test/utils/RepoUtils.test.js @@ -0,0 +1,231 @@ +'use strict'; +const RepoUtils = require('../../utils/RepoUtils'); +const known = require('../../utils/known'); + +jest.mock('../../utils/known', () => { + return { + plugin: { + byName: jest.fn() + }, + lib: { + byName: jest.fn() + } + }; +}); + +const mockedPlugin = { + name: 'phovea_core', + libraries: [ + 'd3' + ], + externals: [ + 'bootstrap-sass' + ] +}; + +const mockedLib = { + name: 'font-awesome', + libraries: [ + 'jquery', + ], + externals: [ + 'marked' + ], + aliases: { + 'd3': 'd3/d3', + 'font-awesome': 'fw/font-awesome' + } + +}; + +known.plugin.byName.mockImplementation(() => { + return mockedPlugin; +}); +known.lib.byName.mockImplementation(() => { + return mockedLib; +}); + + +describe('transfroms repo name to an https url', () => { + + it('repo is already an http url', () => { + const repo = 'https://github.com/phovea/phovea_core.git'; + expect(RepoUtils.toHTTPRepoUrl(repo)).toBe(repo); + }); + + it('repo has format `organization/repo`', () => { + const repo = 'phovea/phovea_core'; + expect(RepoUtils.toHTTPRepoUrl(repo)).toBe('https://github.com/phovea/phovea_core.git'); + }); + + it('repo is only the name of the repo', () => { + const repo = 'ordino'; + expect(RepoUtils.toHTTPRepoUrl(repo)).toBe('https://github.com/Caleydo/ordino.git'); + }); + + it('repo is an SSH url', () => { + const repo = 'git@github.com:phovea/phovea_core.git'; + expect(RepoUtils.toHTTPRepoUrl(repo)).toBe('https://github.com/phovea/phovea_core.git'); + }); +}); + +describe('transfroms repo to a SSH url', () => { + + it('repo is already a SSH url', () => { + const repo = 'git@github.com:phovea/phovea_core.git'; + expect(RepoUtils.toSSHRepoUrl(repo)).toBe(repo); + }); + + it('repo is an http url', () => { + const repo = 'http://github.com/phovea/phovea_core.git'; + expect(RepoUtils.toSSHRepoUrl(repo)).toBe('git@github.com:phovea/phovea_core.git'); + }); + + it('repo is an https url', () => { + const repo = 'https://github.com/phovea/phovea_core.git'; + expect(RepoUtils.toSSHRepoUrl(repo)).toBe('git@github.com:phovea/phovea_core.git'); + }); + + it('repo is only the name of the repo', () => { + const repo = 'ordino_public'; + expect(RepoUtils.toSSHRepoUrl(repo)).toBe('git@github.com:Caleydo/ordino_public.git'); + }); +}); + +describe('transfroms a http(s) to a SSH url', () => { + + it('repo is an empty string', () => { + const repo = ''; + expect(RepoUtils.toSSHRepoUrlFromHTTP(repo)).toBe(''); + }); + + it('repo is already a SSH url', () => { + const repo = 'git@github.com:phovea/phovea_core.git'; + expect(RepoUtils.toSSHRepoUrlFromHTTP(repo)).toBe(repo); + }); + + it('repo is an http url', () => { + const repo = 'http://github.com/phovea/phovea_core.git'; + expect(RepoUtils.toSSHRepoUrlFromHTTP(repo)).toBe('git@github.com:phovea/phovea_core.git'); + }); +}); + +describe('extract `organization/repo` from a SSH or an http url', () => { + + it('repo is an empty string', () => { + const repo = ''; + expect(RepoUtils.simplifyRepoUrl(repo)).toBe(''); + }); + + it('repo is a SSH url', () => { + const repo = 'git@github.com:phovea/phovea_core.git'; + expect(RepoUtils.simplifyRepoUrl(repo)).toBe('phovea/phovea_core'); + }); + + it('repo is an http url', () => { + const repo = 'http://github.com/phovea/phovea_core.git'; + expect(RepoUtils.simplifyRepoUrl(repo)).toBe('phovea/phovea_core'); + }); +}); + +describe('prefix repo name with organization name`', () => { + + it('repo already contains organization', () => { + const repo = 'Caleydo/ordino'; + expect(RepoUtils.toBaseName(repo)).toBe(repo); + }); + + it('repo does not contain organization', () => { + const repo = 'ordino'; + expect(RepoUtils.toBaseName(repo)).toBe('Caleydo/ordino'); + }); +}); + +describe('extact repo name from string containing `org/repo`', () => { + + it('repo contains org', () => { + const repo = 'Caleydo/ordino'; + expect(RepoUtils.toCWD(repo)).toBe('ordino'); + }); + + it('repo is a product', () => { + const repo = 'Caleydo/ordino_product'; + expect(RepoUtils.toCWD(repo)).toBe('ordino'); + }); + + it('repo does not contain org', () => { + const repo = 'ordino'; + expect(RepoUtils.toCWD(repo)).toBe('ordino'); + }); +}); + +describe('parse phovea_product.json', () => { + const result = [ + {repo: 'Caleydo/ordino_public', branch: 'develop'}, + {repo: 'phovea/phovea_core', branch: 'develop'}, + {repo: 'phovea/phovea_ui', branch: 'develop'}, + {repo: 'phovea/phovea_clue', branch: 'develop'}, + {repo: 'phovea/phovea_security_flask', branch: 'develop'}, + {repo: 'datavisyn/tdp_core', branch: 'develop'}, + {repo: 'Caleydo/ordino', branch: 'develop'}, + {repo: 'Caleydo/tdp_gene', branch: 'develop'}, + {repo: 'Caleydo/tdp_publicdb', branch: 'develop'}, + {repo: 'phovea/phovea_server', branch: 'develop'}, + {repo: 'phovea/phovea_data_redis', branch: 'develop'}, + {repo: 'phovea/phovea_data_mongo', branch: 'develop'} + ]; + + const dummyProduct = require('../test-utils/templates/phovea_product_dummy.json'); + it('resulting object has correct structure', () => { + expect(RepoUtils.parsePhoveaProduct(dummyProduct)).toStrictEqual(result); + }); +}); + + +describe('test toLibraryAliasMap works as expected', () => { + const aliases = { + 'd3': 'd3/d3', + 'font-awesome': 'fw/font-awesome' + }; + + it('finds the correct aliases of the modules and libraries', () => { + const moduleNames = [ + 'phovea_core', + 'phovea_d3', + 'phovea_ui', + 'phovea_importer', + ]; + const libraryNames = [ + 'd3', + 'd3v5', + 'lineupjs', + 'bootstrap', + 'd3', + ]; + expect(RepoUtils.toLibraryAliasMap(moduleNames, libraryNames)).toMatchObject(aliases); + }); + + it('returns an empty object if no modules or libraries are provided', () => { + expect(RepoUtils.toLibraryAliasMap([], [])).toMatchObject({}); + }); +}); + +describe('test toLibraryExternals', () => { + + it('finds the correct extrernals of the provided modules and libraries', () => { + const moduleNames = [ + 'phovea_core', + 'phovea_d3', + 'phovea_ui', + 'phovea_importer', + ]; + const libraryNames = [ + 'd3', + 'd3v5', + 'lineupjs', + 'bootstrap', + 'd3', + ]; + expect(RepoUtils.toLibraryExternals(moduleNames, libraryNames)).toStrictEqual(['bootstrap-sass', 'font-awesome', 'marked']); + }); +}); \ No newline at end of file diff --git a/test/utils/WorkspaceUtils.test.js b/test/utils/WorkspaceUtils.test.js new file mode 100644 index 00000000..ebb68535 --- /dev/null +++ b/test/utils/WorkspaceUtils.test.js @@ -0,0 +1,118 @@ +'use strict'; +const WorkspaceUtils = require('../../utils/WorkspaceUtils'); +jest.mock('../../utils/known'); + +describe('Find default app in product object', () => { + + it('returns null if product is undefined', () => { + const product = undefined; + expect(WorkspaceUtils.findDefaultApp(product)).toBe(null); + }); + + it('returns null if product has no entry type `web`', () => { + const product = [ + { + type: 'api', + label: 'phovea_ui', + repo: 'phovea/phovea_ui', + branch: 'develop', + additional: [] + + }]; + expect(WorkspaceUtils.findDefaultApp(product)).toBe(null); + }); + + it('returns parsed repo name when entry has type `web`', () => { + const product = [ + { + type: 'web', + label: 'ordino', + repo: 'Caleydo/ordino_public', + branch: 'develop', + additional: [ + { + 'name': 'phovea_core', + 'repo': 'phovea/phovea_core', + 'branch': 'develop' + }, + { + 'name': 'phovea_ui', + 'repo': 'phovea/phovea_ui', + 'branch': 'develop' + } + ] + + }, + { + type: 'api', + label: 'repo', + repo: 'org/repo', + branch: 'develop', + additional: [] + + }]; + expect(WorkspaceUtils.findDefaultApp(product)).toMatchObject({name: 'ordino_public', additional: ['phovea_core', 'phovea_ui']}); + }); +}); + +describe('Test `buildPossibleAdditionalPlugins()`', () => { + + + const known = require('../../utils/known'); + known.plugin = { + listWeb: [{ + 'name': 'phovea_core', + 'type': 'lib', + 'description': 'Phovea Core Plugin', + 'repository': 'https://github.com/phovea/phovea_core.git', + 'dependencies': { + 'phovea_core': '^4.0.0' + }, + 'develop': { + 'dependencies': { + 'phovea_core': 'github:phovea/phovea_core#develop' + } + }, + 'libraries': [] + },], + listServer: [{ + 'name': 'phovea_server', + 'type': 'service', + 'description': 'Phovea Server Plugin', + 'repository': 'https://github.com/phovea/phovea_server.git', + 'requirements': { + 'phovea_server': '>=5.0.1,<6.0.0' + }, + 'develop': { + 'requirements': { + '-e git+https://github.com/phovea/phovea_server.git': '@develop#egg=phovea_server' + } + } + }], + }; + it('builds additional web plugins', () => { + + const result = [{ + 'name': 'phovea_core: Phovea Core Plugin', + 'short': 'phovea_core', + 'value': { + 'name': 'phovea_core', + 'repo': 'phovea/phovea_core', + }, + }]; + expect(WorkspaceUtils.buildPossibleAdditionalPlugins('web')).toMatchObject(result); + expect(WorkspaceUtils.buildPossibleAdditionalPlugins('static')).toMatchObject(result); + }); + + it('builds additional python plugins', () => { + const result = [{ + 'name': 'phovea_server: Phovea Server Plugin', + 'short': 'phovea_server', + 'value': { + 'name': 'phovea_server', + 'repo': 'phovea/phovea_server', + }, + }]; + expect(WorkspaceUtils.buildPossibleAdditionalPlugins('python')).toMatchObject(result); + }); +}); \ No newline at end of file diff --git a/test/utils-installedVersions.test.js b/test/utils/utils-installedVersions.test.js similarity index 98% rename from test/utils-installedVersions.test.js rename to test/utils/utils-installedVersions.test.js index 3f4f068e..3791e026 100644 --- a/test/utils-installedVersions.test.js +++ b/test/utils/utils-installedVersions.test.js @@ -1,5 +1,5 @@ 'use strict'; -const installedVersions = require('../utils/installedVersions'); +const installedVersions = require('../../utils/installedVersions'); describe('installedVersions() behaves as expected', () => { beforeEach(() => { diff --git a/test/workspace.test.js b/test/workspace.test.js index e3b8bc73..413187b2 100644 --- a/test/workspace.test.js +++ b/test/workspace.test.js @@ -4,6 +4,7 @@ const assert = require('yeoman-assert'); const helpers = require('yeoman-test'); const fse = require('fs-extra'); const {template} = require('lodash'); +const dependencies = require('./test-utils/generator-dependencies'); /** * Get the path to the templates of a specific subgenerator. @@ -12,35 +13,6 @@ const {template} = require('lodash'); */ const templatePath = (subgenerator, file) => path.join(__dirname, `../generators/${subgenerator}/templates/${file}`); -/** - * Subgenerators composed with the `init-lib` subgenerator. - */ -const LIB_GENERATOR_DEPENDENCIES = [ - '../generators/_node', - '../generators/init-lib', - '../generators/_init-web', - '../generators/_check-own-version', - '../generators/check-node-version', -].map((d) => path.join(__dirname, d)); - -/** - * Subgenerators composed with the `init-app` subgenerator. - */ -const APP_GENERATOR_DEPENDENCIES = [ - '../generators/_node', - '../generators/_init-web', - '../generators/_check-own-version', - '../generators/check-node-version', -].map((d) => path.join(__dirname, d)); - -/** - * Subgenerators composed with the `workspace` subgenerator. - */ -const WORKSPACE_GENERATOR_DEPENDENCIES = [ - '../generators/_check-own-version', - '../generators/check-node-version', -].map((d) => path.join(__dirname, d)); - const expectedFiles = [ '.idea/misc.xml', '.idea/remote-mappings.xml', @@ -112,7 +84,7 @@ describe('Run yo phovea:init-lib, yo phovea:init-app and yo:phovea:workspace seq // Run yo phovea:init-lib await helpers .run(path.join(__dirname, '../generators/init-lib')) - .withGenerators(LIB_GENERATOR_DEPENDENCIES) + .withGenerators(dependencies.INIT_LIB) .inTmpDir((dir) => { workingDirectory = dir; fse.mkdirSync(`./${libPlugin}`); @@ -122,7 +94,7 @@ describe('Run yo phovea:init-lib, yo phovea:init-app and yo:phovea:workspace seq // Run yo:phovea:init-app await helpers .run(path.join(__dirname, '../generators/init-app')) - .withGenerators(APP_GENERATOR_DEPENDENCIES) + .withGenerators(dependencies.INIT_APP) .withPrompts({ app: 'appName' }) @@ -135,7 +107,7 @@ describe('Run yo phovea:init-lib, yo phovea:init-app and yo:phovea:workspace seq // Run yo phovea:workspace await helpers .run(path.join(__dirname, '../generators/workspace')) - .withGenerators(WORKSPACE_GENERATOR_DEPENDENCIES) + .withGenerators(dependencies.COMMON) .inTmpDir(() => { workspace = workingDirectory.replace('/tmp/', ''); process.chdir(workingDirectory); diff --git a/utils/GeneratorUtils.js b/utils/GeneratorUtils.js new file mode 100644 index 00000000..11d1af2f --- /dev/null +++ b/utils/GeneratorUtils.js @@ -0,0 +1,132 @@ + +const fs = require('fs-extra'); +const yeoman = require('yeoman-environment'); + +module.exports = class GeneratorUtils { + /** + * Creates directory in the given path. + * @param {string} dir Directory + */ + static mkdir(dir) { + return new Promise((resolve) => fs.ensureDir(dir, resolve)); + } + + /** + * Similar to the composeWith method of the base yeoman generator but it waits shortly till the generator is finished. + * @param {string} generator Generator name, i.e, `init-lib`. + * @param {Object} options Options to call the generator with. + * @param {*} args Arguments to pass to the generator. + * @param {*} cwd The directory to run the generator in. + * @param {*} adapter The current generator adapter. + */ + static yo(generator, options, args, cwd, adapter) { + // call yo internally + const env = yeoman.createEnv([], { + cwd + }, adapter); + const _args = Array.isArray(args) ? args.join(' ') : args || ''; + return new Promise((resolve, reject) => { + try { + console.log(`Running: yo phovea:${generator} ${_args}`); + env.lookup(() => { + env.run(`phovea:${generator} ${_args}`, options || {}, () => { + // wait a second after running yo to commit the files correctly + setTimeout(() => resolve(), 500); + }); + }); + } catch (e) { + console.error('Error', e, e.stack); + reject(e); + } + }); + } + + /** + * Creates object with custom formatting functions that can be called inside a template file when copying a template. + * + * @param {{}} config Config file + */ + static stringifyAble(config) { + return Object.assign({ + stringifyPython: (obj, space) => { + let base = GeneratorUtils.stringifyInline(obj, space); + // python different true false + base = base.replace(/: true/g, ': True').replace(/: false/g, ': False'); + return base; + }, + stringify: GeneratorUtils.stringifyInline, + isWeb: (p) => { + const { + plugin + } = require('./known'); + return plugin.isTypeWeb(p); + } + }, config); + } + + /** + * Stringifies object and applies custom formatting. + * + * @param {{}} obj Object to stringify. + * @param {string} space String containing the spaces to use to fromat stringified object. + */ + static stringifyInline(obj, space) { + let base = JSON.stringify(obj, null, ' '); + // common style + base = base.replace(/"/g, '\''); + // prefix with space + base = base.split('\n').map((l) => space + l).join('\n'); + return base.substring(space.length); // skip the first space + } + + /** + * Parses a string of the format `key=value` into an object. + * Nested fields are defined using dot notation. + * + * @param {string} text String to format + * @example + * + * const text = ` + * count=3 + * type.lib=false + * ` + * + * toJSONFromText(text) + * // => {count: 3, type: {lib: false }} + */ + static toJSONFromText(text) { + + const r = {}; + if (typeof text !== 'string') return r; + + text.split('\n').forEach((line) => { + const trimmedLine = line.trim(); + if (trimmedLine.length === 0) { // ignore empty lines (e.g. new line added by editor) + return; + } + + const splitPoint = trimmedLine.indexOf('='); + const key = trimmedLine.slice(0, splitPoint); + let value = trimmedLine.slice(splitPoint + 1); + value = value.trim(); + if (!isNaN(parseFloat(value))) { + value = parseFloat(value); + } + + if (value === 'true' || value === 'false') { + value = JSON.parse(value); + } + + let obj = r; + const keys = key.trim().split('.'); + keys.slice(0, keys.length - 1).forEach((k) => { + if (!(k in obj)) { + obj[k] = {}; + } + obj = obj[k]; + }); + obj[keys[keys.length - 1]] = value; + }); + return r; + } +}; diff --git a/utils/NpmUtils.js b/utils/NpmUtils.js new file mode 100644 index 00000000..9be0bdfa --- /dev/null +++ b/utils/NpmUtils.js @@ -0,0 +1,251 @@ +'use strict'; + +const semver = require('semver'); +const chalk = require('chalk'); +const {intersect} = require('semver-intersect'); + + +module.exports = class NpmUtils { + + /** + * Finds the intersection of an array of semver versions. If no intersection exists the maximum version is returned. + * @param {string} name Name of the dependency. + * @param {string[]} versions Array of versions found for the dependency in the different workspace plugins. + */ + static mergeVersions(name, versions, logWarning = true) { + if (versions.some((v) => v === 'latest')) { + throw new Error(chalk.red('Invalid version. Please avoid using version latest in package.json.')); + } + // create set + versions = Array.from(new Set(versions)); + if (versions.length === 1) { + return versions[0]; + } + // filter for github and gitlab version strings + const gitRepos = versions.filter((d) => d.includes('github') || d.includes('gitlab')); + // npm version strings + const noGitRepos = versions.filter((v) => !gitRepos.includes(v)); + + if (gitRepos.length) { + return NpmUtils.mergeGithubVersions(name, gitRepos, noGitRepos); + } + + try { + return intersect(...noGitRepos).toString(); + } catch (e) { + // map to base version, sort descending take first + const max = NpmUtils.findMaxVersion(noGitRepos); + if (logWarning) { + console.warn(`cannot find common intersecting version for ${name} = ${versions.join(', ')}, taking max "${max}" for now`); + } + + return max.toString(); + } + } + + /** + * Extracts git version strings, compares them with the npm versions and returns the intersection or max + * @param {string} name Name of the dependency + * @param {string[]} gitBranches Git version strings, i.e, `github:phovea/phovea_core#semver:~7.0.1` + * @param {string[]} npmVersions Npm version strings, i.e, `^7.0.0` + */ + static mergeGithubVersions(name, gitBranches, npmVersions) { + const versions = Array.from(new Set(gitBranches.map((branch) => branch.split('#')[1]))); + const areSemverVersions = versions.every((version) => version.includes('semver')); + if (areSemverVersions) { + const prefix = gitBranches[0].split('#')[0]; + const parsedSemver = versions.map((version) => version.replace('semver:', '')); + const gitVersion = NpmUtils.mergeVersions(name, parsedSemver, false); + const allVersions = [gitVersion, ...npmVersions]; + const max = NpmUtils.mergeVersions(name, allVersions, false); + const areEqual = (v1, v2) => v1 === NpmUtils.mergeVersions(name, [v1, v2], false); + return areEqual(gitVersion, max) ? `${prefix}#semver:${gitVersion}` : max; + } + + const uniqueGitBranchesCount = versions.length; + if (uniqueGitBranchesCount === 1) { + return gitBranches[0]; + } + + throw new Error(chalk.red(`Versions ${chalk.white(gitBranches.join(', '))} point to different branches, which can lead to workspace errors.\nPlease use the same branch in all versions.`)); + } + + /** + * Finds the max version of an array of versions, i.e., `['1.2.3-alpha.1', '4.0.1-beta.0', '^3.8.0', '~4.0.0', '^2.8.0', '^4.0.0', '4.0.1-rc.0']`. + * Can handle range(caret, tilde) and prerelease versions. + * @param {Array} versions Array of semver versions including range versions. + */ + static findMaxVersion(versions) { + const nonRangeVersions = versions.filter((v) => !NpmUtils.hasRangeVersionTag(v)).map((v) => semver.prerelease(v) ? v : semver.coerce(v).version); // Filter out versions with `~` and `^` + // Sort versions and get max. Method `semver.rcomapre()` fails when you try comparing ranges, i.e., `~2.0.0` + const maxNonRangeVersion = nonRangeVersions.sort(semver.rcompare)[0] || nonRangeVersions[nonRangeVersions.length - 1]; + if (versions.some((v) => NpmUtils.hasRangeVersionTag(v))) { + const maxCaretRange = NpmUtils.findMaxCaretRange(versions); // ['^1.0.0', '^1.2.3']--> '^1.2.3' + const maxTildeRange = NpmUtils.findMaxTildeRange(versions); // ['~1.0.0', '~1.2.5']--> '~1.2.5' + const maxRange = maxCaretRange && maxTildeRange ? NpmUtils.findMaxRange(maxTildeRange, maxCaretRange) : maxTildeRange || maxCaretRange; + return maxNonRangeVersion && semver.gtr(maxNonRangeVersion, maxRange) ? maxNonRangeVersion : maxRange; // check maxNonRangeVersion is greater than all the versions possible in the range. + } + return maxNonRangeVersion; + } + + /** + * Find max between a tilde range and a caret range. + * @param {string} tildeRange , i.e., '~2.0.3' + * @param {string} caretRange , i.e., '^3.5.6' + * @returns {string} Returns a tilde range or a caret range. + */ + static findMaxRange(tildeRange, caretRange) { + const parsedCaretRange = semver.coerce(caretRange); // create a semver object from tag to access `major` and `version` properties + const parsedTildeRange = semver.coerce(tildeRange); + // tilde range can only be greater than caret range when its major is greater, i.e, '~3.5.6' > '^2.9.0' + if (semver.gt(semver.coerce(parsedTildeRange.major), semver.coerce(parsedCaretRange.major))) { + return tildeRange; + } else { // in any other case the caret range is greater (has a greater upper domain) + return caretRange; + } + } + + + /** + * Remove caret or tilde and format version. + * @param {string} range Possible values: `^2.3.4, `~3.0.0`. + * @returns {string} Return a version string without the range tags (tilde, caret). + */ + static removeRangeTag(range) { + return semver.prerelease(semver.minVersion(range)) ? semver.minVersion(range) : semver.coerce(range).version; + } + + /** + * Finds the max caret range of an array of caret ranges. + * @param {string[]} versions Possible values: `['^2.3.4', '^3.0.0']` + * @returns {void | string} Returns the caret range with the highest upper domain or void if there are no caret ranges in the versions array. + */ + static findMaxCaretRange(versions) { + const caretRanges = versions.filter((v) => v.startsWith('^')); + if (caretRanges) { + const parsedTags = caretRanges.map((r) => NpmUtils.removeRangeTag(r)); + const max = parsedTags.sort(semver.rcompare)[0] || caretRanges[caretRanges.length - 1]; + return caretRanges.find((r) => semver.eq(NpmUtils.removeRangeTag(r), max)); + } + } + + /** + * Finds the max tilde range of an array of tilde ranges. + * @param {string[]} versions Possible values: `['~2.3.4', '~3.0.0']` + * @returns {void | string} Returns the tilde range with the highest upper domain or void if there are no tilde ranges in the versions array. + */ + static findMaxTildeRange(versions) { + const tildeRanges = versions.filter((v) => v.startsWith('~')); + if (tildeRanges) { + const parsedTags = tildeRanges.map((r) => NpmUtils.removeRangeTag(r)); + const max = parsedTags.sort(semver.rcompare)[0] || tildeRanges[tildeRanges.length - 1]; + return tildeRanges.find((r) => semver.eq(NpmUtils.removeRangeTag(r), max)); + } + } + + /** + * Check if version is a caret or a tilde range. + * @param {string} version Possible values: `^=2.0.0`, `~3.5.6`, `3.4.5`. + * @returns {boolean} + */ + static hasRangeVersionTag(version) { + return /^[\^~]/gi.test(version); + } + + /** + * Find the highest possible target version from a list of versions. + * The target version can be a range of versions and supports the npm semver syntax. + * + * @param {string[]} sourceVersions List of source versions (e.g., [`2.0.0`, `v2.1.0`, ...]) + * @param {string} targetVersion version or version range supporting the npm semver syntax + * @returns {string|undefined} Returns the highest possible version from the list of source versions + */ + static findHighestVersion(sourceVersions, targetVersion) { + const semver = require('semver'); + + const regex = /^([\^~])/gi; // test if target version starts with ^ or ~ + // shortcut if exact target version is required + if (regex.test(targetVersion) === false) { + return sourceVersions.find((v) => v === targetVersion); + } + + const versions = sourceVersions + .map((version) => { + return { + version, + satisfied: semver.satisfies(version, targetVersion) + }; + }) + .filter((v) => v.satisfied) + .sort((a, b) => b.version.localeCompare(a.version)); // sort version descending + + return (versions[0]) ? versions[0].version : undefined; // first element = highest version + } + + /** + * Checks if a given name is a git commit. + * + * @param {string} name name to check + * @returns {boolean} Returns `true` if name is a git commit. Otherwise returns `false`. + */ + static isGitCommit(name) { + return /^[0-9a-f]+$/gi.test(name); + } + + /** + * Checks if a given name is a version tag that starts with `v`. + * + * @param {string} name name to check + * @returns {boolean} Returns `true` if name is a version tag. Otherwise returns `false`. + */ + static isExactVersionTag(name) { + return /^v\d+.*/gi.test(name); + } + + /** + * Checks if a given name is an advanced version tag supporting npm's version ranges. + * A version tag must start with `^v`, `~v`. + * + * @see https://docs.npmjs.com/misc/semver#advanced-range-syntax + * + * @param {string} name name to check + * @returns {boolean} Returns `true` if name is a version tag. Otherwise returns `false`. + */ + static isAdvancedVersionTag(name) { + return /^[\^~]v/gi.test(name); + } + + /** + * Extract tag names starting with `v` (e.g., `v2.0.1`) from the given git log. + * The git log must have the structure: + * ``` + * refs/tags/ + * ``` + * The output can be generated using the command `git ls-remote --tags ` + * + * @param {string} gitLog Console output with list of git hashes and git tags + * @returns {string[]} Returns an array with tag names starting with `v` + */ + static extractVersionsFromGitLog(gitLog) { + const regex = /refs\/tags\/([v]{1}.*[^}])\n/gm; // capture all tag names starting with `v` + let m; + const versionTags = []; + + while ((m = regex.exec(gitLog)) !== null) { + // This is necessary to avoid infinite loops with zero-width matches + if (m.index === regex.lastIndex) { + regex.lastIndex++; + } + versionTags.push(m[1]); + } + + return versionTags; + } + /** + * Checks if version contains `-` thus branch is not master. + * @param {string} version Package.json version + */ + static useDevVersion(version) { + return (version || '').includes('-'); + } +}; \ No newline at end of file diff --git a/utils/PipUtils.js b/utils/PipUtils.js new file mode 100644 index 00000000..e95bcc73 --- /dev/null +++ b/utils/PipUtils.js @@ -0,0 +1,114 @@ +'use strict'; + +const semver = require('semver'); +const {intersect} = require('semver-intersect'); + + +module.exports = class PipUtils { + + /** + * Finds the intersection of an array of pip versions, i.e, `^=2.2.0`. If no intersection exists the maximum version is returned. + * @param {string} name Name of the requirement. + * @param {string[]} versions Array of versions found for the requirement in the different workspace plugins. + * TODO find the correct max version for ranges... + */ + static mergePipVersions(name, versions) { + versions = Array.from(new Set(versions)).filter(Boolean); // unique + not empty entries + if (versions.length === 0) { + return ''; + } + if (versions.length === 1) { + return versions[0]; + } + const gitBranch = versions.find((d) => d.startsWith('@')); + if (gitBranch) { + return gitBranch; + } + + const nodeVersions = versions.map(PipUtils.toSemVer); + // first try to find a good intersection + try { + return PipUtils.toPipVersion(intersect(...nodeVersions).toString()); + } catch (e) { + // map to base version, sort descending take first + const max = PipUtils.toPipVersion(nodeVersions.map(semver.coerce).sort(semver.rcompare)[0]) || versions[versions.length - 1]; + console.warn(`cannot find common intersecting version for ${name} = ${versions.join(', ')}, taking max "${max}" for now`); + return max.toString(); + } + } + + /** + * Transforms a pip to semver version, i.e, `~=2.0.0` --> `~2.0.0` + * @param {string} version + */ + static toSemVer(version) { + if (version.startsWith('~=')) { + return `~${version.slice(2)}`; + } + if (version.startsWith('^=')) { + return `^${version.slice(2)}`; + } + if (version.startsWith('==')) { + return version.slice(2); + } + return version; + } + + /** + * Transforms a semver to pip version, i.e, `2.0.0` --> `==2.0.0` + * @param {string} version + */ + static toPipVersion(version) { + if (!version) { + return null; + } + version = version.toString(); + if (version.startsWith('~')) { + return `~=${version.slice(1)}`; + } + if (version.startsWith('^')) { + return `^=${version.slice(1)}`; + } + // TODO range + // fix + return `==${version}`; + } + + /** + * Transform the requirements.txt file from a string to an object with name as key and version as value. + * @param {string} file Requirements.txt file. + */ + static parseRequirements(file) { + if (!file) { + return {}; + } + file = file.trim(); + if (file === '') { + return {}; + } + const versions = {}; + file.split('\n').forEach((line) => { + line = line.trim(); + + if (line.startsWith('-e')) { + // editable special dependency + const branchSeparator = line.indexOf('@'); + const name = line.slice(0, branchSeparator).trim(); + versions[name] = line.slice(branchSeparator).trim(); + return; + } + + if (line.startsWith('#') || line.startsWith('-')) { + return; // skip + } + const versionSeparator = line.search(/[\^~=>!]/); + if (versionSeparator >= 0) { + const name = line.slice(0, versionSeparator).trim(); + versions[name] = line.slice(versionSeparator).trim(); + } else { + versions[line] = ''; + } + }); + return versions; + } +}; \ No newline at end of file diff --git a/utils/RepoUtils.js b/utils/RepoUtils.js new file mode 100644 index 00000000..1a9a8138 --- /dev/null +++ b/utils/RepoUtils.js @@ -0,0 +1,186 @@ +const known = require('./known'); +const _ = require('lodash'); + +module.exports = class RepoUtils { + + /** + * Transforms repo name or SSH url to an https url + * @param {string} repo Repo + */ + static toHTTPRepoUrl(repo) { + if (repo.startsWith('http')) { + return repo; + } + if (repo.startsWith('git@')) { + const m = repo.match(/(https?:\/\/([^/]+)\/|git@(.+):)([\w\d-_/]+)(.git)?/); + return `https://${m[3]}/${m[4]}.git`; + } + if (!repo.includes('/')) { + repo = `Caleydo/${repo}`; + } + return `https://github.com/${repo}.git`; + } + + + /** + * Transforms repo name or http(s) url to an SSH url + * @param {string} repo Repo + */ + static toSSHRepoUrl(repo) { + if (repo.startsWith('git@')) { + return repo; + } + if (repo.startsWith('http')) { + const m = repo.match(/(https?:\/\/([^/]+)\/|git@(.+):)([\w\d-_/]+)(.git)?/); + return `git@${m[2]}:${m[4]}.git`; + } + if (!repo.includes('/')) { + repo = `Caleydo/${repo}`; + } + return `git@github.com:${repo}.git`; + } + + /** + * Transforms repo http(s) url to an SSH url. + * @param {string} repo Repo. + */ + static toSSHRepoUrlFromHTTP(repo) { + if (repo.startsWith('git@')) { + return repo; + } + const m = repo.match(/(https?:\/\/([^/]+)\/|git@(.+):)([\w\d-_/]+)(.git)?/); + if (m) { + return `git@${m[2]}:${m[4]}.git`; + } + return repo; + } + + /** + * Extracts organization and repo names from a SSH or an http url. + * @param {string} repo Repo. + */ + static simplifyRepoUrl(repo) { + // matches http and git urls + const m = repo.match(/(https?:\/\/[^/]+\/|git@.+:)([\w\d-_/]+)(.git)?/); + if (m) { + // just the repo part + return m[2]; + } + return repo; + } + /** + * Checks if repo name includes organization. If not it adds `Caleydo/` prefix. + * @param {string} name Name of the repo. + */ + static toBaseName(name) { + if (name.includes('/')) { + return name; + } + return `Caleydo/${name}`; + } + + /** + * Extracts repo name and removes `_product` suffix from name. + * @param {string} basename Repo name optionaly prefixed with the organization name, i.e, `Caleydo/ordino`. + */ + static toCWD(basename) { + try { + let match = basename.match(/.*\/(.*)/)[1]; + if (match.endsWith('_product')) { + match = match.slice(0, -8); + } + return match; + + } catch { + return basename; + } + } + + static toRepository(plugin, useSSH) { + const p = known.plugin.byName(plugin); + return useSSH ? RepoUtils.RtoSSHRepoUrl(p.repository) : RepoUtils.toHTTPRepoUrl(p.repository); + } + /** + * Parses the `phovea_product.json` file and returns an array of objects containing the repo name and branch + * @param {{}} product + */ + static parsePhoveaProduct(product) { + const names = new Set(); + const repos = []; + product.forEach((p) => { + const repo = p.repo || 'phovea/' + p.name; + if (!names.has(repo)) { + names.add(repo); + repos.push({ + repo, + branch: p.branch || 'master' + }); + } + (p.additional || []).forEach((pi) => { + const repo = pi.repo || 'phovea/' + pi.name; + if (!names.has(repo)) { + names.add(repo); + repos.push({ + repo, + branch: pi.branch || 'master' + }); + } + }); + }); + return repos; + } + + /** + * Finds the aliases of the selected modules and libraries. + * @param {string[]} moduleNames Array of modules, i.e, `['phovea_clue', 'phovea_core']` + * @param {string[]} libraryNames Array of libraries, i.e, `['d3', 'font-awesome]` + */ + static toLibraryAliasMap(moduleNames = [], libraryNames = []) { + let r = {}; + moduleNames.forEach((m) => { + const plugin = known.plugin.byName(m); + if (!plugin) { + console.log('cant find plugin: ', m); + return; + } + libraryNames.push(...(plugin.libraries || [])); + }); + libraryNames.forEach((l) => { + const lib = known.lib.byName(l); + if (!lib) { + console.log('cant find library: ', l); + return; + } + _.merge(r, lib.aliases); + }); + return r; + } + + /** + * Finds the externals of the selected modules and libraries. + * @param {string[]} moduleNames Array of modules, i.e, `['phovea_clue', 'phovea_core']` + * @param {string[]} libraryNames Array of libraries, i.e, `['d3', 'font-awesome]` + */ + static toLibraryExternals(moduleNames = [], libraryNames = []) { + let r = []; + moduleNames.forEach((m) => { + const plugin = known.plugin.byName(m); + if (!plugin) { + console.log('cant find plugin: ', m); + return; + } + r.push(...(plugin.externals || [])); + libraryNames.push(...(plugin.libraries || [])); + }); + libraryNames.forEach((l) => { + const lib = known.lib.byName(l); + if (!lib) { + console.log('cant find library: ', l); + return; + } + r.push(lib.name); + r.push(...(lib.externals || [])); + }); + return Array.from(new Set(r)); + } +}; diff --git a/utils/SpawnUtils.js b/utils/SpawnUtils.js new file mode 100644 index 00000000..43f561ce --- /dev/null +++ b/utils/SpawnUtils.js @@ -0,0 +1,56 @@ +'use strict'; + +const spawnSync = require('child_process').spawnSync; + +module.exports = class SpawnUtils { + + /** + * Execute a shell command - abort if it fails. + * @param {string} cmd Command to execute. + * @param {string | string[]} argline Arguments to execute the command with. + * @param {string} cwd The directory in Which the command should be excecuted. + */ + static spawnOrAbort(cmd, argline, cwd, verbose) { + const result = SpawnUtils.spawnSync(cmd, argline, cwd, verbose); + const stdout = result.stdout; + if (SpawnUtils.failed(result)) { + return SpawnUtils.abort(`Failed: "${cmd} ${Array.isArray(argline) ? argline.join(' ') : argline}" - status code: ${result.status}`); + + } else if (stdout && stdout.toString()) { + console.log(stdout.toString().trim()); + } + + return Promise.resolve(cmd); + } + + /** + * Execute shell command + * @param {string} cmd Command to execute. + * @param {string | string[]} argline Arguments to execute the command with. + * @param {string} cwd The directory in Which the command should be excecuted. + */ + static spawnSync(cmd, argline, cwd, verbose) { + const options = { + ...cwd ? {cwd} : {}, + ...verbose ? {stdio: 'inherit'} : {} + }; + + return spawnSync(cmd, Array.isArray(argline) ? argline : argline.split(' '), options); + } + + /** + * Shell command result object + * @param {{}} spawnResult + */ + static failed(spawnResult) { + return spawnResult.status !== 0; + } + + /** + * Reject Promise with error message. + * @param {string} msg Error message to log. + */ + static abort(msg) { + return Promise.reject(msg ? msg : 'Step Failed: Aborting'); + } +}; diff --git a/utils/WorkspaceUtils.js b/utils/WorkspaceUtils.js new file mode 100644 index 00000000..01e46141 --- /dev/null +++ b/utils/WorkspaceUtils.js @@ -0,0 +1,147 @@ +const path = require('path'); +const glob = require('glob').sync; +const RepoUtils = require('./RepoUtils'); +const GeneratorUtils = require('./GeneratorUtils'); +const known = require('./known'); +const SpawnUtils = require('./SpawnUtils'); + +module.exports = class WorkspaceUtils { + /** + * Returns a list of all web plugins (i.e., the directory names) for a given workspace directory. + * A valid web plugin must contain a `webpack.config.js` file. Otherwise the directory is ignored. + * + * @param {string} workspaceDirectory Workspace directory used as current work directory (cwd) + * @returns {string[]} List of plugin names + */ + static listWebPlugins(workspaceDirectory) { + const files = glob('*/webpack.config.js', { + cwd: workspaceDirectory + }); + return files.map(path.dirname).sort(); + } + + /** + * Returns a list of all server plugins (i.e., the directory names) for a given workspace directory. + * A valid server plugin must contain a `requirements.txt` file. Otherwise the directory is ignored. + * + * @param {string} workspaceDirectory Workspace directory used as current work directory (cwd) + * @returns {string[]} List of plugin names + */ + static listServerPlugins(workspaceDirectory) { + const files = glob('*/requirements.txt', { + cwd: workspaceDirectory + }); + return files.map(path.dirname).sort(); + } + + /** + * Returns a list of all plugins (i.e., the directory names) for a given workspace directory. + * A valid web plugin must contain a `webpack.config.js` file. + * A valid server plugin must contain a `requirements.txt` file. + * Otherwise the directory is ignored. + * + * @param {string} workspaceDirectory Workspace directory used as current work directory (cwd) + * @returns {string[]} List of plugin names + */ + static listAllPlugins(workspaceDirectory) { + const webPlugins = WorkspaceUtils.listWebPlugins(workspaceDirectory); + const serverPlugins = WorkspaceUtils.listServerPlugins(workspaceDirectory); + return [...new Set([].concat(...webPlugins, ...serverPlugins))].sort(); + } + + /** + * Extract default app from `phovea_product.json`. + * @param {Object} product Content of the `phovea_product.json`. + */ + static findDefaultApp(product) { + if (!product) { + return null; + } + for (let p of product) { + if (p.type === 'web') { + return { + name: p.repo.slice(p.repo.lastIndexOf('/') + 1).replace('.git', ''), + additional: p.additional.map(({name}) => name) + }; + } + return null; + } + } + /** + * Calls the clone repo generator with the name of the repo. + * @param {string} repo Repository name. + * @param {string} branch Branch to clone + * @param {Object} extras Extra git options + * @param {string} dir Where to clone the repo + * @param {string} cwd Where to run the generator + * @param {boolean} cloneSSH SSH or HTTP url + */ + static cloneRepo(repo, branch, extras, dir = '', cwd, cloneSSH) { + const repoUrl = cloneSSH ? RepoUtils.toSSHRepoUrl(repo) : RepoUtils.toHTTPRepoUrl(repo); + return GeneratorUtils.yo(`clone-repo`, { + branch, + extras: extras || '', + dir, + cwd + }, repoUrl, cwd); // repository URL as argument + } + + static resolveNeighbors(plugins, useSSH, types, shallow, destinationPath) { + let missing = []; + const addMissing = (p) => { + const configPath = path.join(destinationPath, p + '.yo-rc.json'); + console.log(configPath); + const config = this.fs.readJSON(configPath, {'generator-phovea': {}})['generator-phovea']; + let modules = [].concat(config.modules || [], config.smodules || []); + console.log(`${p} => ${modules.join(' ')}`); + if (types && types !== 'both') { + // filter to just certain sub types + const filter = types === 'web' ? known.plugin.isTypeWeb : known.plugin.isTypeServer; + modules = modules.filter((m) => known.plugin.isTypeHybrid(m) || filter(m)); + } + missing.push(...modules.filter((m) => plugins.indexOf(m) < 0)); + }; + + plugins.forEach(addMissing); + + while (missing.length > 0) { + let next = missing.shift(); + let repo = RepoUtils.toRepository(next, useSSH); + let args = ['clone', repo]; + if (shallow) { + args.splice(1, 0, '--depth', '1'); + } + console.log(`git clone ${args.join(' ')}`); + SpawnUtils.spaw('git', args, { + cwd: destinationPath + }); + plugins.push(next); + addMissing(next); + } + } + + + static resolveAllNeighbors(useSSH, types, destinationPath) { + const files = glob('*/.yo-rc.json', { + cwd: destinationPath + }); + const plugins = files.map(path.dirname); + return WorkspaceUtils.resolveNeighbors(plugins, useSSH, types, destinationPath); + } + + + /** + * Creates an array of additional plugins depending on the type. + * @param {string} type Type of the plugin. `web, static, python` + */ + static buildPossibleAdditionalPlugins(type) { + const toDescription = (d) => ({ + value: {name: d.name, repo: RepoUtils.simplifyRepoUrl(d.repository)}, + name: `${d.name}: ${d.description}`, + short: d.name + }); + + const plugins = known.plugin; + return ((type === 'web' || type === 'static') ? plugins.listWeb : plugins.listServer).map(toDescription); + } +}; diff --git a/utils/index.js b/utils/index.js deleted file mode 100644 index d8739f83..00000000 --- a/utils/index.js +++ /dev/null @@ -1,270 +0,0 @@ -'use strict'; -const Generator = require('yeoman-generator'); -const {merge, template} = require('lodash'); -const path = require('path'); -const glob = require('glob').sync; -const fs = require('fs-extra'); -const chalk = require('chalk'); - -/** - * Modify package.json by passing the configuration - * @param {object} config Current configuration - * @param {*} unset - * @param {*} extra - * @param {*} replaceExtra - * @param {string} cwd The directory from which the generator is being called, i.e., `tdp_core/`. - * If cwd is provided than the `package.json` is going to be written to that subdirectory, otherwise to the current directory. - */ -function patchPackageJSON(config, unset, extra, replaceExtra, cwd = '') { - const pkg = this.fs.readJSON(this.destinationPath(cwd + 'package.json'), {}); - let pkgPatch; - if (fs.existsSync(this.templatePath('package.tmpl.json'))) { - pkgPatch = JSON.parse(template(this.fs.read(this.templatePath('package.tmpl.json')))(config)); - } else { - pkgPatch = {}; - } - merge(pkg, pkgPatch); - if (replaceExtra && extra) { - Object.assign(pkg, extra); - } else { - merge(pkg, extra || {}); - } - - (unset || []).forEach((d) => delete pkg[d]); - - this.fs.writeJSON(this.destinationPath(cwd + 'package.json'), pkg); -} - -function stringifyInline(obj, space) { - let base = JSON.stringify(obj, null, ' '); - // common style - base = base.replace(/"/g, '\''); - // prefix with space - base = base.split('\n').map((l) => space + l).join('\n'); - return base.substring(space.length); // skip the first space -} - -function stringifyAble(config) { - return Object.assign({ - stringifyPython: (obj, space) => { - let base = stringifyInline(obj, space); - // python different true false - base = base.replace(/: true/g, ': True').replace(/: false/g, ': False'); - return base; - }, - stringify: stringifyInline, - isWeb: (p) => { - const { - plugin - } = require('./known'); - return plugin.isTypeWeb(p); - } - }, config); -} - -/** - * Copies the template files to the current directory or to a subdirectory if `cwd` is provided. - * @param {object} config Current configuration - * @param {*} withSamples - * @param {string} cwd The directory from which the generator is being called, i.e., `tdp_core/`. - * If `cwd` is provided than the `package.json` is going to be written to that subdirectory, otherwise to the current directory. - */ -function writeTemplates(config, withSamples, cwd = '') { - const includeDot = { - globOptions: { - dot: true - } - }; - - const pattern = stringifyAble(config); - - const copyTpl = (base, dbase, initialize_once) => { - // see https://github.com/SBoudrias/mem-fs-editor/issues/25 - // copyTpl doesn't support glob options - const f = glob(base + '/**/*', { - dot: true - }); - f.forEach((fi) => { - const rel = path.relative(base, fi); - if(!initialize_once || !fs.existsSync(this.destinationPath(cwd + dbase + rel))) { - this.fs.copyTpl(fi, this.destinationPath(cwd + dbase + rel), pattern); - } - }); - }; - - const copy = (prefix) => { - if (fs.existsSync(this.templatePath(prefix + 'plain'))) { - this.fs.copy(this.templatePath(prefix + 'plain/**/*'), this.destinationPath(cwd), includeDot); - } - - const plainTemplatePath = this.templatePath(prefix + 'plain_initialize_once'); - if (fs.existsSync(plainTemplatePath)) { - copyTpl(plainTemplatePath, '', true); - } - - copyTpl(this.templatePath(prefix + 'processed'), '', false); - - if (config.name) { - if (fs.existsSync(this.templatePath(prefix + 'pluginname_plain'))) { - this.fs.copy(this.templatePath(prefix + 'pluginname_plain/**/*'), this.destinationPath(cwd + config.name.toLowerCase() + '/'), includeDot); - } - copyTpl(this.templatePath(prefix + 'pluginname_processed'), cwd + config.name.toLowerCase() + '/', false); - } - }; - copy(''); - if (withSamples) { - copy('sample_'); - } -} - -function useDevVersion(cwd = '') { - const pkg = this.fs.readJSON(this.destinationPath(cwd + 'package.json'), { - version: '1.0.0' - }); - // assumption having a suffix like -SNAPSHOT use the dev version - return (pkg.version || '').includes('-'); -} - -class BaseInitPluginGenerator extends Generator { - - constructor(args, options, basetype) { - super(args, options); - this.type = path.basename(path.dirname(this.resolved)).substring(5); // init-web ... web - this.basetype = basetype || 'web'; - // Make options available - this.option('skipInstall'); - this.option('noSamples'); - this.option('useDefaults'); - this.cwd = ''; - } - - initializing() { - if (this._isInvalidWorkspace()) { - throw new Error(chalk.red('Execution failed, because a ".yo-rc.json" and ".yo-rc-workspace.json" file was found. If this directory is a workspace, please remove the ".yo-rc.json" file and try again.\n')); - } - - this.composeWith(['phovea:_check-own-version', 'phovea:check-node-version']); - - this.config.defaults({ - type: this.type - }); - } - - _isWorkspace() { - return fs.existsSync(this.destinationPath('.yo-rc-workspace.json')); - } - - _hasConfigFile() { - return fs.existsSync(this.destinationPath('.yo-rc.json')); - } - - /** - * If there is both a `.yo-rc-workspace.json` and `.yo-rc.json` file in the current directory - * the workspace is invalid and the generator cannot function properly. - */ - _isInvalidWorkspace() { - return this._isWorkspace() && this._hasConfigFile(); - } - - /** - * Create a subdirectory in the current directory. - * Initialize the property cwd. - * @param {string} dir Directory name. - */ - _mkdir(dir) { - if (this._isWorkspace() && this.cwd !== dir + '/') { - this.cwd = dir + '/'; - this.log('Create directory: ' + dir); - return new Promise((resolve) => fs.ensureDir(dir, resolve)); - } - } - - readmeAddon() { - const f = this.templatePath('README.partial.md'); - if (fs.existsSync(f)) { - return this.fs.read(f); - } - return ''; - } - - default() { - this.composeWith('phovea:_init-' + this.basetype, { - options: Object.assign({ - readme: this.readmeAddon() + (this.options.readme ? `\n\n${this.options.readme}` : '') - }, this.options), - isWorkspace: this._isWorkspace() // inform the sub generator that the cwd is the workspace to avoid reading prompt default values from the workspace package.json - }); - } - - writing() { - const config = this.config.getAll(); - this._mkdir(config.cwd || config.name); - if (fs.existsSync(this.templatePath('package.tmpl.json'))) { - this._patchPackageJSON(config, null, null, this.cwd); - } - if (fs.existsSync(this.templatePath('_gitignore'))) { - this.fs.copy(this.templatePath('_gitignore'), this.destinationPath(this.cwd + '.gitignore')); - } - - this._writeTemplates(config, !this.options.noSamples, this.cwd); - - } - - _patchPackageJSON(config, unset, extra, cwd) { - return patchPackageJSON.call(this, config, unset, extra, null, cwd); - } - - _writeTemplates(config, withSamples, cwd) { - return writeTemplates.call(this, config, withSamples, cwd); - } -} - -class BaseInitServerGenerator extends BaseInitPluginGenerator { - - constructor(args, options) { - super(args, options, 'python'); - } - - initializing() { - // since just last in the hierarchy used, need to do super calls - super.initializing(); - } - - default() { - return super.default(); - } - - writing() { - return super.writing(); - } -} - -class BaseInitHybridGenerator extends BaseInitPluginGenerator { - - constructor(args, options) { - super(args, options, 'hybrid'); - } - - initializing() { - // since just last in the hierarchy used, need to do super calls - super.initializing(); - } - - default() { - return super.default(); - } - - writing() { - return super.writing(); - } -} - -module.exports = { - Base: BaseInitPluginGenerator, - BasePython: BaseInitServerGenerator, - BaseHybrid: BaseInitHybridGenerator, - patchPackageJSON: patchPackageJSON, - writeTemplates: writeTemplates, - stringifyAble: stringifyAble, - useDevVersion: useDevVersion -}; diff --git a/utils/list-plugins.js b/utils/list-plugins.js deleted file mode 100644 index 5bb7b43e..00000000 --- a/utils/list-plugins.js +++ /dev/null @@ -1,52 +0,0 @@ -const path = require('path'); -const glob = require('glob').sync; - -/** - * Returns a list of all web plugins (i.e., the directory names) for a given workspace directory. - * A valid web plugin must contain a `webpack.config.js` file. Otherwise the directory is ignored. - * - * @param {string} workspaceDirectory Workspace directory used as current work directory (cwd) - * @returns {string[]} List of plugin names - */ -function listWebPlugins(workspaceDirectory) { - const files = glob('*/webpack.config.js', { - cwd: workspaceDirectory - }); - return files.map(path.dirname).sort(); -} - -/** - * Returns a list of all server plugins (i.e., the directory names) for a given workspace directory. - * A valid server plugin must contain a `requirements.txt` file. Otherwise the directory is ignored. - * - * @param {string} workspaceDirectory Workspace directory used as current work directory (cwd) - * @returns {string[]} List of plugin names - */ -function listServerPlugins(workspaceDirectory) { - const files = glob('*/requirements.txt', { - cwd: workspaceDirectory - }); - return files.map(path.dirname).sort(); -} - -/** - * Returns a list of all plugins (i.e., the directory names) for a given workspace directory. - * A valid web plugin must contain a `webpack.config.js` file. - * A valid server plugin must contain a `requirements.txt` file. - * Otherwise the directory is ignored. - * - * @param {string} workspaceDirectory Workspace directory used as current work directory (cwd) - * @returns {string[]} List of plugin names - */ -function listAllPlugins(workspaceDirectory) { - const webPlugins = listWebPlugins(workspaceDirectory); - const serverPlugins = listServerPlugins(workspaceDirectory); - return [...new Set([].concat(...webPlugins, ...serverPlugins))].sort(); -} - -module.exports = { - listWebPlugins, - listServerPlugins, - listAllPlugins -}; - diff --git a/utils/pip.js b/utils/pip.js deleted file mode 100644 index 290d5dd8..00000000 --- a/utils/pip.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Created by Samuel Gratzl on 05.04.2017. - */ - -function parseRequirements(file) { - if (!file) { - return {}; - } - file = file.trim(); - if (file === '') { - return {}; - } - const versions = {}; - file.split('\n').forEach((line) => { - line = line.trim(); - - if (line.startsWith('-e')) { - // editable special dependency - const branchSeparator = line.indexOf('@'); - const name = line.slice(0, branchSeparator).trim(); - versions[name] = line.slice(branchSeparator).trim(); - return; - } - - if (line.startsWith('#') || line.startsWith('-')) { - return; // skip - } - const versionSeparator = line.search(/[\^~=>!]/); - if (versionSeparator >= 0) { - const name = line.slice(0, versionSeparator).trim(); - versions[name] = line.slice(versionSeparator).trim(); - } else { - versions[line] = ''; - } - }); - return versions; -} - -module.exports.parseRequirements = parseRequirements; diff --git a/utils/repo.js b/utils/repo.js deleted file mode 100644 index 80d8a810..00000000 --- a/utils/repo.js +++ /dev/null @@ -1,49 +0,0 @@ - -module.exports.toHTTPRepoUrl = (repo) => { - if (repo.startsWith('http')) { - return repo; - } - if (repo.startsWith('git@')) { - const m = repo.match(/(https?:\/\/([^/]+)\/|git@(.+):)([\w\d-_/]+)(.git)?/); - return `https://${m[3]}/${m[4]}.git`; - } - if (!repo.includes('/')) { - repo = `Caleydo/${repo}`; - } - return `https://github.com/${repo}.git`; -}; - -module.exports.toSSHRepoUrl = (repo) => { - if (repo.startsWith('git@')) { - return repo; - } - if (repo.startsWith('http')) { - const m = repo.match(/(https?:\/\/([^/]+)\/|git@(.+):)([\w\d-_/]+)(.git)?/); - return `git@${m[2]}:${m[4]}.git`; - } - if (!repo.includes('/')) { - repo = `Caleydo/${repo}`; - } - return `git@github.com:${repo}.git`; -}; - -module.exports.toSSHRepoUrlFromHTTP = (repo) => { - if (repo.startsWith('git@')) { - return repo; - } - const m = repo.match(/(https?:\/\/([^/]+)\/|git@(.+):)([\w\d-_/]+)(.git)?/); - if (m) { - return `git@${m[2]}:${m[4]}.git`; - } - return repo; -}; - -module.exports.simplifyRepoUrl = (repo) => { - // matches http and git urls - const m = repo.match(/(https?:\/\/[^/]+\/|git@.+:)([\w\d-_/]+)(.git)?/); - if (m) { - // just the repo part - return m[2]; - } - return repo; -}; diff --git a/utils/version.js b/utils/version.js deleted file mode 100644 index 998046e9..00000000 --- a/utils/version.js +++ /dev/null @@ -1,296 +0,0 @@ -'use strict'; - -const semver = require('semver'); -const chalk = require('chalk'); -const { - intersect -} = require('semver-intersect'); - -function mergeVersions(name, versions, logWarning = true) { - if (versions.some((v) => v === 'latest')) { - throw new Error(chalk.red('Invalid version. Please avoid using version latest in package.json.')); - } - // create set - versions = Array.from(new Set(versions)); - if (versions.length === 1) { - return versions[0]; - } - // filter for github and gitlab version strings - const gitRepos = versions.filter((d) => d.includes('github') || d.includes('gitlab')); - // npm version strings - const noGitRepos = versions.filter((v) => !gitRepos.includes(v)); - - if (gitRepos.length) { - return mergeGithubVersions(name, gitRepos, noGitRepos); - } - - try { - return intersect(...noGitRepos).toString(); - } catch (e) { - // map to base version, sort descending take first - const max = findMaxVersion(noGitRepos); - if (logWarning) { - console.warn(`cannot find common intersecting version for ${name} = ${versions.join(', ')}, taking max "${max}" for now`); - } - - return max.toString(); - } -} - -module.exports.mergeVersions = mergeVersions; - - -/** - * Extracts git version strings, compares them with the npm versions and returns the intersection or max - * @param {string} name Name of the dependency - * @param {string[]} gitBranches Git version strings, i.e, `github:phovea/phovea_core#semver:~7.0.1` - * @param {string[]} npmVersions Npm version strings, i.e, `^7.0.0` - */ -function mergeGithubVersions(name, gitBranches, npmVersions) { - const versions = Array.from(new Set(gitBranches.map((branch) => branch.split('#')[1]))); - const areSemverVersions = versions.every((version) => version.includes('semver')); - if (areSemverVersions) { - const prefix = gitBranches[0].split('#')[0]; - const parsedSemver = versions.map((version) => version.replace('semver:', '')); - const gitVersion = mergeVersions(name, parsedSemver, false); - const allVersions = [gitVersion, ...npmVersions]; - const max = mergeVersions(name, allVersions, false); - const areEqual = (v1, v2) => v1 === mergeVersions(name, [v1, v2], false); - return areEqual(gitVersion, max) ? `${prefix}#semver:${gitVersion}` : max; - } - - const uniqueGitBranchesCount = versions.length; - if (uniqueGitBranchesCount === 1) { - return gitBranches[0]; - } - - throw new Error(chalk.red(`Versions ${chalk.white(gitBranches.join(', '))} point to different branches, which can lead to workspace errors.\nPlease use the same branch in all versions.`)); -} - -/** - * Finds the max version of an array of versions, i.e., `['1.2.3-alpha.1', '4.0.1-beta.0', '^3.8.0', '~4.0.0', '^2.8.0', '^4.0.0', '4.0.1-rc.0']`. - * Can handle range(caret, tilde) and prerelease versions. - * @param {Array} versions Array of semver versions including range versions. - */ -function findMaxVersion(versions) { - const nonRangeVersions = versions.filter((v) => !hasRangeVersionTag(v)).map((v) => semver.prerelease(v) ? v : semver.coerce(v).version); // Filter out versions with `~` and `^` - // Sort versions and get max. Method `semver.rcomapre()` fails when you try comparing ranges, i.e., `~2.0.0` - const maxNonRangeVersion = nonRangeVersions.sort(semver.rcompare)[0] || nonRangeVersions[nonRangeVersions.length - 1]; - if (versions.some((v) => hasRangeVersionTag(v))) { - const maxCaretRange = findMaxCaretRange(versions); // ['^1.0.0', '^1.2.3']--> '^1.2.3' - const maxTildeRange = findMaxTildeRange(versions); // ['~1.0.0', '~1.2.5']--> '~1.2.5' - const maxRange = maxCaretRange && maxTildeRange ? findMaxRange(maxTildeRange, maxCaretRange) : maxTildeRange || maxCaretRange; - return maxNonRangeVersion && semver.gtr(maxNonRangeVersion, maxRange) ? maxNonRangeVersion : maxRange; // check maxNonRangeVersion is greater than all the versions possible in the range. - } - return maxNonRangeVersion; -} - -/** - * Find max between a tilde range and a caret range. - * @param {string} tildeRange , i.e., '~2.0.3' - * @param {string} caretRange , i.e., '^3.5.6' - * @returns {string} Returns a tilde range or a caret range. - */ -function findMaxRange(tildeRange, caretRange) { - const parsedCaretRange = semver.coerce(caretRange); // create a semver object from tag to access `major` and `version` properties - const parsedTildeRange = semver.coerce(tildeRange); - // tilde range can only be greater than caret range when its major is greater, i.e, '~3.5.6' > '^2.9.0' - if (semver.gt(semver.coerce(parsedTildeRange.major), semver.coerce(parsedCaretRange.major))) { - return tildeRange; - } else { // in any other case the caret range is greater (has a greater upper domain) - return caretRange; - } -} - -module.exports.findMaxVersion = findMaxVersion; - -/** - * Remove caret or tilde and format version. - * @param {string} range Possible values: `^2.3.4, `~3.0.0`. - * @returns {string} Return a version string without the range tags (tilde, caret). - */ -function removeRangeTag(range) { - return semver.prerelease(semver.minVersion(range)) ? semver.minVersion(range) : semver.coerce(range).version; -} - -/** - * Finds the max caret range of an array of caret ranges. - * @param {string[]} versions Possible values: `['^2.3.4', '^3.0.0']` - * @returns {void | string} Returns the caret range with the highest upper domain or void if there are no caret ranges in the versions array. - */ -function findMaxCaretRange(versions) { - const caretRanges = versions.filter((v) => v.startsWith('^')); - if (caretRanges) { - const parsedTags = caretRanges.map((r) => removeRangeTag(r)); - const max = parsedTags.sort(semver.rcompare)[0] || caretRanges[caretRanges.length - 1]; - return caretRanges.find((r) => semver.eq(removeRangeTag(r), max)); - } -} - -/** - * Finds the max tilde range of an array of tilde ranges. - * @param {string[]} versions Possible values: `['~2.3.4', '~3.0.0']` - * @returns {void | string} Returns the tilde range with the highest upper domain or void if there are no tilde ranges in the versions array. - */ -function findMaxTildeRange(versions) { - const tildeRanges = versions.filter((v) => v.startsWith('~')); - if (tildeRanges) { - const parsedTags = tildeRanges.map((r) => removeRangeTag(r)); - const max = parsedTags.sort(semver.rcompare)[0] || tildeRanges[tildeRanges.length - 1]; - return tildeRanges.find((r) => semver.eq(removeRangeTag(r), max)); - } -} - -/** - * Check if version is a caret or a tilde range. - * @param {string} version Possible values: `^=2.0.0`, `~3.5.6`, `3.4.5`. - * @returns {boolean} - */ -function hasRangeVersionTag(version) { - return /^[\^~]/gi.test(version); -} - -function toSemVer(version) { - if (version.startsWith('~=')) { - return `~${version.slice(2)}`; - } - if (version.startsWith('^=')) { - return `^${version.slice(2)}`; - } - if (version.startsWith('==')) { - return version.slice(2); - } - return version; -} - -function toPipVersion(version) { - if (!version) { - return null; - } - version = version.toString(); - if (version.startsWith('~')) { - return `~=${version.slice(1)}`; - } - if (version.startsWith('^')) { - return `^=${version.slice(1)}`; - } - // TODO range - // fix - return `==${version}`; -} -// -module.exports.mergePipVersions = (name, versions) => { - versions = Array.from(new Set(versions)).filter(Boolean); // unique + not empty entries - if (versions.length === 0) { - return ''; - } - if (versions.length === 1) { - return versions[0]; - } - const gitBranch = versions.find((d) => d.startsWith('@')); - if (gitBranch) { - return gitBranch; - } - - const nodeVersions = versions.map(toSemVer); - // first try to find a good intersection - try { - return toPipVersion(intersect(...nodeVersions).toString()); - } catch (e) { - // map to base version, sort descending take first - const max = toPipVersion(nodeVersions.map(semver.coerce).sort(semver.rcompare)[0]) || versions[versions.length - 1]; - console.warn(`cannot find common intersecting version for ${name} = ${versions.join(', ')}, taking max "${max}" for now`); - return max.toString(); - } -}; - -/** - * Extract tag names starting with `v` (e.g., `v2.0.1`) from the given git log. - * The git log must have the structure: - * ``` - * refs/tags/ - * ``` - * The output can be generated using the command `git ls-remote --tags ` - * - * @param {string} gitLog Console output with list of git hashes and git tags - * @returns {string[]} Returns an array with tag names starting with `v` - */ -module.exports.extractVersionsFromGitLog = (gitLog) => { - const regex = /refs\/tags\/([v]{1}.*[^}])\n/gm; // capture all tag names starting with `v` - let m; - const versionTags = []; - - while ((m = regex.exec(gitLog)) !== null) { - // This is necessary to avoid infinite loops with zero-width matches - if (m.index === regex.lastIndex) { - regex.lastIndex++; - } - versionTags.push(m[1]); - } - - return versionTags; -}; - -/** - * Find the highest possible target version from a list of versions. - * The target version can be a range of versions and supports the npm semver syntax. - * - * @param {string[]} sourceVersions List of source versions (e.g., [`2.0.0`, `v2.1.0`, ...]) - * @param {string} targetVersion version or version range supporting the npm semver syntax - * @returns {string|undefined} Returns the highest possible version from the list of source versions - */ -module.exports.findHighestVersion = (sourceVersions, targetVersion) => { - const semver = require('semver'); - - const regex = /^([\^~])/gi; // test if target version starts with ^ or ~ - // shortcut if exact target version is required - if (regex.test(targetVersion) === false) { - return sourceVersions.find((v) => v === targetVersion); - } - - const versions = sourceVersions - .map((version) => { - return { - version, - satisfied: semver.satisfies(version, targetVersion) - }; - }) - .filter((v) => v.satisfied) - .sort((a, b) => b.version.localeCompare(a.version)); // sort version descending - - return (versions[0]) ? versions[0].version : undefined; // first element = highest version -}; - -/** - * Checks if a given name is an advanced version tag supporting npm's version ranges. - * A version tag must start with `^v`, `~v`. - * - * @see https://docs.npmjs.com/misc/semver#advanced-range-syntax - * - * @param {string} name name to check - * @returns {boolean} Returns `true` if name is a version tag. Otherwise returns `false`. - */ -module.exports.isAdvancedVersionTag = (name) => { - return /^[\^~]v/gi.test(name); -}; - -/** - * Checks if a given name is a version tag that starts with `v`. - * - * @param {string} name name to check - * @returns {boolean} Returns `true` if name is a version tag. Otherwise returns `false`. - */ -module.exports.isExactVersionTag = (name) => { - return /^v\d+.*/gi.test(name); -}; - -/** - * Checks if a given name is a git commit. - * - * @param {string} name name to check - * @returns {boolean} Returns `true` if name is a git commit. Otherwise returns `false`. - */ -module.exports.isGitCommit = (name) => { - return /^[0-9a-f]+$/gi.test(name); -}; -