diff --git a/.travis.yml b/.travis.yml index 70c7614d6..2ae357bc0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,22 +2,22 @@ language: node_js node_js: - node + - 8 - 6 before_install: - phantomjs --version before_script: - - npm install patternengine-node-mustache - - npm install patternengine-node-underscore - - npm install patternengine-node-handlebars + - npm install @pattern-lab/patternengine-node-mustache + - npm install @pattern-lab/patternengine-node-underscore + - npm install @pattern-lab/patternengine-node-handlebars - npm install patternengine-node-twig branches: only: - master - dev - - dev-3.0 notifications: webhooks: diff --git a/core/lib/pattern_engines.js b/core/lib/pattern_engines.js index 7582b4ea7..8b825ed29 100644 --- a/core/lib/pattern_engines.js +++ b/core/lib/pattern_engines.js @@ -1,47 +1,103 @@ // special shoutout to Geoffrey Pursell for single-handedly making Pattern Lab Node Pattern Engines possible! 'use strict'; +const {existsSync, lstatSync, readdirSync} = require('fs'); const path = require('path'); -const diveSync = require('diveSync'); const chalk = require('chalk'); const engineMatcher = /^patternengine-node-(.*)$/; -const enginesDirectories = [ - { - displayName: 'the core', - path: path.resolve(__dirname, '..', '..', 'node_modules') - }, - { - displayName: 'the edition or test directory', - path: path.join(process.cwd(), 'node_modules') - } -]; +const scopeMatch = /^@(.*)$/; +const isDir = fPath => lstatSync(fPath).isDirectory(); + +const enginesDirectories = [{ + displayName: 'the core', + path: path.resolve(__dirname, '..', '..', 'node_modules') +}, { + displayName: 'the edition or test directory', + path: path.join(process.cwd(), 'node_modules') +}]; // given a path: return the engine name if the path points to a valid engine // module directory, or false if it doesn't function isEngineModule(filePath) { const baseName = path.basename(filePath); const engineMatch = baseName.match(engineMatcher); - + if (engineMatch) { return engineMatch[1]; } return false; } -function findEngineModulesInDirectory(dir) { - const foundEngines = []; +/** + * @name isScopedPackage + * @desc Checks whether a path in modules belongs to a scoped package + * @param {string} filePath - The pathname to check + * @return {Boolean} - Returns a bool when found, false othersie + */ +function isScopedPackage(filePath) { + const baseName = path.basename(filePath); + return scopeMatch.test(baseName); +} - diveSync(dir, { - recursive: false, - directories: true - }, function (err, filePath) { - if (err) { throw err; } - const foundEngineName = isEngineModule(filePath); - if (foundEngineName) { - foundEngines.push({ - name: foundEngineName, - modulePath: filePath - }); - } - }); +/** + * @name resolveEngines + * @desc Creates an array of all available patternlab engines + * @param {string} dir - The directory to search for engines and scoped engines) + * @return {Array} An array of engine objects + */ +function resolveEngines(dir) { + + // Guard against non-existent directories. + if (!existsSync(dir)) { + return []; // Silence is golden … + } + + /** + * @name walk + * @desc Traverse the given path and gather possible engines + * @param {string} fPath - The file path to traverse + * @param {Array} engines - An array of engines from the inner most matches + * @return {Array} - The final array of engines + */ + const walk = (fPath, engines) => { + + /** + * @name dirList + * @desc A list of all directories in the given path + * @type {Array} + */ + const dirList = readdirSync(fPath).filter(p => isDir(path.join(fPath, p))); + + /** + * @name e + * @desc For the current dir get all engines + * @type {Array} + */ + const e = engines.concat(dirList + .filter(isEngineModule) + .map(engine => { + return { + name: isEngineModule(engine), + modulePath: path.join(fPath, engine) + } + }) + ); + + /** + * 1. Flatten all engines from inner recursions and current dir + * 2. Filter the dirList for scoped packages + * 3. Map over every scoped package and recurse into it to find scoped engines + */ + return [].concat( + ...e, + ...dirList + .filter(isScopedPackage) // 2 + .map(scope => walk(path.join(fPath, scope), e)) // 3 + ); + }; + + return walk(dir, []); +} +function findEngineModulesInDirectory(dir) { + const foundEngines = resolveEngines(dir) return foundEngines; } @@ -61,10 +117,10 @@ function findEngineModulesInDirectory(dir) { // methods and properites below should therefore be on its prototype. const PatternEngines = Object.create({ - + loadAllEngines: function (patternLabConfig) { var self = this; - + // Try to load engines! We scan for engines at each path specified above. This // function is kind of a big deal. enginesDirectories.forEach(function (engineDirectory) { @@ -72,7 +128,7 @@ const PatternEngines = Object.create({ if (patternLabConfig.debug) { console.log(chalk.bold(`Loading engines from ${engineDirectory.displayName}...\n`)); } - + // find all engine-named things in this directory and try to load them, // unless it's already been loaded. enginesInThisDir.forEach(function (engineDiscovery) { @@ -81,7 +137,7 @@ const PatternEngines = Object.create({ if (patternLabConfig.debug) { chalk.green(successMessage); } - + try { // Give it a try! load 'er up. But not if we already have, // of course. Also pass the pattern lab config object into @@ -108,7 +164,7 @@ const PatternEngines = Object.create({ }); console.log(''); }); - + // Complain if for some reason we haven't loaded any engines. if (Object.keys(self).length === 0) { throw new Error('No engines loaded! Something is seriously wrong.'); @@ -117,7 +173,7 @@ const PatternEngines = Object.create({ console.log(chalk.bold('Done loading engines.\n')); } }, - + getEngineNameForPattern: function (pattern) { // avoid circular dependency by putting this in here. TODO: is this slow? const of = require('./object_factory'); @@ -126,7 +182,7 @@ const PatternEngines = Object.create({ const engineNames = Object.keys(this); for (let i = 0; i < engineNames.length; i++) { const engine = this[engineNames[i]]; - + if (Array.isArray(engine.engineFileExtension)) { if (engine.engineFileExtension.includes(pattern.fileExtension)) { return engine.engineName; @@ -139,12 +195,12 @@ const PatternEngines = Object.create({ } } } - + // otherwise, assume it's a plain mustache template string and act // accordingly return 'mustache'; }, - + getEngineForPattern: function (pattern) { if (pattern.isPseudoPattern) { return this.getEngineForPattern(pattern.basePattern); @@ -153,7 +209,7 @@ const PatternEngines = Object.create({ return this[engineName]; } }, - + // combine all found engines into a single array of supported extensions getSupportedFileExtensions: function () { const engineNames = Object.keys(PatternEngines); @@ -162,19 +218,19 @@ const PatternEngines = Object.create({ }); return [].concat.apply([], allEnginesExtensions); }, - + isFileExtensionSupported: function (fileExtension) { const supportedExtensions = PatternEngines.getSupportedFileExtensions(); return (supportedExtensions.lastIndexOf(fileExtension) !== -1); }, - + // given a filename, return a boolean: whether or not the filename indicates // that the file is pseudopattern JSON isPseudoPatternJSON: function (filename) { const extension = path.extname(filename); return (extension === '.json' && filename.indexOf('~') > -1); }, - + // takes a filename string, not a full path; a basename (plus extension) // ignore _underscored patterns, dotfiles, and anything not recognized by a // loaded pattern engine. Pseudo-pattern .json files ARE considered to be @@ -183,14 +239,14 @@ const PatternEngines = Object.create({ // skip hidden patterns/files without a second thought const extension = path.extname(filename); if (filename.charAt(0) === '.' || - (extension === '.json' && !PatternEngines.isPseudoPatternJSON(filename))) { + (extension === '.json' && !PatternEngines.isPseudoPatternJSON(filename))) { return false; } - + // not a hidden pattern, let's dig deeper const supportedPatternFileExtensions = PatternEngines.getSupportedFileExtensions(); return (supportedPatternFileExtensions.lastIndexOf(extension) !== -1 || - PatternEngines.isPseudoPatternJSON(filename)); + PatternEngines.isPseudoPatternJSON(filename)); } });