UNPKG

aem-clientlib-webpack-plugin

Version:

A webpack plugin to generate clientlib structure based a clientlib.config.js file

331 lines (297 loc) 12.5 kB
/* eslint no-console: 0 */ /* imports */ import _ from 'lodash'; import 'babel-core/register'; import 'babel-polyfill'; import Chalk from 'chalk'; import FSE from 'fs-extra'; import FS from 'fs'; import Glob from 'glob'; import MM from 'micromatch'; import Path from 'path'; import Util from 'util'; import Watch from 'node-watch'; /* relative imports */ import ClientlibTemplateEngine from './clientlib-template-engine'; import LoggerFactory from './logger-factory'; import Syncer from './syncer'; import Builder from './builder'; /* imports end */ export default class AEMClientLibGeneratorPlugin { constructor(_options) { // Configure your plugin with options... this.options = _options; this.exitOnErrors = typeof (this.options.exitOnErrors) !== 'undefined' ? this.options.exitOnErrors : true; this.options.cleanBuildsOnce = typeof (this.options.cleanBuildsOnce) !== 'undefined' ? this.options.cleanBuildsOnce : true; this.options.cleanBuilds = typeof (this.options.cleanBuilds) !== 'undefined' ? this.options.cleanBuilds : false; this.setImmediatePromise = Util.promisify(setImmediate); this.options.logLevel = _options.logLevel ? _options.logLevel : 'info'; this.options.onBuild = _options.onBuild ? _options.onBuild : _.noop; this.options.sync = typeof _options.sync === 'function' ? _options.sync() : _options.sync; this.logger = LoggerFactory.getInstance(this.options.logLevel); this.state = { cleaned: false, watching: false, setupDone: false }; // TODO: allow use of this.options.templatePath this.templateEngine = new ClientlibTemplateEngine(false, this.options.templateSettings); } setUp() { if (this.state.setupDone) { return; } this.state.watching = true; this.state.setupDone = true; if (this.options.sync) { this.syncer = new Syncer(this.options.sync.targets, this.options.sync.pushInterval || 1000, this.options.sync.onPushEnd || _.noop); } if (this.isWatchEnabled()) { _.forEach(this.options.watchPaths, (watch) => { this.createWatcher(watch.path, watch.match, watch.syncOnly); }); } this.builder = new Builder(this.options); } apply(compiler) { compiler.plugin('emit', (compilation, callback) => { this.setUp(); this.copiedFiles = []; this.logger.info('compiler is emitting files...'); if (this.exitOnErrors && compilation.errors.length) { return; } this.builder.build(compilation, compiler); setTimeout(() => { this.generateClientLibs(callback); }, 1000); }); } createWatcher(path, pattern, isSyncOnly) { const nw = Watch(path, { recursive: true, persistent: true, }, ((evt, name) => { if (this.syncer && evt === 'update') { if (typeof (pattern) === 'undefined' || MM([name], pattern).length > 0) { if (isSyncOnly) { this.syncer.enqueue(name); } else { this.generateClientLibs(); } } } })); process.on('SIGINT', nw.close); return nw; } generateClientLibs(callback) { const promise = this.options.before ? this.options.before : Util.promisify(setImmediate); promise(this).then(() => { this.logger.info('generating clientlib...'); if (this.options.cleanBuildsOnce && !this.state.cleaned) { this.state.cleaned = true; return this.cleanClientLibs().catch(this.handleError); } else if (this.options.cleanBuilds) { return this.cleanClientLibs().catch(this.handleError); } return this.setImmediatePromise(); }).then(() => this.ensureClientLibFoldersExists()) .then(() => this.copyFilesToLibs()) .then(() => this.createClientLibConfig()) .then(() => { this.logger.info('clientlib generated'); if (this.syncer && this.copiedFiles.length > 0) { Syncer.uploadImmediately(this.copiedFiles, this.options.sync.targets, (err, host, syncer) => { console.log('new pending callback called'); if (callback) { callback(err, host, syncer); } if (this.options.onBuild) { this.options.onBuild(this.options); } if (typeof this.options.sync.onPushEnd === 'function') { this.options.sync.onPushEnd(); } }, this.options.logLevel); this.copiedFiles = []; } else if (callback) { callback(); this.options.onBuild(this.options); } if (!this.isWatchEnabled()) { process.exit(0); } return true; }) .catch(this.handleError); } cleanClientLibs(libs = this.options.libs, baseDir = this.options.context) { return Promise.all(_.map(libs, (lib) => { const dir = Path.resolve(baseDir, lib.destination, lib.name); this.logger.verbose('cleaning directory:', Chalk.cyan(Path.relative(baseDir, dir))); const promise = this.options.beforeEach ? this.options.beforeEach : Util.promisify(setImmediate); return promise(lib).then(() => FSE.emptyDir(dir).catch(this.handleError)).catch(this.handleError); })).catch(this.handleError); } handleError(err) { console.error('aem-clientlib-webpack-plugin', 'handleError', err); console.trace('aem-clientlib-webpack-plugin', 'trace', err); if (this && this.isWatchEnabled && !this.isWatchEnabled()) { process.exit(1); } } ensureClientLibFoldersExists(libs = this.options.libs, baseDir = this.options.context) { return Promise.all(_.map(libs, (lib) => { const dir = Path.resolve(baseDir, lib.destination, lib.name); this.logger.verbose('ensuring directory exists:', Chalk.cyan(Path.relative(baseDir, dir))); return FSE.ensureDir(dir).catch(this.handleError); })); } createClientLibConfig(libs = this.options.libs, baseDir = this.options.context) { const templateFn = this.templateEngine.compile(); let syncEntireLib = false; if (typeof this.isFirstRun === 'undefined') { this.isFirstRun = true; if (this.options.sync && this.options.sync.pushEntireClientlibOnFirstRun) { syncEntireLib = true; } } else { this.isFirstRun = false; } return Promise.all(_.map(libs, (lib) => { const xmlStr = templateFn({ categoryName: typeof (lib.categoryName) === 'string' ? lib.categoryName : lib.name, dependencies: lib.dependencies ? lib.dependencies : '', }); const file = Path.resolve(baseDir, lib.destination, lib.name, '.content.xml'); const promise = this.options.beforeEach ? this.options.beforeEach : Util.promisify(setImmediate); let uploadPromiseFn = Promise.resolve.bind(Promise); const matchResult = this.fileMatchesContent(file, xmlStr); if (this.syncer && (syncEntireLib || !matchResult)) { uploadPromiseFn = Syncer.uploadImmediately; this.copiedFiles = []; this.logger.verbose('creating config:', Chalk.cyan(Path.relative(baseDir, file))); } else if (this.syncer && matchResult) { this.logger.verbose('no change to config:', Chalk.cyan(Path.relative(baseDir, file))); return uploadPromiseFn(); } return promise(lib).then(() => FSE.outputFile(file, xmlStr).then(() => uploadPromiseFn(file, this.options.sync.targets, this.options.sync.onPushEnd, this.options.logLevel))).catch(this.handleError); })).catch(this.handleError); } copyFilesToLibs(libs = this.options.libs, baseDir = this.options.context) { return Promise.all(_.map(libs, lib => this.copyAssetFilesToLib(lib, baseDir))).catch(this.handleError); } copyAssetFilesToLib(lib, baseDir = this.options.context) { const clientLibPath = Path.resolve(baseDir, lib.destination, lib.name); const promises = []; Object.keys(lib.assets).forEach((kind) => { const promise = this.options.beforeEach ? this.options.beforeEach : Util.promisify(setImmediate); promise(lib).then(() => { const assets = this.buildAssetPaths(lib.assets[kind], kind, baseDir); assets.forEach((asset, i) => { const srcFile = Path.resolve(baseDir, assets[i].src); const destFolder = Path.resolve(clientLibPath, assets[i].dest); const destFile = Path.resolve(clientLibPath, assets[i].dest, Path.basename(srcFile)); asset.destFile = destFile; FSE.ensureDirSync(destFolder); const compareResult = this.compareFileFunc(srcFile, destFile); if (compareResult === true || (compareResult === 'dir' && !FS.existsSync(destFile))) { promises.push(FSE.copy(srcFile, destFile, { preserveTimestamps: true }).catch(this.handleError)); this.copiedFiles.push(destFile); } }); if (['js', 'css'].indexOf(kind) > -1) { promises.push(this.createAssetTextFile(assets, kind, clientLibPath, (typeof (lib.baseTxtFile) === 'object') ? lib.baseTxtFile[kind] : null).catch(this.handleError)); } return true; }); }); return Promise.all(promises).catch(this.handleError); } buildAssetPaths(sourceFiles, kind, baseDir) { let paths = []; sourceFiles.forEach((sourceFile) => { const flattenedPaths = this.flattenAssetPathPatterns(sourceFile, kind, baseDir); paths = paths.concat(flattenedPaths); }); return paths; } flattenAssetPathPatterns(pattern, kind, baseDir) { return _.map(Glob.sync(pattern.src ? pattern.src : pattern, { cwd: baseDir }), src => ({ src, dest: this.getDestPath(pattern, kind, src), excludeFromTxt: (typeof pattern === 'object' && typeof (pattern.excludeFromTxt) === 'boolean') ? pattern.excludeFromTxt : false, })); } getDestPath(pattern, kind, src) { let destFolder = './'; if (typeof pattern === 'object' && pattern.dest) { destFolder = pattern.dest; } return pattern.base ? Path.dirname(Path.join(kind, destFolder, Path.relative(pattern.base, src))) : Path.join(kind, destFolder); } createAssetTextFile(assets, kind, clientlibFolder, baseTxtFile) { const text = [`#base=${kind}`]; assets = _.sortBy(assets, ['destFile']); assets.forEach((asset) => { if (!Path.extname(asset.destFile).endsWith(kind)) { return; } if (asset.excludeFromTxt) { return; } const relativePath = Path.relative(Path.resolve(clientlibFolder, kind), asset.destFile); if (Path.basename(relativePath) === relativePath) { text.push(relativePath); } else if (text.lastIndexOf(`#base=${Path.dirname(relativePath)}`) === -1) { text.push(`#base=${Path.dirname(relativePath)}`); text.push(Path.basename(relativePath)); } else { text.push(Path.basename(relativePath)); } }); const destFile = Path.resolve(clientlibFolder, `${kind}.txt`); let txtContent = text.join('\n'); if (typeof (baseTxtFile) === 'string' && FSE.existsSync(baseTxtFile)) { const txFileContext = FSE.readFileSync(baseTxtFile); txtContent = `${txFileContext}\n${text.join('\n')}`; } this.copiedFiles.push(destFile); return FSE.outputFile(destFile, txtContent).catch(this.handleError); } compareFileFunc(src, dest) { const file1 = FSE.existsSync(src); const file2 = FSE.existsSync(dest); if (file1 === true && file2 === false) { return true; } else if (file1 === false) { return false; } const stats1 = FSE.statSync(src); const stats2 = FSE.statSync(dest); if (stats1.isDirectory()) { return 'dir'; } else if (stats1.mtimeMs !== stats2.mtimeMs) { this.logger.verbose('copying file.', Chalk.cyan(Path.relative(this.options.context, src))); return true; } else if (stats1.size !== stats2.size) { this.logger.verbose('copying file:', Chalk.cyan(Path.relative(this.options.context, src))); return true; } this.logger.verbose('skipping file', Chalk.cyan(Path.relative(this.options.context, src))); return false; } fileMatchesContent(filePath, content) { return FSE.existsSync(filePath) && FSE.readFileSync(filePath).toString() === content; } isWatchEnabled() { return process.argv.indexOf('--watch') !== -1; } } export class SyncerUtil extends Syncer {} export class BuilderUtil extends Builder {}