liferay-theme-tasks
Version:
A set of tasks for building and deploying Liferay Portal themes.
386 lines (316 loc) • 9.04 kB
JavaScript
/**
* SPDX-FileCopyrightText: © 2017 Liferay, Inc. <https://liferay.com>
* SPDX-License-Identifier: MIT
*/
;
const chalk = require('chalk');
const fs = require('fs-extra');
const watch = require('gulp-watch');
const http = require('http');
const httpProxy = require('http-proxy');
const passes = require('http-proxy/lib/http-proxy/passes/web-outgoing');
const opn = require('opn');
const path = require('path');
const portfinder = require('portfinder');
const tinylr = require('tiny-lr');
const url = require('url');
const util = require('util');
const project = require('../../lib/project');
const themeUtil = require('../../lib/util');
const PASSES = Object.values(passes);
const accessAsync = util.promisify(fs.access);
const DEPLOYMENT_STRATEGIES = themeUtil.DEPLOYMENT_STRATEGIES;
const EXPLODED_BUILD_DIR_NAME = '.web_bundle_build';
const MIME_TYPES = {
'.css': 'text/css',
'.ico': 'image/x-icon',
'.js': 'text/javascript',
'.map': 'application/json',
'.svg': 'image/svg+xml',
};
/**
* Splits a path into an array of path components.
*/
function getPathComponents(pathString) {
return pathString.split(path.sep);
}
/**
* Give a path to a resource such as "src/css/partials/_header.scss",
* returns the the name of the child directory under "src/" containing
* the resource (eg. "css").
*/
function getResourceDir(pathString, pathSrc) {
const relativePath = path.relative(pathSrc.asNative, pathString);
return getPathComponents(relativePath)[0];
}
/**
* Returns a Promise that resolves to `true` if `file` exists and `false`
* otherwise.
*/
function isReadable(file) {
return accessAsync(file, fs.constants.R_OK).then(
() => true,
() => false
);
}
module.exports = function () {
const {gulp, options, store} = project;
const {runSequence} = gulp;
const {argv, distName, pathBuild, pathSrc, resourcePrefix} = options;
const {deploymentStrategy, dockerContainerName} = store;
// Calculate some values
const proxyUrl = argv.url || store.url;
const pluginName = store.pluginName || '';
const explodedBuildDir = path.join(project.dir, EXPLODED_BUILD_DIR_NAME);
const dockerThemePath = path.posix.join('/tmp', pluginName);
const dockerBundleDirPath = path.posix.join(
dockerThemePath,
EXPLODED_BUILD_DIR_NAME
);
/**
* Start watching project folder
*/
gulp.task('watch', () => {
project.watching = true;
// Get tasks array
const taskArray = getCleanTaskArray(deploymentStrategy);
// Push final task that deploys the theme and starts live reloads
taskArray.push((error) => {
if (error) {
throw error;
}
// eslint-disable-next-line promise/catch-or-return
Promise.all([
portfinder.getPortPromise({port: 9080}),
portfinder.getPortPromise({port: 35729}),
]).then(([httpPort, tinylrPort]) => {
store.webBundleDir = 'watching';
startWatch(httpPort, tinylrPort, proxyUrl);
});
});
// Run tasks in sequence
runSequence(...taskArray);
});
/**
* Clean the exploded build dir
*/
gulp.task('watch:clean', (callback) => {
fs.removeSync(explodedBuildDir);
callback();
});
/**
* Clean the remote exploded build dir in docker
*/
gulp.task('watch:docker:clean', (callback) => {
themeUtil.dockerExec(
dockerContainerName,
'rm -rf ' + dockerBundleDirPath
);
callback();
});
/**
* Copy the exploded build dir to docker
*/
gulp.task('watch:docker:copy', (callback) => {
themeUtil.dockerExec(
dockerContainerName,
'mkdir -p ' + dockerBundleDirPath
);
themeUtil.dockerCopy(
dockerContainerName,
explodedBuildDir,
dockerBundleDirPath,
callback
);
});
/**
* Copy output files to exploded build dir
*/
gulp.task('watch:setup', () => {
return gulp
.src(pathBuild.join('**', '*').asPosix)
.pipe(gulp.dest(explodedBuildDir));
});
/**
* Cleanup watch machinery
*/
gulp.task('watch:teardown', (callback) => {
store.webBundleDir = undefined;
const taskArray = getTeardownTaskArray();
taskArray.push(callback);
runSequence(...taskArray);
});
let livereload;
gulp.task('watch:reload', (callback) => {
const {changedFile} = store;
const srcPath = path.relative(project.dir, changedFile.path);
const dstPath = srcPath.replace(/^src\//, '');
const urlPath = `${resourcePrefix}/${distName}/${dstPath}`;
livereload.changed({
body: {
files: [urlPath],
},
});
callback();
});
/**
* Start live reload server and watch for changes in project files.
* @param {int} httpPort The port for the http server
* @param {int} tinylrPort The port for the livereload server
* @param {string} proxyUrl The proxy target URL
*/
function startWatch(httpPort, tinylrPort, proxyUrl) {
clearChangedFile();
const themePattern = new RegExp(
`(?!.*.(ftl|tpl|vm))(${resourcePrefix}/${distName}/)(.*)`
);
const livereloadTag = `<script src="http://localhost:${tinylrPort}/livereload.js"></script>`;
livereload = tinylr();
livereload.server.on('error', (error) => {
// eslint-disable-next-line no-console
console.error(error);
});
livereload.listen(tinylrPort);
const proxy = httpProxy.createServer();
proxy.on('proxyReq', (proxyReq, _req, _res, _options) => {
// Disable compression because it complicates the task of appending
// our livereload tag.
proxyReq.setHeader('Accept-Encoding', 'identity');
});
proxy.on('proxyRes', (proxyRes, req, res) => {
// Make sure that "web passes" (eg. header setting and such) still
// happen even though we are in "selfHandleResponse" mode.
// See: https://github.com/nodejitsu/node-http-proxy/issues/1263
for (let i = 0; i < PASSES.length; i++) {
if (PASSES[i](req, res, proxyRes, project.options)) {
break;
}
}
proxyRes.on('data', (data) => {
res.write(data);
});
proxyRes.on('end', () => {
const appendLivereloadTag =
req.headers.accept &&
req.headers.accept.includes('text/html') &&
(res.getHeader('Content-Type') || '').indexOf(
'text/html'
) === 0;
if (appendLivereloadTag) {
res.end(livereloadTag);
}
else {
res.end();
}
});
});
proxy.on('error', (error) => {
// eslint-disable-next-line no-console
console.error(error);
});
http.createServer((req, res) => {
const dispatchToProxy = () =>
proxy.web(req, res, {
selfHandleResponse: true,
target: proxyUrl,
});
const requestUrl = url.parse(req.url);
const match = themePattern.exec(requestUrl.pathname);
if (match) {
const filepath = path.resolve('build', match[3]);
const ext = path.extname(filepath);
// eslint-disable-next-line promise/catch-or-return
isReadable(filepath).then((exists) => {
if (exists) {
if (MIME_TYPES[ext]) {
res.setHeader('Content-Type', MIME_TYPES[ext]);
}
fs.createReadStream(filepath)
.on('error', (error) => {
// eslint-disable-next-line no-console
console.error(error);
})
.pipe(res);
}
else {
dispatchToProxy();
}
});
}
else {
dispatchToProxy();
}
}).listen(httpPort, () => {
const url = `http://localhost:${httpPort}/`;
const messages = [
`Watch mode is now active at: ${url}`,
`Proxying: ${proxyUrl}`,
];
const width = messages.reduce((max, line) => {
return Math.max(line.length, max);
}, 0);
const ruler = '-'.repeat(width);
// eslint-disable-next-line no-console
console.log(
'\n' + chalk.yellow([ruler, ...messages, ruler].join('\n'))
);
opn(url);
});
watch(path.join(pathSrc.asPosix, '**', '*'), (vinyl) => {
store.changedFile = vinyl;
const resourceDir = getResourceDir(vinyl.path, pathSrc);
const taskArray = getBuildTaskArray(resourceDir);
runSequence(...taskArray);
});
}
function clearChangedFile() {
store.changedFile = undefined;
}
function getTeardownTaskArray() {
const taskArray = ['watch:clean'];
if (deploymentStrategy === DEPLOYMENT_STRATEGIES.DOCKER_CONTAINER) {
taskArray.push('watch:docker:clean');
}
return taskArray;
}
function getBuildTaskArray(resourceDir) {
let taskArray;
if (resourceDir === 'css') {
taskArray = [
'build:clean',
'build:base',
'build:src',
'build:themelet-src',
'build:themelet-css-inject',
'build:rename-css-dir',
'build:compile-css',
'build:move-compiled-css',
'build:remove-old-css-dir',
'watch:reload',
];
}
else if (resourceDir === 'js') {
taskArray = ['build:src', 'watch:reload'];
}
else {
taskArray = ['deploy', 'watch:reload'];
}
return taskArray;
}
function getCleanTaskArray(deploymentStrategy) {
switch (deploymentStrategy) {
case DEPLOYMENT_STRATEGIES.LOCAL_APP_SERVER:
return ['deploy', 'watch:clean', 'watch:setup'];
case DEPLOYMENT_STRATEGIES.DOCKER_CONTAINER:
return [
'deploy',
'watch:clean',
'watch:docker:clean',
'watch:setup',
'watch:docker:copy',
];
default:
return [];
}
}
};