UNPKG

liferay-theme-tasks

Version:

A set of tasks for building and deploying Liferay Portal themes.

417 lines (334 loc) 8.83 kB
/** * SPDX-FileCopyrightText: © 2017 Liferay, Inc. <https://liferay.com> * SPDX-License-Identifier: MIT */ const async = require('async'); const spawn = require('cross-spawn'); const fs = require('fs'); const globby = require('globby'); const _ = require('lodash'); const npmKeyword = require('npm-keyword'); const os = require('os'); const packageJson = require('package-json'); const path = require('path'); const {URL} = require('url'); const project = require('../../lib/project'); function getLiferayThemeModule(name, callback) { getPackageJSON( { name, }, (error, pkg) => { if (pkg && !isLiferayTheme(pkg)) { pkg = null; error = new Error( 'Package is not a Liferay theme or themelet module' ); } callback(error, pkg); } ); } /** * Wrapper for spawn.sync that fails on any error. */ function run(command, args) { const getDescription = () => `${command} ${args.join(' ')}`; let error; const results = spawn.sync(command, args); if (results.error) { error = new Error( `Command \`${getDescription()}\` encountered an error: ${ results.error }` ); } else if (results.signal) { error = new Error( `Command \`${getDescription()}\` exited due to signal: ${ results.signal }` ); } else if (results.status) { error = new Error( `Command \`${getDescription()}\` exited with status: ${ results.status }` ); } if (error) { if (results.stdout) { // eslint-disable-next-line no-console console.log(results.stdout.toString()); } if (results.stderr) { // eslint-disable-next-line no-console console.log(results.stderr.toString()); } throw error; } } /** * Given a package URL, attempts to download it, extract the package.json, and * validate it. */ function getLiferayThemeModuleFromURL(url) { new URL(url); let config; // Install the package in a temporary directory in order to get // the package.json. withScratchDirectory(() => { run('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). run('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; } } function getLiferayThemeModules(config, callback) { if (_.isUndefined(callback)) { callback = config; config = {}; } const globalModules = _.isUndefined(config.globalModules) ? true : config.globalModules; config.keyword = config.keyword || 'liferay-theme'; const searchFn = globalModules ? searchGlobalModules : searchNpm; searchFn.call(this, config, (moduleResults) => { reportDiscardedModules( moduleResults, LiferayThemeModuleStatus.NO_PACKAGE_JSON, 'with no package.json' ); reportDiscardedModules( moduleResults, LiferayThemeModuleStatus.NO_LIFERAY_THEME, 'with no liferayTheme section in package.json' ); const themeConfig = project.themeConfig.config; reportDiscardedModules( moduleResults, LiferayThemeModuleStatus.TARGET_VERSION_DOES_NOT_MATCH, `not targeting ${themeConfig.version} version` ); reportDiscardedModules( moduleResults, LiferayThemeModuleStatus.THEMELET_FLAG_DOES_NOT_MATCH, 'with mismatching themelet flag' ); callback(moduleResults[LiferayThemeModuleStatus.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(callback) { const template = path.join(os.tmpdir(), 'theme-finder-'); const directory = fs.mkdtempSync(template); const cwd = process.cwd(); try { process.chdir(directory); callback(); } 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 console.log( 'Warning: found', Object.keys(moduleResults[outcome]).length, 'packages (matching criteria)', message ); } } function findThemeModulesIn(paths) { let modules = []; _.forEach(paths, (rootPath) => { if (!rootPath) { return; } modules = globby .sync(['*-theme', '*-themelet'], { cwd: rootPath, }) .map((match) => path.join(rootPath, match)) .concat(modules); }); return modules; } function getNpmPaths() { let paths = []; const win32 = process.platform === 'win32'; _.forEach( path.join(project.dir, '..').split(path.sep), (part, index, parts) => { let lookup = path.join( ...parts.slice(0, index + 1).concat(['node_modules']) ); if (!win32) { lookup = '/' + lookup; } paths.push(lookup); } ); if (process.env.NODE_PATH) { paths = _.compact(process.env.NODE_PATH.split(path.delimiter)).concat( paths ); } else { const results = spawn.sync('npm', ['root', '-g']); if (!results.error && results.stdout) { const npmRoot = results.stdout.toString(); if (npmRoot) { paths.push(_.trim(npmRoot)); } } if (win32) { paths.push(path.join(process.env.APPDATA, 'npm', 'node_modules')); } else { paths.push('/usr/lib/node_modules'); paths.push('/usr/local/lib/node_modules'); } } return paths.reverse(); } function getPackageJSON(theme, callback) { packageJson(theme.name, {fullMetadata: true}) .then((pkg) => callback(null, pkg)) .catch(callback); } const LiferayThemeModuleStatus = { NO_LIFERAY_THEME: 'NO_LIFERAY_THEME', NO_PACKAGE_JSON: 'NO_PACKAGE_JSON', OK: 'OK', TARGET_VERSION_DOES_NOT_MATCH: 'TARGET_VERSION_DOES_NOT_MATCH', THEMELET_FLAG_DOES_NOT_MATCH: 'THEMELET_FLAG_DOES_NOT_MATCH', }; function getLiferayThemeModuleStatus(pkg, themelet) { if (pkg) { const liferayTheme = pkg.liferayTheme; if (!liferayTheme) { return LiferayThemeModuleStatus.NO_LIFERAY_THEME; } const liferayThemeVersion = liferayTheme.version; const themeConfig = project.themeConfig.config; if ( _.isArray(liferayThemeVersion) && !_.includes(liferayThemeVersion, themeConfig.version) ) { return LiferayThemeModuleStatus.TARGET_VERSION_DOES_NOT_MATCH; } if ( !_.isArray(liferayThemeVersion) && liferayThemeVersion !== '*' && liferayThemeVersion !== themeConfig.version ) { return LiferayThemeModuleStatus.TARGET_VERSION_DOES_NOT_MATCH; } const liferayThemelet = liferayTheme.themelet || false; if (themelet !== liferayThemelet) { return LiferayThemeModuleStatus.THEMELET_FLAG_DOES_NOT_MATCH; } return LiferayThemeModuleStatus.OK; } return LiferayThemeModuleStatus.NO_PACKAGE_JSON; } function matchesSearchTerms(pkg, searchTerms) { const description = pkg.description; return ( pkg.name.indexOf(searchTerms) > -1 || (description && description.indexOf(searchTerms) > -1) ); } function reduceModuleResults(modules, config) { const searchTerms = config.searchTerms; const themelet = config.themelet; return _.reduce( modules, (result, item) => { if (searchTerms && !matchesSearchTerms(item, searchTerms)) { return result; } const outcome = getLiferayThemeModuleStatus(item, themelet); result[outcome] = result[outcome] || {}; result[outcome][item.name] = item; return result; }, {} ); } function searchGlobalModules(config, callback) { let modules = findThemeModulesIn(getNpmPaths()); modules = _.reduce( modules, (result, item) => { try { // eslint-disable-next-line @liferay/no-dynamic-require const json = require(path.join(item, 'package.json')); json.__realPath__ = item; result.push(json); } catch (error) { // Swallow. } return result; }, [] ); const moduleResults = reduceModuleResults(modules, config); callback(moduleResults); } function searchNpm(config, callback) { // eslint-disable-next-line promise/catch-or-return npmKeyword(config.keyword).then((packages) => { async.map(packages, getPackageJSON, (error, results) => { if (error) { callback(error); return; } const moduleResults = reduceModuleResults(results, config); callback(moduleResults); }); }); }