diff --git a/packages/liferay-theme-tasks/lib/prompts/extend_prompt.js b/packages/liferay-theme-tasks/lib/prompts/extend_prompt.js index 80dc9d9b..bdf597fb 100644 --- a/packages/liferay-theme-tasks/lib/prompts/extend_prompt.js +++ b/packages/liferay-theme-tasks/lib/prompts/extend_prompt.js @@ -12,6 +12,7 @@ const inquirer = require('inquirer'); const GlobalModulePrompt = require('./global_module_prompt'); const lfrThemeConfig = require('../liferay_theme_config'); const NPMModulePrompt = require('./npm_module_prompt'); +const URLPackagePrompt = require('./url_package_prompt'); const promptUtil = require('./prompt_util'); const themeFinder = require('../theme_finder'); @@ -141,12 +142,17 @@ class ExtendPrompt { if (themeSource === 'global') { GlobalModulePrompt.prompt( config, - _.bind(this._afterPromptModule, this) + this._afterPromptModule.bind(this) ); } else if (themeSource === 'npm') { NPMModulePrompt.prompt( config, - _.bind(this._afterPromptModule, this) + this._afterPromptModule.bind(this) + ); + } else if (themeSource === 'url') { + URLPackagePrompt.prompt( + config, + this._afterPromptModule.bind(this) ); } } @@ -217,6 +223,10 @@ class ExtendPrompt { name: 'Search npm registry (published modules)', value: 'npm', }, + { + name: 'Specify a package URL', + value: 'url', + }, ]; if (extendType === 'theme') { diff --git a/packages/liferay-theme-tasks/lib/prompts/url_package_prompt.js b/packages/liferay-theme-tasks/lib/prompts/url_package_prompt.js new file mode 100644 index 00000000..bf706081 --- /dev/null +++ b/packages/liferay-theme-tasks/lib/prompts/url_package_prompt.js @@ -0,0 +1,59 @@ +/** + * © 2017 Liferay, Inc. + * + * SPDX-License-Identifier: MIT + */ + +const inquirer = require('inquirer'); +const {URL} = require('url'); + +const themeFinder = require('../theme_finder'); + +class URLPackagePrompt { + constructor(...args) { + this.init(...args); + } + + init(config, cb) { + this.done = cb; + + inquirer.prompt( + [ + { + message: 'Enter the URL for the package:', + name: 'packageURL', + type: 'input', + validate: this._validatePackageURL, + }, + ], + this._afterPrompt.bind(this) + ); + } + + _validatePackageURL(packageURL, _answers) { + try { + new URL(packageURL); + } catch (err) { + return `"${packageURL}" is not a valid URL`; + } + + return true; + } + + _afterPrompt(answers) { + const config = themeFinder.getLiferayThemeModuleFromURL(answers.packageURL); + answers.module = config.name; + answers.modules = { + [config.name]: Object.assign( + {}, + config, + {realPath: answers.packageURL}, + ), + }; + this.done(answers); + } +} + +URLPackagePrompt.prompt = (config, cb) => new URLPackagePrompt(config, cb); + +module.exports = URLPackagePrompt; diff --git a/packages/liferay-theme-tasks/lib/theme_finder.js b/packages/liferay-theme-tasks/lib/theme_finder.js index 976f0f5d..a4f99ba6 100644 --- a/packages/liferay-theme-tasks/lib/theme_finder.js +++ b/packages/liferay-theme-tasks/lib/theme_finder.js @@ -6,11 +6,15 @@ const _ = require('lodash'); const async = require('async'); +const child_process = require('child_process'); +const fs = require('fs'); const globby = require('globby'); const npmKeyword = require('npm-keyword'); +const os = require('os'); const packageJson = require('package-json'); const path = require('path'); const spawn = require('cross-spawn'); +const {URL} = require('url'); const lfrThemeConfig = require('./liferay_theme_config'); @@ -22,10 +26,7 @@ function getLiferayThemeModule(name, cb) { name, }, (err, pkg) => { - if ( - (pkg && !pkg.liferayTheme) || - (pkg && !_.includes(pkg.keywords, 'liferay-theme')) - ) { + if (!isLiferayTheme(pkg)) { pkg = null; err = new Error( @@ -38,6 +39,51 @@ function getLiferayThemeModule(name, cb) { ); } +/** + * Given a package URL, attempts to download it, extract the package.json, and + * validate it. + */ +function getLiferayThemeModuleFromURL(url, cb) { + try { + // Throw if `url` is invalid. + new URL(url); + + let config; + + // Install the package in a temporary directory in order to get + // the package.json. + withScratchDirectory(() => { + child_process.spawnSync('npm', ['init', '-y']); + + // Ideally, we wouldn't install any dependencies at all, but this is + // the closest we can get (production only, skipping optional + // dependencies). + child_process.spawnSync('npm', [ + 'install', + '--ignore-scripts', + '--no-optional', + '--production', + url, + ]); + + // Just in case package name doesn't match URL basename, read it. + const {dependencies} = JSON.parse(fs.readFileSync('package.json')); + const themeName = Object.keys(dependencies)[0]; + + const json = path.join('node_modules', themeName, 'package.json'); + config = JSON.parse(fs.readFileSync(json)); + }); + + if (!isLiferayTheme(config)) { + throw new Error(`URL ${url} is not a liferay-theme`); + } else { + return config; + } + } catch (err) { + cb(err); + } +} + function getLiferayThemeModules(config, cb) { if (_.isUndefined(cb)) { cb = config; @@ -79,16 +125,41 @@ function getLiferayThemeModules(config, cb) { }); } -module.exports = {getLiferayThemeModule, getLiferayThemeModules}; - -const LiferayThemeModuleStatus = { - NO_PACKAGE_JSON: 'NO_PACKAGE_JSON', - NO_LIFERAY_THEME: 'NO_LIFERAY_THEME', - TARGET_VERSION_DOES_NOT_MATCH: 'TARGET_VERSION_DOES_NOT_MATCH', - THEMELET_FLAG_DOES_NOT_MATCH: 'THEMELET_FLAG_DOES_NOT_MATCH', - OK: 'OK', +module.exports = { + getLiferayThemeModule, + getLiferayThemeModuleFromURL, + getLiferayThemeModules, }; +/** + * Execute `cb()` in the context of a temporary directory. + * + * Note that `cb()` should be entirely synchronous, because clean-up is + * performed as soon as it returns. + */ +function withScratchDirectory(cb) { + const template = path.join(os.tmpdir(), 'theme-finder-'); + const directory = fs.mkdtempSync(template); + + const cwd = process.cwd(); + + try { + process.chdir(directory); + cb(); + } finally { + process.chdir(cwd); + } +} + +function isLiferayTheme(config) { + return ( + config && + config.liferayTheme && + config.keywords && + config.keywords.indexOf('liferay-theme') !== -1 + ); +} + function reportDiscardedModules(moduleResults, outcome, message) { if (moduleResults[outcome]) { // eslint-disable-next-line no-console @@ -172,6 +243,14 @@ function getPackageJSON(theme, cb) { .catch(cb); } +const LiferayThemeModuleStatus = { + NO_PACKAGE_JSON: 'NO_PACKAGE_JSON', + NO_LIFERAY_THEME: 'NO_LIFERAY_THEME', + TARGET_VERSION_DOES_NOT_MATCH: 'TARGET_VERSION_DOES_NOT_MATCH', + THEMELET_FLAG_DOES_NOT_MATCH: 'THEMELET_FLAG_DOES_NOT_MATCH', + OK: 'OK', +}; + function getLiferayThemeModuleStatus(pkg, themelet) { if (pkg) { const liferayTheme = pkg.liferayTheme;