patternlab-node
Version:
Pattern Lab is a collection of tools to help you create atomic design systems. This is the node command line interface (CLI).
665 lines (561 loc) • 26.2 kB
JavaScript
/*
* patternlab-node - v2.12.0 - 2017
*
* Brian Muenzenmeyer, Geoff Pursell, Raphael Okon, tburny and the web community.
* Licensed under the MIT license.
*
* Many thanks to Brad Frost and Dave Olsen for inspiration, encouragement, and advice.
*
*/
;
var diveSync = require('diveSync'),
_ = require('lodash'),
path = require('path'),
chalk = require('chalk'),
cleanHtml = require('js-beautify').html,
inherits = require('util').inherits,
pm = require('./plugin_manager'),
fs = require('fs-extra'),
packageInfo = require('../../package.json'),
dataLoader = require('./data_loader')(),
plutils = require('./utilities'),
jsonCopy = require('./json_copy'),
ui = require('./ui_builder'),
ui_builder = new ui(),
pe = require('./pattern_exporter'),
pattern_exporter = new pe(),
PatternGraph = require('./pattern_graph').PatternGraph,
updateNotifier = require('update-notifier');
//register our log events
plutils.log.on('error', msg => console.log(msg));
plutils.log.on('debug', msg => console.log(msg));
plutils.log.on('warning', msg => console.log(msg));
plutils.log.on('info', msg => console.log(msg));
console.log(
chalk.bold('\n====[ Pattern Lab / Node'),
`- v${packageInfo.version}`,
chalk.bold(']====\n')
);
var patternEngines = require('./pattern_engines');
var EventEmitter = require('events').EventEmitter;
//bootstrap update notifier
updateNotifier({
pkg: packageInfo,
updateCheckInterval: 1000 * 60 * 60 * 24 // notify at most once a day
}).notify();
/**
* Given a path, load info from the folder to compile into a single config object.
* @param dataFilesPath
* @param fsDep
* @returns {{}}
*/
function buildPatternData(dataFilesPath, fsDep) {
return dataLoader.loadDataFromFolder(dataFilesPath, 'listitems', fsDep);
}
// GTP: these two diveSync pattern processors factored out so they can be reused
// from unit tests to reduce code dupe!
function processAllPatternsIterative(pattern_assembler, patterns_dir, patternlab) {
diveSync(
patterns_dir,
function (err, file) {
//log any errors
if (err) {
console.log(err);
return;
}
pattern_assembler.process_pattern_iterative(path.relative(patterns_dir, file), patternlab);
}
);
}
function processAllPatternsRecursive(pattern_assembler, patterns_dir, patternlab) {
diveSync(
patterns_dir,
function (err, file) {
//log any errors
if (err) {
console.log(err);
return;
}
pattern_assembler.process_pattern_recursive(path.relative(patterns_dir, file), patternlab);
}
);
}
function checkConfiguration(patternlab) {
//default the output suffixes if not present
var outputFileSuffixes = {
rendered: '.rendered',
rawTemplate: '',
markupOnly: '.markup-only'
};
if (!patternlab.config.outputFileSuffixes) {
plutils.warning('Configuration Object "outputFileSuffixes" not found, and defaulted to the following:');
console.log(outputFileSuffixes);
plutils.warning('Since Pattern Lab Core 2.3.0 this configuration option is required. Suggest you add it to your patternlab-config.json file.');
console.log();
}
patternlab.config.outputFileSuffixes = _.extend(outputFileSuffixes, patternlab.config.outputFileSuffixes);
}
/**
* Finds and calls the main method of any found plugins.
* @param patternlab - global data store
*/
//todo, move this to plugin_manager
function initializePlugins(patternlab) {
if (!patternlab.config.plugins) { return; }
var plugin_manager = new pm(patternlab.config, path.resolve(__dirname, '../../patternlab-config.json'));
var foundPlugins = plugin_manager.detect_plugins();
if (foundPlugins && foundPlugins.length > 0) {
for (var i = 0; i < foundPlugins.length; i++) {
let pluginKey = foundPlugins[i];
if (patternlab.config.debug) {
console.log('Found plugin: ', pluginKey);
console.log('Attempting to load and initialize plugin.');
}
var plugin = plugin_manager.load_plugin(pluginKey);
plugin(patternlab);
}
}
}
/**
* Installs a given plugin. Assumes it has already been pulled down via npm
* @param pluginName - the name of the plugin
*/
function installPlugin(pluginName) {
//get the config
var configPath = path.resolve(process.cwd(), 'patternlab-config.json');
var config = fs.readJSONSync(path.resolve(configPath), 'utf8');
var plugin_manager = new pm(config, configPath);
plugin_manager.install_plugin(pluginName);
}
function PatternLabEventEmitter() {
EventEmitter.call(this);
}
inherits(PatternLabEventEmitter, EventEmitter);
var patternlab_engine = function (config) {
'use strict';
var pa = require('./pattern_assembler'),
lh = require('./lineage_hunter'),
sm = require('./starterkit_manager'),
Pattern = require('./object_factory').Pattern,
CompileState = require('./object_factory').CompileState,
patternlab = {};
patternlab.engines = patternEngines;
var pattern_assembler = new pa(),
lineage_hunter = new lh();
patternlab.package = fs.readJSONSync(path.resolve(__dirname, '../../package.json'));
patternlab.config = config || fs.readJSONSync(path.resolve(__dirname, '../../patternlab-config.json'));
patternlab.events = new PatternLabEventEmitter();
// Initialized when building
patternlab.graph = null;
checkConfiguration(patternlab);
//todo: determine if this is the best place to wire up plugins
initializePlugins(patternlab);
var paths = patternlab.config.paths;
function getVersion() {
console.log(patternlab.package.version);
}
function getSupportedTemplateExtensions() {
return patternlab.engines.getSupportedFileExtensions();
}
function help() {
console.log('');
console.log('|=======================================|');
plutils.debug(' Pattern Lab Node Help v' + patternlab.package.version);
console.log('|=======================================|');
console.log('');
console.log('Command Line Interface - usually consumed by an edition');
console.log('');
plutils.debug(' patternlab:build');
console.log(' > Compiles the patterns and frontend, outputting to config.paths.public');
console.log('');
plutils.debug(' patternlab:patternsonly');
console.log(' > Compiles the patterns only, outputting to config.paths.public');
console.log('');
plutils.debug(' patternlab:version');
console.log(' > Return the version of patternlab-node you have installed');
console.log('');
plutils.debug(' patternlab:help');
console.log(' > Get more information about patternlab-node, pattern lab in general, and where to report issues.');
console.log('');
plutils.debug(' patternlab:liststarterkits');
console.log(' > Returns a url with the list of available starterkits hosted on the Pattern Lab organization Github account');
console.log('');
plutils.debug(' patternlab:loadstarterkit');
console.log(' > Load a starterkit into config.paths.source/*');
console.log(' > NOTE: Overwrites existing content, and only cleans out existing directory if --clean=true argument is passed.');
console.log(' > NOTE: In most cases, `npm install starterkit-name` will precede this call.');
console.log(' > arguments:');
console.log(' -- kit ');
console.log(' > the name of the starter kit to load');
console.log(' -- clean ');
console.log(' > removes all files from config.paths.source/ prior to load');
console.log(' > example (gulp):');
console.log(' `gulp patternlab:loadstarterkit --kit=starterkit-mustache-demo`');
console.log('');
console.log('===============================');
console.log('');
console.log('Visit http://patternlab.io/ for more info about Pattern Lab');
console.log('Visit https://github.com/pattern-lab/patternlab-node/issues to open an issue.');
console.log('Visit https://github.com/pattern-lab/patternlab-node/wiki to view the changelog, roadmap, and other info.');
console.log('');
console.log('===============================');
}
function printDebug() {
// A replacer function to pass to stringify below; this is here to prevent
// the debug output from blowing up into a massive fireball of circular
// references. This happens specifically with the Handlebars engine. Remove
// if you like 180MB log files.
function propertyStringReplacer(key, value) {
if (key === 'engine' && value && value.engineName) {
return '{' + value.engineName + ' engine object}';
}
return value;
}
//debug file can be written by setting flag on patternlab-config.json
if (patternlab.config.debug) {
console.log('writing patternlab debug file to ./patternlab.json');
fs.outputFileSync('./patternlab.json', JSON.stringify(patternlab, propertyStringReplacer, 3));
}
}
function setCacheBust() {
if (patternlab.config.cacheBust) {
if (patternlab.config.debug) {
console.log('setting cacheBuster value for frontend assets.');
}
patternlab.cacheBuster = new Date().getTime();
} else {
patternlab.cacheBuster = 0;
}
}
function listStarterkits() {
var starterkit_manager = new sm(patternlab.config);
return starterkit_manager.list_starterkits();
}
function loadStarterKit(starterkitName, clean) {
var starterkit_manager = new sm(patternlab.config);
starterkit_manager.load_starterkit(starterkitName, clean);
}
/**
* Process the user-defined pattern head and prepare it for rendering
*/
function processHeadPattern() {
try {
var headPath = path.resolve(paths.source.meta, '_00-head.mustache');
var headPattern = new Pattern(headPath, null, patternlab);
headPattern.template = fs.readFileSync(headPath, 'utf8');
headPattern.isPattern = false;
headPattern.isMetaPattern = true;
pattern_assembler.decomposePattern(headPattern, patternlab, true);
patternlab.userHead = headPattern.extendedTemplate;
}
catch (ex) {
plutils.error('\nWARNING: Could not find the user-editable header template, currently configured to be at ' + path.join(config.paths.source.meta, '_00-head.mustache') + '. Your configured path may be incorrect (check paths.source.meta in your config file), the file may have been deleted, or it may have been left in the wrong place during a migration or update.\n');
if (patternlab.config.debug) { console.log(ex); }
process.exit(1);
}
}
/**
* Process the user-defined pattern footer and prepare it for rendering
*/
function processFootPattern() {
try {
var footPath = path.resolve(paths.source.meta, '_01-foot.mustache');
var footPattern = new Pattern(footPath, null, patternlab);
footPattern.template = fs.readFileSync(footPath, 'utf8');
footPattern.isPattern = false;
footPattern.isMetaPattern = true;
pattern_assembler.decomposePattern(footPattern, patternlab, true);
patternlab.userFoot = footPattern.extendedTemplate;
}
catch (ex) {
plutils.error('\nWARNING: Could not find the user-editable footer template, currently configured to be at ' + path.join(config.paths.source.meta, '_01-foot.mustache') + '. Your configured path may be incorrect (check paths.source.meta in your config file), the file may have been deleted, or it may have been left in the wrong place during a migration or update.\n');
if (patternlab.config.debug) { console.log(ex); }
process.exit(1);
}
}
function writePatternFiles(headHTML, pattern, footerHTML) {
const nullFormatter = str => str;
const defaultFormatter = codeString => cleanHtml(codeString, {indent_size: 2});
const makePath = type => path.join(paths.public.patterns, pattern.getPatternLink(patternlab, type));
const patternPage = headHTML + pattern.patternPartialCode + footerHTML;
const eng = pattern.engine;
//beautify the output if configured to do so
const formatters = config.cleanOutputHtml ? {
rendered: eng.renderedCodeFormatter || defaultFormatter,
rawTemplate: eng.rawTemplateCodeFormatter || defaultFormatter,
markupOnly: eng.markupOnlyCodeFormatter || defaultFormatter
} : {
rendered: nullFormatter,
rawTemplate: nullFormatter,
markupOnly: nullFormatter
};
//prepare the path and contents of each output file
const outputFiles = [
{ path: makePath('rendered'), content: formatters.rendered(patternPage, pattern) },
{ path: makePath('rawTemplate'), content: formatters.rawTemplate(pattern.template, pattern) },
{ path: makePath('markupOnly'), content: formatters.markupOnly(pattern.patternPartialCode, pattern) }
].concat(
eng.addOutputFiles ? eng.addOutputFiles(paths, patternlab) : []
);
//write the compiled template to the public patterns directory
outputFiles.forEach(outFile => fs.outputFileSync(outFile.path, outFile.content));
}
function renderSinglePattern(pattern, head) {
// Pattern does not need to be built and recompiled more than once
if (!pattern.isPattern || pattern.compileState === CompileState.CLEAN) {
return false;
}
// Allows serializing the compile state
patternlab.graph.node(pattern).compileState = pattern.compileState = CompileState.BUILDING;
//todo move this into lineage_hunter
pattern.patternLineages = pattern.lineage;
pattern.patternLineageExists = pattern.lineage.length > 0;
pattern.patternLineagesR = pattern.lineageR;
pattern.patternLineageRExists = pattern.lineageR.length > 0;
pattern.patternLineageEExists = pattern.patternLineageExists || pattern.patternLineageRExists;
patternlab.events.emit('patternlab-pattern-before-data-merge', patternlab, pattern);
//render the pattern, but first consolidate any data we may have
var allData;
try {
allData = jsonCopy(patternlab.data, 'config.paths.source.data global data');
} catch (err) {
console.log('There was an error parsing JSON for ' + pattern.relPath);
console.log(err);
}
allData = plutils.mergeData(allData, pattern.jsonFileData);
allData.cacheBuster = patternlab.cacheBuster;
//re-rendering the headHTML each time allows pattern-specific data to influence the head of the pattern
pattern.header = head;
var headHTML = pattern_assembler.renderPattern(pattern.header, allData);
//render the extendedTemplate with all data
pattern.patternPartialCode = pattern_assembler.renderPattern(pattern, allData);
// stringify this data for individual pattern rendering and use on the styleguide
// see if patternData really needs these other duped values
// construct our extraOutput dump
var extraOutput = Object.assign({}, pattern.extraOutput, pattern.allMarkdown);
delete(extraOutput.title);
delete(extraOutput.state);
delete(extraOutput.markdown);
pattern.patternData = JSON.stringify({
cssEnabled: false,
patternLineageExists: pattern.patternLineageExists,
patternLineages: pattern.patternLineages,
lineage: pattern.patternLineages,
patternLineageRExists: pattern.patternLineageRExists,
patternLineagesR: pattern.patternLineagesR,
lineageR: pattern.patternLineagesR,
patternLineageEExists: pattern.patternLineageExists || pattern.patternLineageRExists,
patternDesc: pattern.patternDescExists ? pattern.patternDesc : '',
patternBreadcrumb:
pattern.patternGroup === pattern.patternSubGroup ? {
patternType: pattern.patternGroup
} : {
patternType: pattern.patternGroup,
patternSubtype: pattern.patternSubGroup
},
patternExtension: pattern.fileExtension.substr(1), //remove the dot because styleguide asset default adds it for us
patternName: pattern.patternName,
patternPartial: pattern.patternPartial,
patternState: pattern.patternState,
patternEngineName: pattern.engine.engineName,
extraOutput: extraOutput
});
//set the pattern-specific footer by compiling the general-footer with data, and then adding it to the meta footer
var footerPartial = pattern_assembler.renderPattern(patternlab.footer, {
isPattern: pattern.isPattern,
patternData: pattern.patternData,
cacheBuster: patternlab.cacheBuster
});
var allFooterData;
try {
allFooterData = jsonCopy(patternlab.data, 'config.paths.source.data global data');
} catch (err) {
console.log('There was an error parsing JSON for ' + pattern.relPath);
console.log(err);
}
allFooterData = plutils.mergeData(allFooterData, pattern.jsonFileData);
allFooterData.patternLabFoot = footerPartial;
var footerHTML = pattern_assembler.renderPattern(patternlab.userFoot, allFooterData);
patternlab.events.emit('patternlab-pattern-write-begin', patternlab, pattern);
//write the compiled template to the public patterns directory
writePatternFiles(headHTML, pattern, footerHTML);
patternlab.events.emit('patternlab-pattern-write-end', patternlab, pattern);
// Allows serializing the compile state
patternlab.graph.node(pattern).compileState = pattern.compileState = CompileState.CLEAN;
plutils.log.info("Built pattern: " + pattern.patternPartial);
return true;
}
/**
* If a graph was serialized and then {@code deletePatternDir == true}, there is a mismatch in the
* pattern metadata and not all patterns might be recompiled.
* For that reason an empty graph is returned in this case, so every pattern will be flagged as
* "needs recompile". Otherwise the pattern graph is loaded from the meta data.
*
* @param patternlab
* @param {boolean} deletePatternDir When {@code true}, an empty graph is returned
* @return {PatternGraph}
*/
function loadPatternGraph(deletePatternDir) {
// Sanity check to prevent problems when code is refactored
if (deletePatternDir) {
return PatternGraph.empty();
}
return PatternGraph.loadFromFile(patternlab);
}
function buildPatterns(deletePatternDir) {
patternlab.events.emit('patternlab-build-pattern-start', patternlab);
let graph = patternlab.graph = loadPatternGraph(deletePatternDir);
let graphNeedsUpgrade = !PatternGraph.checkVersion(graph);
if (graphNeedsUpgrade) {
plutils.log.info("Due to an upgrade, a complete rebuild is required and the public/patterns directory was deleted. " +
"Incremental build is available again on the next successful run.");
// Ensure that the freshly built graph has the latest version again.
patternlab.graph.upgradeVersion();
}
// Flags
let incrementalBuildsEnabled = !(deletePatternDir || graphNeedsUpgrade);
if (incrementalBuildsEnabled) {
plutils.log.info("Incremental builds enabled.");
} else {
// needs to be done BEFORE processing patterns
fs.removeSync(paths.public.patterns);
fs.emptyDirSync(paths.public.patterns);
}
try {
patternlab.data = buildPatternData(paths.source.data, fs);
} catch (ex) {
plutils.error('missing or malformed' + paths.source.data + 'data.json Pattern Lab may not work without this file.');
patternlab.data = {};
}
try {
patternlab.listitems = dataLoader.loadDataFromFile(path.resolve(paths.source.data, 'listitems'), fs);
} catch (ex) {
plutils.warning('WARNING: missing or malformed ' + paths.source.data + 'listitems file. Pattern Lab may not work without this file.');
patternlab.listitems = {};
}
try {
patternlab.header = fs.readFileSync(path.resolve(paths.source.patternlabFiles, 'partials', 'general-header.mustache'), 'utf8');
patternlab.footer = fs.readFileSync(path.resolve(paths.source.patternlabFiles, 'partials', 'general-footer.mustache'), 'utf8');
patternlab.patternSection = fs.readFileSync(path.resolve(paths.source.patternlabFiles, 'partials', 'patternSection.mustache'), 'utf8');
patternlab.patternSectionSubType = fs.readFileSync(path.resolve(paths.source.patternlabFiles, 'partials', 'patternSectionSubtype.mustache'), 'utf8');
patternlab.viewAll = fs.readFileSync(path.resolve(paths.source.patternlabFiles, 'viewall.mustache'), 'utf8');
} catch (ex) {
console.log(ex);
plutils.error('\nERROR: missing an essential file from ' + paths.source.patternlabFiles + '. Pattern Lab won\'t work without this file.\n');
process.exit(1);
}
patternlab.patterns = [];
patternlab.subtypePatterns = {};
patternlab.partials = {};
patternlab.data.link = {};
setCacheBust();
pattern_assembler.combine_listItems(patternlab);
patternlab.events.emit('patternlab-build-global-data-end', patternlab);
// diveSync once to perform iterative populating of patternlab object
processAllPatternsIterative(pattern_assembler, paths.source.patterns, patternlab);
patternlab.events.emit('patternlab-pattern-iteration-end', patternlab);
//now that all the main patterns are known, look for any links that might be within data and expand them
//we need to do this before expanding patterns & partials into extendedTemplates, otherwise we could lose the data -> partial reference
pattern_assembler.parse_data_links(patternlab);
//diveSync again to recursively include partials, filling out the
//extendedTemplate property of the patternlab.patterns elements
// TODO we can reduce the time needed by only processing changed patterns and their partials
processAllPatternsRecursive(pattern_assembler, paths.source.patterns, patternlab);
//take the user defined head and foot and process any data and patterns that apply
processHeadPattern();
processFootPattern();
//cascade any patternStates
lineage_hunter.cascade_pattern_states(patternlab);
//set pattern-specific header if necessary
var head;
if (patternlab.userHead) {
head = patternlab.userHead;
} else {
head = patternlab.header;
}
//set the pattern-specific header by compiling the general-header with data, and then adding it to the meta header
patternlab.data.patternLabHead = pattern_assembler.renderPattern(patternlab.header, {
cacheBuster: patternlab.cacheBuster
});
// If deletePatternDir == true or graph needs to be updated
// rebuild all patterns
let patternsToBuild = null;
if (incrementalBuildsEnabled) {
// When the graph was loaded from file, some patterns might have been moved/deleted between runs
// so the graph data become out of sync
patternlab.graph.sync().forEach(n => {
plutils.log.info("[Deleted/Moved] " + n);
});
// TODO Find created or deleted files
let now = new Date().getTime();
pattern_assembler.mark_modified_patterns(now, patternlab);
patternsToBuild = patternlab.graph.compileOrder();
} else {
// build all patterns, mark all to be rebuilt
patternsToBuild = patternlab.patterns;
for (let p of patternsToBuild) {
p.compileState = CompileState.NEEDS_REBUILD;
}
}
//render all patterns last, so lineageR works
patternsToBuild.forEach(pattern => renderSinglePattern(pattern, head));
// Saves the pattern graph when all files have been compiled
PatternGraph.storeToFile(patternlab);
if (patternlab.config.exportToGraphViz) {
PatternGraph.exportToDot(patternlab, "dependencyGraph.dot");
plutils.log.info(`Exported pattern graph to ${path.join(config.paths.public.root, "dependencyGraph.dot")}`);
}
//export patterns if necessary
pattern_exporter.export_patterns(patternlab);
}
return {
version: function () {
return getVersion();
},
build: function (callback, deletePatternDir) {
if (patternlab && patternlab.isBusy) {
console.log('Pattern Lab is busy building a previous run - returning early.');
return;
}
patternlab.isBusy = true;
buildPatterns(deletePatternDir);
ui_builder.buildFrontend(patternlab);
printDebug();
patternlab.isBusy = false;
callback();
},
help: function () {
help();
},
patternsonly: function (callback, deletePatternDir) {
if (patternlab && patternlab.isBusy) {
console.log('Pattern Lab is busy building a previous run - returning early.');
return;
}
patternlab.isBusy = true;
buildPatterns(deletePatternDir);
printDebug();
patternlab.isBusy = false;
callback();
},
liststarterkits: function () {
return listStarterkits();
},
loadstarterkit: function (starterkitName, clean) {
loadStarterKit(starterkitName, clean);
},
installplugin: function (pluginName) {
installPlugin(pluginName);
},
getSupportedTemplateExtensions: function () {
return getSupportedTemplateExtensions();
}
};
};
// export these free functions so they're available without calling the exported
// function, for use in reducing code dupe in unit tests. At least, until we
// have a better way to do this
patternlab_engine.build_pattern_data = buildPatternData;
patternlab_engine.process_all_patterns_iterative = processAllPatternsIterative;
patternlab_engine.process_all_patterns_recursive = processAllPatternsRecursive;
module.exports = patternlab_engine;