diff --git a/.eslintrc b/.eslintrc index dbecc8098..f2463e9b6 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,7 +1,8 @@ { "env": { "node": true, - "builtin": true + "builtin": true, + "es6": true }, "parserOptions": { "ecmaVersion": 6, diff --git a/.gitignore b/.gitignore index 74ab03195..e5a709278 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ Thumbs.db source/css/style.css.map .idea/ public +!test/patterns/public/.gitkeep +!test/patterns/testDependencyGraph.json diff --git a/README.md b/README.md index fa94173db..5146582c1 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ The [command line interface](https://github.com/pattern-lab/patternlab-node/wiki If you'd like to contribute to Pattern Lab Node, please do so! There is always a lot of ground to cover and something for your wheelhouse. -No pull request is too small. Check out any [up for grabs issues](https://github.com/pattern-lab/patternlab-node/labels/up%20for%20grabs) as a good way to get your feet wet, or add some more unit tests. +No pull request is too small. Check out any [up for grabs issues](https://github.com/pattern-lab/patternlab-node/labels/help%20wanted%20%2F%20up%20for%20grabs) as a good way to get your feet wet, or add some more unit tests. ## Guidelines 1. Please keep your pull requests concise and limited to **ONE** substantive change at a time. This makes reviewing and testing so much easier. diff --git a/core/lib/changes_hunter.js b/core/lib/changes_hunter.js new file mode 100644 index 000000000..6000b65b4 --- /dev/null +++ b/core/lib/changes_hunter.js @@ -0,0 +1,81 @@ +"use strict"; +const fs = require("fs-extra"), + CompileState = require('./object_factory').CompileState; + +/** + * For detecting changed patterns. + * @constructor + */ +let ChangesHunter = function () { +}; + +ChangesHunter.prototype = { + + /** + * Checks the build state of a pattern by comparing the modification date of the rendered output + * file with the {@link Pattern.lastModified}. If the pattern was modified after the last + * time it has been rendered, it is flagged for rebuilding via {@link CompileState.NEEDS_REBUILD}. + * + * @param {Pattern} pattern + * @param patternlab + * + * @see {@link CompileState} + */ + checkBuildState: function (pattern, patternlab) { + + //write the compiled template to the public patterns directory + let renderedTemplatePath = + patternlab.config.paths.public.patterns + pattern.getPatternLink(patternlab, 'rendered'); + + if (!pattern.compileState) { + pattern.compileState = CompileState.NEEDS_REBUILD; + } + + try { + // Prevent error message if file does not exist + fs.accessSync(renderedTemplatePath, fs.F_OK); + let outputLastModified = fs.statSync(renderedTemplatePath).mtime.getTime(); + + if (pattern.lastModified && outputLastModified > pattern.lastModified) { + pattern.compileState = CompileState.CLEAN; + } + } catch (e) { + // Output does not exist yet, needs recompile + } + + let node = patternlab.graph.node(pattern); + + // Make the pattern known to the PatternGraph and remember its compileState + if (!node) { + patternlab.graph.add(pattern); + } else { + // Works via object reference, so we directly manipulate the node data here + node.compileState = pattern.compileState; + } + + + }, + + /** + * Updates {Pattern#lastModified} to the files modification date if the file was modified + * after {Pattern#lastModified}. + * + * @param {Pattern} currentPattern + * @param {string} file + */ + checkLastModified: function (currentPattern, file) { + if (file) { + try { + let stat = fs.statSync(file); + + // Needs recompile whenever one of the patterns files (template, json, pseudopatterns) changed + currentPattern.lastModified = + Math.max(stat.mtime.getTime(), currentPattern.lastModified || 0); + } catch (e) { + // Ignore, not a regular file + } + } + } +}; + +module.exports = ChangesHunter; \ No newline at end of file diff --git a/core/lib/lineage_hunter.js b/core/lib/lineage_hunter.js index e74e4a6c9..d20db5771 100644 --- a/core/lib/lineage_hunter.js +++ b/core/lib/lineage_hunter.js @@ -1,10 +1,14 @@ "use strict"; +var extend = require("util")._extend; + var lineage_hunter = function () { var pa = require('./pattern_assembler'); function findlineage(pattern, patternlab) { + // As we are adding edges from pattern to ancestor patterns, ensure it is known to the graph + patternlab.graph.add(pattern); var pattern_assembler = new pa(); @@ -28,6 +32,11 @@ var lineage_hunter = function () { l.lineageState = ancestorPattern.patternState; } + patternlab.graph.add(ancestorPattern); + + // Confusing: pattern includes "ancestorPattern", not the other way round + patternlab.graph.link(pattern, ancestorPattern); + pattern.lineage.push(l); //also, add the lineageR entry if it doesn't exist @@ -44,26 +53,33 @@ var lineage_hunter = function () { } ancestorPattern.lineageR.push(lr); + extend(patternlab.graph.node(ancestorPattern), lr); } } }); } } - function setPatternState(direction, pattern, targetPattern) { - // if the request came from the past, apply target pattern state to current pattern lineage + /** + * Apply the target pattern state either to any predecessors or successors of the given + * pattern in the pattern graph. + * @param direction Either 'fromPast' or 'fromFuture' + * @param pattern {Pattern} + * @param targetPattern {Pattern} + * @param graph {PatternGraph} + */ + function setPatternState(direction, pattern, targetPattern, graph) { + var index = null; if (direction === 'fromPast') { - for (var i = 0; i < pattern.lineageIndex.length; i++) { - if (pattern.lineageIndex[i] === targetPattern.patternPartial) { - pattern.lineage[i].lineageState = targetPattern.patternState; - } - } + index = graph.lineage(pattern); } else { - //the request came from the future, apply target pattern state to current pattern reverse lineage - for (var i = 0; i < pattern.lineageRIndex.length; i++) { - if (pattern.lineageRIndex[i] === targetPattern.patternPartial) { - pattern.lineageR[i].lineageState = targetPattern.patternState; - } + index = graph.lineageR(pattern); + } + + // if the request came from the past, apply target pattern state to current pattern lineage + for (var i = 0; i < index.length; i++) { + if (index[i].patternPartial === targetPattern.patternPartial) { + index[i].lineageState = targetPattern.patternState; } } } @@ -71,35 +87,37 @@ var lineage_hunter = function () { function cascadePatternStates(patternlab) { - var pattern_assembler = new pa(); - for (var i = 0; i < patternlab.patterns.length; i++) { var pattern = patternlab.patterns[i]; //for each pattern with a defined state if (pattern.patternState) { + var lineage = patternlab.graph.lineage(pattern); - if (pattern.lineageIndex && pattern.lineageIndex.length > 0) { + if (lineage && lineage.length > 0) { //find all lineage - patterns being consumed by this one - for (var h = 0; h < pattern.lineageIndex.length; h++) { - var lineagePattern = pattern_assembler.getPartial(pattern.lineageIndex[h], patternlab); - setPatternState('fromFuture', lineagePattern, pattern); + for (var h = 0; h < lineage.length; h++) { + // Not needed, the graph already knows the concrete pattern + // var lineagePattern = pattern_assembler.getPartial(lineageIndex[h], patternlab); + setPatternState('fromFuture', lineage[h], pattern, patternlab.graph); } } - - if (pattern.lineageRIndex && pattern.lineageRIndex.length > 0) { + var lineageR = patternlab.graph.lineageR(pattern); + if (lineageR && lineageR.length > 0) { //find all reverse lineage - that is, patterns consuming this one - for (var j = 0; j < pattern.lineageRIndex.length; j++) { + for (var j = 0; j < lineageR.length; j++) { - var lineageRPattern = pattern_assembler.getPartial(pattern.lineageRIndex[j], patternlab); + var lineageRPattern = lineageR[j]; //only set patternState if pattern.patternState "is less than" the lineageRPattern.patternstate //or if lineageRPattern.patternstate (the consuming pattern) does not have a state //this makes patternlab apply the lowest common ancestor denominator - if (lineageRPattern.patternState === '' || (patternlab.config.patternStateCascade.indexOf(pattern.patternState) - < patternlab.config.patternStateCascade.indexOf(lineageRPattern.patternState))) { + let patternStateCascade = patternlab.config.patternStateCascade; + let patternStateIndex = patternStateCascade.indexOf(pattern.patternState); + let patternReverseStateIndex = patternStateCascade.indexOf(lineageRPattern.patternState); + if (lineageRPattern.patternState === '' || (patternStateIndex < patternReverseStateIndex)) { if (patternlab.config.debug) { console.log('Found a lower common denominator pattern state: ' + pattern.patternState + ' on ' + pattern.patternPartial + '. Setting reverse lineage pattern ' + lineageRPattern.patternPartial + ' from ' + (lineageRPattern.patternState === '' ? '<>' : lineageRPattern.patternState)); @@ -108,9 +126,9 @@ var lineage_hunter = function () { lineageRPattern.patternState = pattern.patternState; //take this opportunity to overwrite the lineageRPattern's lineage state too - setPatternState('fromPast', lineageRPattern, pattern); + setPatternState('fromPast', lineageRPattern, pattern, patternlab.graph); } else { - setPatternState('fromPast', pattern, lineageRPattern); + setPatternState('fromPast', pattern, lineageRPattern, patternlab.graph); } } } diff --git a/core/lib/object_factory.js b/core/lib/object_factory.js index 29b637e4d..1263d80df 100644 --- a/core/lib/object_factory.js +++ b/core/lib/object_factory.js @@ -71,7 +71,23 @@ var Pattern = function (relPath, data, patternlab) { this.lineageR = []; this.lineageRIndex = []; this.isPseudoPattern = false; + this.order = Number.MAX_SAFE_INTEGER; this.engine = patternEngines.getEngineForPattern(this); + + /** + * Determines if this pattern needs to be recompiled. + * + * @ee {@link CompileState}*/ + this.compileState = null; + + /** + * Timestamp in milliseconds when the pattern template or auxilary file (e.g. json) were modified. + * If multiple files are affected, this is the timestamp of the most recent change. + * + * @see {@link pattern} + */ + this.lastModified = null; + }; // Pattern methods @@ -140,7 +156,16 @@ Pattern.prototype = { // factory: creates an empty Pattern for miscellaneous internal use, such as // by list_item_hunter Pattern.createEmpty = function (customProps, patternlab) { - var pattern = new Pattern('', null, patternlab); + let relPath = ''; + if (customProps) { + if (customProps.relPath) { + relPath = customProps.relPath; + } else if (customProps.subdir && customProps.filename) { + relPath = customProps.subdir + path.sep + customProps.filename; + } + } + + var pattern = new Pattern(relPath, null, patternlab); return extend(pattern, customProps); }; @@ -152,6 +177,13 @@ Pattern.create = function (relPath, data, customProps, patternlab) { return extend(newPattern, customProps); }; +var CompileState = { + NEEDS_REBUILD: "needs rebuild", + BUILDING: "building", + CLEAN: "clean" +}; + module.exports = { - Pattern: Pattern + Pattern: Pattern, + CompileState: CompileState }; diff --git a/core/lib/pattern_assembler.js b/core/lib/pattern_assembler.js index 05df15e82..a873ad1d9 100644 --- a/core/lib/pattern_assembler.js +++ b/core/lib/pattern_assembler.js @@ -3,6 +3,7 @@ var path = require('path'), fs = require('fs-extra'), Pattern = require('./object_factory').Pattern, + CompileState = require('./object_factory').CompileState, pph = require('./pseudopattern_hunter'), mp = require('./markdown_parser'), plutils = require('./utilities'), @@ -11,9 +12,11 @@ var path = require('path'), lih = require('./list_item_hunter'), smh = require('./style_modifier_hunter'), ph = require('./parameter_hunter'), + ch = require('./changes_hunter'), JSON5 = require('json5'); var markdown_parser = new mp(); +var changes_hunter = new ch(); var pattern_assembler = function () { // HELPER FUNCTIONS @@ -45,7 +48,7 @@ var pattern_assembler = function () { return patternlab.patterns[i]; } } - plutils.logOrange('Could not find pattern referenced with partial syntax ' + partialName + '. This can occur when a pattern was renamed, moved, or no longer exists but it still called within a different template somewhere.'); + plutils.warning('Could not find pattern referenced with partial syntax ' + partialName + '. This can occur when a pattern was renamed, moved, or no longer exists but it still called within a different template somewhere.'); return undefined; } @@ -81,7 +84,7 @@ var pattern_assembler = function () { if (patternlab.config.patternStates && patternlab.config.patternStates[pattern.patternPartial]) { if (displayDeprecatedWarning) { - plutils.logRed("Deprecation Warning: Using patternlab-config.json patternStates object will be deprecated in favor of the state frontmatter key associated with individual pattern markdown files."); + plutils.error("Deprecation Warning: Using patternlab-config.json patternStates object will be deprecated in favor of the state frontmatter key associated with individual pattern markdown files."); console.log("This feature will still work in it's current form this release (but still be overridden by the new parsing method), and will be removed in the future."); } @@ -123,7 +126,7 @@ var pattern_assembler = function () { } else { patternlab.partials[pattern.patternPartial] = pattern.patternDesc; } - + patternlab.graph.add(pattern); patternlab.patterns.push(pattern); } @@ -245,13 +248,13 @@ var pattern_assembler = function () { var relativeDepth = (relPath.match(/\w(?=\\)|\w(?=\/)/g) || []).length; if (relativeDepth > 2) { console.log(''); - plutils.logOrange('Warning:'); - plutils.logOrange('A pattern file: ' + relPath + ' was found greater than 2 levels deep from ' + patternlab.config.paths.source.patterns + '.'); - plutils.logOrange('It\'s strongly suggested to not deviate from the following structure under _patterns/'); - plutils.logOrange('[patternType]/[patternSubtype]/[patternName].[patternExtension]'); + plutils.warning('Warning:'); + plutils.warning('A pattern file: ' + relPath + ' was found greater than 2 levels deep from ' + patternlab.config.paths.source.patterns + '.'); + plutils.warning('It\'s strongly suggested to not deviate from the following structure under _patterns/'); + plutils.warning('[patternType]/[patternSubtype]/[patternName].[patternExtension]'); console.log(''); - plutils.logOrange('While Pattern Lab may still function, assets may 404 and frontend links may break. Consider yourself warned. '); - plutils.logOrange('Read More: http://patternlab.io/docs/pattern-organization.html'); + plutils.warning('While Pattern Lab may still function, assets may 404 and frontend links may break. Consider yourself warned. '); + plutils.warning('Read More: http://patternlab.io/docs/pattern-organization.html'); console.log(''); } @@ -355,7 +358,9 @@ var pattern_assembler = function () { parsePatternMarkdown(currentPattern, patternlab); //add the raw template to memory - currentPattern.template = fs.readFileSync(path.resolve(patternsPath, relPath), 'utf8'); + var templatePath = path.resolve(patternsPath, currentPattern.relPath); + + currentPattern.template = fs.readFileSync(templatePath, 'utf8'); //find any stylemodifiers that may be in the current pattern currentPattern.stylePartials = currentPattern.findPartialsWithStyleModifiers(); @@ -363,6 +368,12 @@ var pattern_assembler = function () { //find any pattern parameters that may be in the current pattern currentPattern.parameteredPartials = currentPattern.findPartialsWithPatternParameters(); + [templatePath, jsonFilename, listJsonFileName].forEach(file => { + changes_hunter.checkLastModified(currentPattern, file); + }); + + changes_hunter.checkBuildState(currentPattern, patternlab); + //add currentPattern to patternlab.patterns array addPattern(currentPattern, patternlab); @@ -393,6 +404,15 @@ var pattern_assembler = function () { decomposePattern(currentPattern, patternlab); } + function findModifiedPatterns(lastModified, patternlab) { + return patternlab.patterns.filter(p => { + if (p.compileState !== CompileState.CLEAN || ! p.lastModified) { + return true; + } + return p.lastModified >= lastModified; + }); + } + function expandPartials(foundPatternPartials, list_item_hunter, patternlab, currentPattern) { var style_modifier_hunter = new smh(), @@ -506,6 +526,9 @@ var pattern_assembler = function () { } return { + find_modified_patterns: function (lastModified, patternlab) { + return findModifiedPatterns(lastModified, patternlab); + }, find_pattern_partials: function (pattern) { return pattern.findPartials(); }, diff --git a/core/lib/pattern_engines.js b/core/lib/pattern_engines.js index 451f8d4fe..c316cec9d 100644 --- a/core/lib/pattern_engines.js +++ b/core/lib/pattern_engines.js @@ -3,6 +3,7 @@ var path = require('path'); var diveSync = require('diveSync'); +const chalk = require('chalk'); var engineMatcher = /^patternengine-node-(.*)$/; var enginesDirectories = [ { @@ -53,17 +54,15 @@ function findEngineModulesInDirectory(dir) { // Try to load engines! We scan for engines at each path specified above. This // function is kind of a big deal. function loadAllEngines(enginesObject) { - console.log('\nLoading engines...'); - enginesDirectories.forEach(function (engineDirectory) { var enginesInThisDir = findEngineModulesInDirectory(engineDirectory.path); - console.log("...scanning for engines in", engineDirectory.displayName + "..."); + 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) { var errorMessage; - var successMessage = "good to go"; + var successMessage = chalk.green("good to go"); try { // give it a try! load 'er up. But not if we already have, of course. @@ -75,16 +74,17 @@ function loadAllEngines(enginesObject) { errorMessage = err.message; } finally { // report on the status of the engine, one way or another! - console.log('-', engineDiscovery.name, 'engine:', errorMessage ? errorMessage : successMessage); + console.log(` ${engineDiscovery.name}:`, errorMessage ? chalk.red(errorMessage) : successMessage); } }); + console.log(''); }); // Complain if for some reason we haven't loaded any engines. if (Object.keys(enginesObject).length === 0) { throw new Error('No engines loaded! Something is seriously wrong.'); } - console.log('...done loading engines.\n'); + console.log(chalk.bold('Done loading engines.\n')); } diff --git a/core/lib/pattern_graph.js b/core/lib/pattern_graph.js new file mode 100644 index 000000000..08b94ba6d --- /dev/null +++ b/core/lib/pattern_graph.js @@ -0,0 +1,404 @@ +"use strict"; + +const graphlib = require('graphlib'); +const Graph = graphlib.Graph; +const path = require('path'); +const fs = require("fs-extra"); +const Pattern = require('./object_factory').Pattern; +const CompileState = require('./object_factory').CompileState; +const PatternGraphDot = require('./pattern_graph_dot'); +const PatternRegistry = require('./pattern_registry'); + +/** + * The most recent version of the pattern graph. This is used to rebuild the graph when + * the version of a serialized graph does not match the current version. + * @type {number} + */ +const PATTERN_GRAPH_VERSION = 1; + +/** + * Wrapper around a graph library to build a dependency graph of patterns. + * Each node in the graph will maintain a {@link CompileState}. This allows finding all + * changed patterns and their transitive dependencies. + * + * Internally the graph maintains a {@link PatternRegistry} to allow fast lookups of the patterns. + * + * @constructor Constructs a new PatternGraph from a JSON-style JavaScript object or an empty graph + * if no argument is given. + * + * @param {Graph} graph The graphlib graph object + * @param {int} timestamp The unix timestamp + * @param {int} version The graph version. + * + * @returns {{PatternGraph: PatternGraph}} + + * @see PatternGraph#fromJson + * @see #540 + */ +const PatternGraph = function (graph, timestamp, version) { + + this.graph = graph || new Graph({ + directed: true + }); + this.graph.setDefaultEdgeLabel({}); + + // Allows faster lookups for patterns by name for each element in the graph + // The idea here is to make a pattern known to the graph as soon as it exists + this.patterns = new PatternRegistry(); + this.timestamp = timestamp || new Date().getTime(); + this.version = version || PATTERN_GRAPH_VERSION; +}; + +// shorthand. Use relPath as it is always unique, even with subPatternType +var nodeName = + p => p instanceof Pattern ? p.relPath : p; + +PatternGraph.prototype = { + + /** + * Creates an independent copy of the graph where nodes and edges can be modified without + * affecting the source. + */ + clone: function () { + const json = graphlib.json.write(this.graph); + const graph = graphlib.json.read(json); + return new PatternGraph(graph, this.timestamp, this.version); + }, + + /** + * Add a pattern to the graph and copy its {@link Pattern.compileState} to the node's data. + * If the pattern is already known, nothing is done. + * + * @param {Pattern} pattern + */ + add: function (pattern) { + const n = nodeName(pattern); + if (!this.patterns.has(n)) { + this.graph.setNode(n, { + compileState: pattern.compileState + }); + + this.patterns.put(pattern); + } + }, + + remove: function (pattern) { + const n = nodeName(pattern); + this.graph.removeNode(n); + this.patterns.remove(n); + }, + + /** + * Removes nodes from this graph for which the given predicate function returns false. + * @param {function} fn which takes a node name as argument + */ + filter: function (fn) { + this.graph.nodes().forEach(n => { + if (!fn(n)) { + this.remove(n); + } + }); + }, + + /** + * Creates a directed edge in the graph which indicates pattern inclusion. + * Patterns must be {@link PatternGraph.add added} before using this method. + * + * @param {Pattern} patternFrom The pattern (subject) which includes the other pattern + * @param {Pattern} patternTo The pattern (object) that is included by the subject. + * + * @throws {Error} If the pattern is unknown + */ + link: function (patternFrom, patternTo) { + let nameFrom = nodeName(patternFrom); + let nameTo = nodeName(patternTo); + for (let name of [nameFrom, nameTo]) { + if (!this.patterns.has(name)) { + throw new Error("Pattern not known: " + name); + } + } + this.graph.setEdge(nameFrom, nameTo); + }, + + + /** + * Determines if there is one pattern is included by another. + * @param {Pattern} patternFrom + * @param {Pattern} patternTo + * + * @return {boolean} + */ + hasLink: function (patternFrom, patternTo) { + let nameFrom = nodeName(patternFrom); + let nameTo = nodeName(patternTo); + return this.graph.hasEdge(nameFrom, nameTo); + }, + + /** + * Determines the order in which all changed patterns and there transitive predecessors must + * be rebuild. + * + * This first finds all patterns that must be rebuilt, second marks any patterns that transitively + * include these patterns for rebuilding and finally applies topological sorting to the graph. + * + * @return {Array} An Array of {@link Pattern}s in the order by which the changed patters must be + * compiled. + */ + compileOrder: function () { + const compileStateFilter = function (patterns, n) { + const node = patterns.get(n); + return node.compileState !== CompileState.CLEAN; + }; + + /** + * This graph only contains those nodes that need recompilation + * Edges are added in reverse order for topological sorting(e.g. atom -> molecule -> organism, + * where "->" means "included by"). + */ + let compileGraph = new Graph({ + directed: true + }); + + let nodes = this.graph.nodes(); + let changedNodes = nodes.filter(n => compileStateFilter(this.patterns, n)); + this.nodes2patterns(changedNodes).forEach(pattern => { + let patternNode = nodeName(pattern); + if (!compileGraph.hasNode(patternNode)) { + compileGraph.setNode(patternNode); + } + this.applyReverse(pattern, (from, to) => { + from.compileState = CompileState.NEEDS_REBUILD; + let fromName = nodeName(from); + let toName = nodeName(to); + for (let name of [fromName, toName]) { + if (!compileGraph.hasNode(name)) { + compileGraph.setNode(name); + } + } + if (!compileGraph.hasNode(toName)) { + compileGraph.setNode(toName); + } + + // reverse! + compileGraph.setEdge({v:toName, w:fromName}); + }); + }); + + // Apply topological sorting, Start at the leafs of the graphs (e.g. atoms) and go further + // up in the hierarchy + const o = graphlib.alg.topsort(compileGraph); + return this.nodes2patterns(o); + }, + + /** + * Given a node and its predecessor, allows exchanging states between nodes. + * @param pattern + * @param fn A function that takes the currently viewed pattern and node data. Allows synching data + * between patterns and node metadata. + */ + applyReverse: function (pattern, fn) { + for (let p of this.lineageR(pattern)) { + fn(p, pattern); + this.applyReverse(p, fn); + } + }, + + /** + * Find the node fro a pattern + * + * @param {Pattern} pattern + * + * @return [null|Pattern] + */ + node: function (pattern) { + return this.graph.node(nodeName(pattern)); + }, + + /** + * + * @param nodes {Array} + * @return {Array} An Array of Patterns + */ + nodes2patterns: function (nodes) { + return nodes.map(n => this.patterns.get(n)); + }, + + // TODO cache result in a Map[String, Array]? + // We trade the pattern.lineage array - O(pattern.lineage.length << |V|) - vs. O(|V|) of the graph. + // As long as no edges are added or removed, we can cache the result in a Map and just return it. + /** + * Finds all immediate successors of a pattern, i.e. all patterns which the given pattern includes. + * @param pattern + * @return {*|Array} + */ + lineage: function (pattern) { + const nodes = this.graph.successors(nodeName(pattern)); + return this.nodes2patterns(nodes); + }, + + /** + * Returns all patterns that include the given pattern + * @param {Pattern} pattern + * @return {*|Array} + */ + lineageR: function (pattern) { + const nodes = this.graph.predecessors(nodeName(pattern)); + return this.nodes2patterns(nodes); + }, + + /** + * Given a {Pattern}, return all partial names of {Pattern} objects included in this the given pattern + * @param {Pattern} pattern + * + * @see {@link PatternGraph.lineage(pattern)} + */ + lineageIndex: function (pattern) { + const lineage = this.lineage(pattern); + return lineage.map(p => p.patternPartial); + }, + + /** + * Given a {Pattern}, return all partial names of {Pattern} objects which include the given pattern + * @param {Pattern} pattern + * + * @return {Array} + * + * @see {@link PatternGraph.lineageRIndex(pattern)} + */ + lineageRIndex: function (pattern) { + const lineageR = this.lineageR(pattern); + return lineageR.map(p => p.patternPartial); + }, + + /** + * Creates an object representing the graph and meta data. + * @returns {{timestamp: number, graph}} + */ + toJson: function () { + return { + version: this.version, + timestamp: this.timestamp, + graph: graphlib.json.write(this.graph) + }; + }, + + /** + * @return {Array} An array of all node names. + */ + nodes: function () { + return this.graph.nodes(); + }, + + /** + * Updates the version to the most recent one + */ + upgradeVersion: function () { + this.version = PATTERN_GRAPH_VERSION; + } +}; + +/** + * Creates an empty graph with a unix timestamp of 0 as last compilation date. + * @param {int} [version=PATTERN_GRAPH_VERSION] + * @return {PatternGraph} + */ +PatternGraph.empty = function (version) { + return new PatternGraph(null, 0, version || PATTERN_GRAPH_VERSION); +}; + +/** + * Checks if the version of + * @param {PatternGraph|Object} graphOrJson + * @return {boolean} + */ +PatternGraph.checkVersion = function (graphOrJson) { + return graphOrJson.version === PATTERN_GRAPH_VERSION; +}; + +/** + * Error that is thrown if the given version does not match the current graph version. + * + * @param oldVersion + * @constructor + */ +function VersionMismatch(oldVersion) { + this.message = `Version of graph on disk ${oldVersion} != current version ${PATTERN_GRAPH_VERSION}. Please clean your patterns output directory.`; + this.name = "VersionMismatch"; +} + +/** + * Parse the graph from a JSON object. + * @param {object} o The JSON object to read from + * @return {PatternGraph} + */ +PatternGraph.fromJson = function (o) { + if (!PatternGraph.checkVersion(o)) { + throw new VersionMismatch(o.version); + } + const graph = graphlib.json.read(o.graph); + return new PatternGraph(graph, o.timestamp, o.version); +}; + +/** + * Resolve the path to the file containing the serialized graph + * @param {object} patternlab + * @param {string} [file='dependencyGraph.json'] Path to the graph file + * @return {string} + */ +PatternGraph.resolveJsonGraphFile = function (patternlab, file) { + return path.resolve(patternlab.config.paths.public.root, file || 'dependencyGraph.json'); +}; + +/** + * Loads a graph from the file. Does not add any patterns from the patternlab object, + * i.e. graph.patterns will be still empty until all patterns have been processed. + * + * @param {object} patternlab + * @param {string} [file] Optional path to the graph json file + * + * @see {@link PatternGraph.fromJson} + * @see {@link PatternGraph.resolveJsonGraphFile} + */ +PatternGraph.loadFromFile = function (patternlab, file) { + const jsonGraphFile = this.resolveJsonGraphFile(patternlab, file); + + // File is fresh, so simply constuct an empty graph in memory + if (!fs.existsSync(jsonGraphFile)) { + return PatternGraph.empty(); + } + + const obj = fs.readJSONSync(jsonGraphFile); + if (!PatternGraph.checkVersion(obj)) { + return PatternGraph.empty(obj.version); + } + return this.fromJson(obj); +}; + +/** + * Serializes the graph to a file. + * @param patternlab + * @param {string} [file] For unit testing only. + * + * @see {@link PatternGraph.resolveJsonGraphFile} + */ +PatternGraph.storeToFile = function (patternlab, file) { + const jsonGraphFile = this.resolveJsonGraphFile(patternlab, file); + patternlab.graph.timestamp = new Date().getTime(); + fs.writeJSONSync(jsonGraphFile, patternlab.graph.toJson()); +}; + +/** + * Exports this graph to a GraphViz file. + * @param patternlab + @ @param {string} file Output file + */ +PatternGraph.exportToDot = function (patternlab, file) { + const dotFile = this.resolveJsonGraphFile(patternlab, file); + const g = PatternGraphDot.generate(patternlab.graph); + fs.outputFileSync(dotFile, g); +}; + +module.exports = { + PatternGraph: PatternGraph, + PATTERN_GRAPH_VERSION: PATTERN_GRAPH_VERSION +}; diff --git a/core/lib/pattern_graph_dot.js b/core/lib/pattern_graph_dot.js new file mode 100644 index 000000000..5d87c383e --- /dev/null +++ b/core/lib/pattern_graph_dot.js @@ -0,0 +1,145 @@ +"use strict"; + +/** + * Overall settings + * @return {[string,string,string,string,string,string,string]} + */ +function header() { + return [ + "strict digraph {", + 'graph [fontname = "helvetica" size=20]', + + /*compound=true;*/ + "concentrate=true;", + "rankdir=LR;", + "ranksep=\"4 equally·\";", + "node [style=filled,color=white];", + "edge [style=dotted constraint=false]" + ]; +} + +/** + * Graph nodes cannot start with numbers in GrahViz and must not contain dashes. + * @param name + * @return {string} + */ +const niceKey = function (name) { + return "O" + name.replace("-", ""); +}; + +/** + * Adds the output for defining a node in GraphViz. + * + * @param {Pattern} pattern + * @return {string} + */ +function addNode(pattern) { + let more = ""; + if (pattern.isPseudoPattern) { + more = " [fillcolor=grey]"; + } + return "\"" + pattern.name + "\"" + more + ";\n"; +} + +/** + * + * @param {Pattern} from + * @param {Pattern} to + * @param {string} color A valid color, e.g. HTMl or a color name + * @return {string} + */ +function addEdge(from, to, color) { + return `"${from.name}" -> "${to.name}" [color=${color}];\n`; +} + +/** + * Creates a sub-graph which is used to group atoms, molecules, etc. + * @param group + * @param patterns + * @return {[*,*,string,string,*,*,string]} + */ +function subGraph(group, patterns) { + const s = niceKey(group); + return [ + "subgraph cluster_X" + s + " {", + "label=<" + group + ">;", + "style=filled;", + "color=lightgrey;", + s + " [shape=box];", + patterns.map(addNode).join(""), + + //patterns.map(p => "\"" + p.name + "\"").join(" -> ") + "[style=invis]", + "}" + ]; + +} + +function footer() { + return ["}"]; +} + +const PatternGraphDot = {}; + +/** + * Create the GraphViz representation of the given graph + * @param patternGraph + * @return {string} + */ +PatternGraphDot.generate = function (patternGraph) { + const g = patternGraph.graph; + const patterns = patternGraph.patterns; + let buckets = new Map(); + const colors = ["darkgreen", "firebrick", "slateblue", "darkgoldenrod", "black"]; + const colorMap = new Map(); + let colIdx = 0; + for (let p of patterns.partials.values()) { + if (p.isPseudoPattern || !p.patternType) { + continue; + } + let bucket = buckets.get(p.patternType); + if (bucket) { + bucket.push(p); + } else { + bucket = [p]; + colorMap.set(p.patternType, colors[colIdx++]); + + // Repeat if there are more categories + colIdx = colIdx % colors.length; + } + buckets.set(p.patternType, bucket); + } + + let res = header(); + const sortedKeys = Array.from(buckets.keys()).sort(); + + const niceKeys = sortedKeys.map(niceKey); + + let subGraphLines = []; + + + for (let key of sortedKeys) { + const subPatterns = buckets.get(key); + subGraphLines = subGraphLines.concat(subGraph(key, subPatterns)); + } + res = res.concat(subGraphLines); + res.push("edge[style=solid];"); + + + foo: for (let edge of g.edges()) { + let fromTo = patternGraph.nodes2patterns([edge.v, edge.w]); + for (let pattern of fromTo) { + if (pattern.isPseudoPattern || !pattern.patternType) { + continue foo; + } + } + const thisColor = colorMap.get(fromTo[0].patternType); + res.push(addEdge(fromTo[0], fromTo[1], thisColor)); + } + + res.push(niceKeys.reverse().join(" -> ") + "[constraint=true];"); + res = res.concat(footer()); + return res.join("\n") + "\n"; +}; + + +module.exports = PatternGraphDot; diff --git a/core/lib/pattern_registry.js b/core/lib/pattern_registry.js new file mode 100644 index 000000000..eda22d0ae --- /dev/null +++ b/core/lib/pattern_registry.js @@ -0,0 +1,100 @@ +"use strict"; + +/** + * Allows lookups for patterns via a central registry. + * @constructor + */ +const PatternRegistry = function () { + this.key2pattern = new Map(); + + /** For lookups by {@link Pattern#partialKey} */ + this.partials = new Map(); +}; + +PatternRegistry.prototype = { + + allPatterns: function () { + return Array.from(this.key2pattern.values()); + }, + + has: function (name) { + return this.key2pattern.has(name); + }, + + get: function (name) { + return this.key2pattern.get(name); + }, + + /** + * Adds the given pattern to the registry. If a pattern with the same key exists, it is replaced. + * @param pattern {Pattern|*} + */ + put: function (pattern) { + const name = PatternRegistry.partialName(pattern); + this.partials.set(name, pattern); + const key = PatternRegistry.patternKey(pattern); + this.key2pattern.set(key, pattern); + }, + + remove: function (name) { + this.key2pattern.delete(name); + }, + + getPartial: function (partialName) { + /* + Code in here has been moved from pattern_assembler.getPartial() to prepare for some refactoring. + There are a few advantages to this method: + - use a map lookup instead of interating through all patterns + - get rid of dependency to the patternlab object + - make code more readable + */ + + // This previously has been a for loop over an array in pattern_ + let byPartialName = this.partials.get(partialName); + if (this.partials.has(partialName)) { + return byPartialName; + } + + + let patterns = this.allPatterns(); + + //else look by verbose syntax + for (let thisPattern of patterns) { + switch (partialName) { + case thisPattern.relPath: + case thisPattern.subdir + '/' + thisPattern.fileName: + return thisPattern; + } + } + + //return the fuzzy match if all else fails + for (let thisPattern of patterns) { + const partialParts = partialName.split('-'), + partialType = partialParts[0], + partialNameEnd = partialParts.slice(1).join('-'); + + const patternPartial = thisPattern.patternPartial; + if (patternPartial.split('-')[0] === partialType + && patternPartial.indexOf(partialNameEnd) > -1) { + return thisPattern; + } + } + return undefined; + } +}; + +PatternRegistry.patternKey = function (pattern) { + return pattern.relPath; +}; + +/** + * Defines how the partial key of a pattern is resolved. + * + * @param pattern {Pattern} + * @return {string} + */ +PatternRegistry.partialName = function (pattern) { + return pattern.patternPartial; +}; + +module.exports = PatternRegistry; diff --git a/core/lib/patternlab.js b/core/lib/patternlab.js index df3275f18..680ef52f4 100644 --- a/core/lib/patternlab.js +++ b/core/lib/patternlab.js @@ -1,5 +1,5 @@ /* - * patternlab-node - v2.6.2 - 2016 + * patternlab-node - v2.7.0 - 2016 * * Brian Muenzenmeyer, Geoff Pursell, and the web community. * Licensed under the MIT license. @@ -14,12 +14,28 @@ var diveSync = require('diveSync'), glob = require('glob'), _ = 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'), - plutils = require('./utilities'); - + packageInfo = require('../../package.json'), + plutils = require('./utilities'), + PatternGraph = require('./pattern_graph').PatternGraph; + +//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; function buildPatternData(dataFilesPath, fsDep) { @@ -71,9 +87,9 @@ function checkConfiguration(patternlab) { }; if (!patternlab.config.outputFileSuffixes) { - plutils.logOrange('Configuration Object "outputFileSuffixes" not found, and defaulted to the following:'); + plutils.warning('Configuration Object "outputFileSuffixes" not found, and defaulted to the following:'); console.log(outputFileSuffixes); - plutils.logOrange('Since Pattern Lab Core 2.3.0 this configuration option is required. Suggest you add it to your patternlab-config.json file.'); + 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); @@ -135,8 +151,11 @@ var patternlab_engine = function (config) { ui = require('./ui_builder'), sm = require('./starterkit_manager'), Pattern = require('./object_factory').Pattern, + CompileState = require('./object_factory').CompileState, patternlab = {}; + patternlab.engines = patternEngines; + var pattern_assembler = new pa(), pattern_exporter = new pe(), lineage_hunter = new lh(); @@ -145,8 +164,12 @@ var patternlab_engine = function (config) { 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; @@ -160,34 +183,34 @@ var patternlab_engine = function (config) { console.log(''); console.log('|=======================================|'); - plutils.logGreen(' Pattern Lab Node Help v' + patternlab.package.version); + 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.logGreen(' patternlab:build'); + plutils.debug(' patternlab:build'); console.log(' > Compiles the patterns and frontend, outputting to config.paths.public'); console.log(''); - plutils.logGreen(' patternlab:patternsonly'); + plutils.debug(' patternlab:patternsonly'); console.log(' > Compiles the patterns only, outputting to config.paths.public'); console.log(''); - plutils.logGreen(' patternlab:version'); + plutils.debug(' patternlab:version'); console.log(' > Return the version of patternlab-node you have installed'); console.log(''); - plutils.logGreen(' patternlab:help'); + plutils.debug(' patternlab:help'); console.log(' > Get more information about patternlab-node, pattern lab in general, and where to report issues.'); console.log(''); - plutils.logGreen(' patternlab:liststarterkits'); + 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.logGreen(' patternlab:loadstarterkit'); + 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.'); @@ -263,7 +286,7 @@ var patternlab_engine = function (config) { patternlab.userHead = headPattern.extendedTemplate; } catch (ex) { - plutils.logRed('\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'); + 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); } @@ -283,7 +306,7 @@ var patternlab_engine = function (config) { patternlab.userFoot = footPattern.extendedTemplate; } catch (ex) { - plutils.logRed('\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'); + 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); } @@ -320,20 +343,116 @@ var patternlab_engine = function (config) { 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 = JSON5.parse(JSON5.stringify(patternlab.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 + 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: {} + }); + + //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 = JSON5.parse(JSON5.stringify(patternlab.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; + } + function buildPatterns(deletePatternDir) { patternlab.events.emit('patternlab-build-pattern-start', patternlab); + patternlab.graph = PatternGraph.loadFromFile(patternlab); try { patternlab.data = buildPatternData(paths.source.data, fs); } catch (ex) { - plutils.logRed('missing or malformed' + paths.source.data + 'data.json Pattern Lab may not work without this file.'); + plutils.error('missing or malformed' + paths.source.data + 'data.json Pattern Lab may not work without this file.'); patternlab.data = {}; } try { patternlab.listitems = fs.readJSONSync(path.resolve(paths.source.data, 'listitems.json')); } catch (ex) { - plutils.logOrange('WARNING: missing or malformed ' + paths.source.data + 'listitems.json file. Pattern Lab may not work without this file.'); + plutils.warning('WARNING: missing or malformed ' + paths.source.data + 'listitems.json file. Pattern Lab may not work without this file.'); patternlab.listitems = {}; } try { @@ -344,7 +463,7 @@ var patternlab_engine = function (config) { patternlab.viewAll = fs.readFileSync(path.resolve(paths.source.patternlabFiles, 'viewall.mustache'), 'utf8'); } catch (ex) { console.log(ex); - plutils.logRed('\nERROR: missing an essential file from ' + paths.source.patternlabFiles + '. Pattern Lab won\'t work without this file.\n'); + 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 = []; @@ -365,6 +484,7 @@ var patternlab_engine = function (config) { //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 @@ -378,12 +498,6 @@ var patternlab_engine = function (config) { //cascade any patternStates lineage_hunter.cascade_pattern_states(patternlab); - //delete the contents of config.patterns.public before writing - if (deletePatternDir) { - fs.removeSync(paths.public.patterns); - fs.emptyDirSync(paths.public.patterns); - } - //set pattern-specific header if necessary var head; if (patternlab.userHead) { @@ -397,96 +511,46 @@ var patternlab_engine = function (config) { cacheBuster: patternlab.cacheBuster }); - //render all patterns last, so lineageR works - patternlab.patterns.forEach(function (pattern) { + let patternsToBuild = patternlab.patterns; - if (!pattern.isPattern) { - return false; - } + let graphNeedsUpgrade = !PatternGraph.checkVersion(patternlab.graph); - //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 = JSON5.parse(JSON5.stringify(patternlab.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 - 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: {} - }); - - //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 = JSON5.parse(JSON5.stringify(patternlab.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; + // Incremental builds are enabled, but we cannot use them + if (!deletePatternDir && graphNeedsUpgrade) { + plutils.log.info("Due to an upgrade, a complete rebuild is required. " + + "Incremental build is available again on the next run."); - var footerHTML = pattern_assembler.renderPattern(patternlab.userFoot, allFooterData); + // Ensure that the freshly built graph has the latest version again. + patternlab.graph.upgradeVersion(); + } + + //delete the contents of config.patterns.public before writing + //Also if the serialized graph must be updated + if (deletePatternDir || graphNeedsUpgrade) { + fs.removeSync(paths.public.patterns); + fs.emptyDirSync(paths.public.patterns); + } else { + // TODO Find created or deleted files + let now = new Date().getTime(); + var modified = pattern_assembler.find_modified_patterns(now, patternlab); - patternlab.events.emit('patternlab-pattern-write-begin', patternlab, pattern); + // First mark all modified files + for (let p of modified) { + p.compileState = CompileState.NEEDS_REBUILD; + } + patternsToBuild = patternlab.graph.compileOrder(); + } - //write the compiled template to the public patterns directory - writePatternFiles(headHTML, pattern, footerHTML); - patternlab.events.emit('patternlab-pattern-write-end', patternlab, pattern); + //render all patterns last, so lineageR works + patternsToBuild.forEach(pattern => renderSinglePattern(pattern, head)); - return true; - }); + // 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); diff --git a/core/lib/plugin_manager.js b/core/lib/plugin_manager.js index 2a0869036..136cc7324 100644 --- a/core/lib/plugin_manager.js +++ b/core/lib/plugin_manager.js @@ -29,8 +29,8 @@ var plugin_manager = function (config, configPath) { try { var pluginDirStats = fs.statSync(pluginPath); } catch (ex) { - util.logRed(pluginName + ' not found, please use npm to install it first.'); - util.logRed(pluginName + ' not loaded.'); + util.error(pluginName + ' not found, please use npm to install it first.'); + util.error(pluginName + ' not loaded.'); return; } var pluginPathDirExists = pluginDirStats.isDirectory(); @@ -50,7 +50,7 @@ var plugin_manager = function (config, configPath) { //write config entry back fs.outputFileSync(path.resolve(configPath), JSON.stringify(diskConfig, null, 2)); - util.logGreen('Plugin ' + pluginName + ' installed.'); + util.debug('Plugin ' + pluginName + ' installed.'); //todo, tell them how to uninstall or disable diff --git a/core/lib/pseudopattern_hunter.js b/core/lib/pseudopattern_hunter.js index d695b4f0e..fbcca89ad 100644 --- a/core/lib/pseudopattern_hunter.js +++ b/core/lib/pseudopattern_hunter.js @@ -1,5 +1,7 @@ "use strict"; +var ch = require('./changes_hunter'); + var pseudopattern_hunter = function () { function findpseudopatterns(currentPattern, patternlab) { @@ -14,6 +16,7 @@ var pseudopattern_hunter = function () { var pattern_assembler = new pa(); var lineage_hunter = new lh(); + var changes_hunter = new ch(); var paths = patternlab.config.paths; //look for a pseudo pattern by checking if there is a file containing same @@ -33,7 +36,8 @@ var pseudopattern_hunter = function () { //we want to do everything we normally would here, except instead read the pseudoPattern data try { - var variantFileData = fs.readJSONSync(path.resolve(paths.source.patterns, pseudoPatterns[i])); + var variantFileFullPath = path.resolve(paths.source.patterns, pseudoPatterns[i]); + var variantFileData = fs.readJSONSync(variantFileFullPath); } catch (err) { console.log('There was an error parsing pseudopattern JSON for ' + currentPattern.relPath); console.log(err); @@ -44,6 +48,7 @@ var pseudopattern_hunter = function () { var variantName = pseudoPatterns[i].substring(pseudoPatterns[i].indexOf('~') + 1).split('.')[0]; var variantFilePath = path.join(currentPattern.subdir, currentPattern.fileName + '~' + variantName + '.json'); + var lm = fs.statSync(variantFileFullPath); var patternVariant = Pattern.create(variantFilePath, variantFileData, { //use the same template as the non-variant template: currentPattern.template, @@ -54,10 +59,18 @@ var pseudopattern_hunter = function () { stylePartials: currentPattern.stylePartials, parameteredPartials: currentPattern.parameteredPartials, + // Only regular patterns are discovered during iterative walks + // Need to recompile on data change or template change + lastModified: Math.max(currentPattern.lastModified, lm.mtime), + // use the same template engine as the non-variant engine: currentPattern.engine }, patternlab); + changes_hunter.checkBuildState(patternVariant, patternlab); + patternlab.graph.add(patternVariant); + patternlab.graph.link(patternVariant, currentPattern); + //process the companion markdown file if it exists pattern_assembler.parse_pattern_markdown(patternVariant, patternlab); diff --git a/core/lib/starterkit_manager.js b/core/lib/starterkit_manager.js index 3dec08115..db4153486 100644 --- a/core/lib/starterkit_manager.js +++ b/core/lib/starterkit_manager.js @@ -8,7 +8,7 @@ var starterkit_manager = function (config) { paths = config.paths; /** - * Loads npm module identified by the starterkitName parameter. + * Loads npm module identified by the starterkitName parameter. * * @param starterkitName {string} Kit name * @param clean {boolean} Indicates if the directory should be cleaned before loading @@ -22,8 +22,8 @@ var starterkit_manager = function (config) { try { var kitDirStats = fs.statSync(kitPath); } catch (ex) { - util.logRed(starterkitName + ' not found, please use npm to install it first.'); - util.logRed(starterkitName + ' not loaded.'); + util.error(starterkitName + ' not found, please use npm to install it first.'); + util.error(starterkitName + ' not loaded.'); return; } var kitPathDirExists = kitDirStats.isDirectory(); @@ -40,7 +40,7 @@ var starterkit_manager = function (config) { if (ex) { console.error(ex); } - util.logGreen('starterkit ' + starterkitName + ' loaded successfully.'); + util.debug('starterkit ' + starterkitName + ' loaded successfully.'); }); } } catch (ex) { diff --git a/core/lib/ui_builder.js b/core/lib/ui_builder.js index c8b5be431..e112dd8b3 100644 --- a/core/lib/ui_builder.js +++ b/core/lib/ui_builder.js @@ -115,6 +115,7 @@ var ui_builder = function () { var docPattern = patternlab.subtypePatterns[pattern.patternGroup + (isSubtypePattern ? '-' + pattern.patternSubGroup : '')]; if (docPattern) { docPattern.isDocPattern = true; + docPattern.order = -Number.MAX_SAFE_INTEGER; return docPattern; } @@ -130,7 +131,8 @@ var ui_builder = function () { isPattern: false, engine: null, flatPatternPath: pattern.flatPatternPath, - isDocPattern: true + isDocPattern: true, + order: -Number.MAX_SAFE_INTEGER }, patternlab ); @@ -165,7 +167,7 @@ var ui_builder = function () { var patternType = _.find(patternlab.patternTypes, ['patternType', pattern.patternType]); if (!patternType) { - plutils.logRed('Could not find patternType' + pattern.patternType + '. This is a critical error.'); + plutils.error('Could not find patternType' + pattern.patternType + '. This is a critical error.'); console.trace(); process.exit(1); } @@ -184,7 +186,7 @@ var ui_builder = function () { var patternSubType = _.find(patternType.patternTypeItems, ['patternSubtype', pattern.patternSubType]); if (!patternSubType) { - plutils.logRed('Could not find patternType ' + pattern.patternType + '-' + pattern.patternType + '. This is a critical error.'); + plutils.error('Could not find patternType ' + pattern.patternType + '-' + pattern.patternType + '. This is a critical error.'); console.trace(); process.exit(1); } @@ -199,16 +201,16 @@ var ui_builder = function () { * @param pattern - the pattern to register */ function addPatternSubType(patternlab, pattern) { + let newSubType = { + patternSubtypeLC: pattern.patternSubGroup.toLowerCase(), + patternSubtypeUC: pattern.patternSubGroup.charAt(0).toUpperCase() + pattern.patternSubGroup.slice(1), + patternSubtype: pattern.patternSubType, + patternSubtypeDash: pattern.patternSubGroup, //todo verify + patternSubtypeItems: [] + }; var patternType = getPatternType(patternlab, pattern); - patternType.patternTypeItems.push( - { - patternSubtypeLC: pattern.patternSubGroup.toLowerCase(), - patternSubtypeUC: pattern.patternSubGroup.charAt(0).toUpperCase() + pattern.patternSubGroup.slice(1), - patternSubtype: pattern.patternSubType, - patternSubtypeDash: pattern.patternSubGroup, //todo verify - patternSubtypeItems: [] - } - ); + let insertIndex = _.sortedIndexBy(patternType.patternTypeItems, newSubType, 'patternSubtype'); + patternType.patternTypeItems.splice(insertIndex, 0, newSubType); } /** @@ -230,7 +232,8 @@ var ui_builder = function () { patternName: pattern.patternName, patternState: pattern.patternState, patternSrcPath: encodeURI(pattern.subdir + '/' + pattern.fileName), - patternPath: patternPath + patternPath: patternPath, + order: pattern.order }; } @@ -242,23 +245,25 @@ var ui_builder = function () { * @param createViewAllVariant - whether or not to create the special view all item */ function addPatternSubTypeItem(patternlab, pattern, createSubtypeViewAllVarient) { - var patternSubType = getPatternSubType(patternlab, pattern); + let newSubTypeItem; + if (createSubtypeViewAllVarient) { - patternSubType.patternSubtypeItems.push( - { - patternPartial: 'viewall-' + pattern.patternGroup + '-' + pattern.patternSubGroup, - patternName: 'View All', - patternPath: encodeURI(pattern.flatPatternPath + '/index.html'), - patternType: pattern.patternType, - patternSubtype: pattern.patternSubtype - } - ); + newSubTypeItem = { + patternPartial: 'viewall-' + pattern.patternGroup + '-' + pattern.patternSubGroup, + patternName: 'View All', + patternPath: encodeURI(pattern.flatPatternPath + '/index.html'), + patternType: pattern.patternType, + patternSubtype: pattern.patternSubtype, + order: 0 + }; } else { - patternSubType.patternSubtypeItems.push( - createPatternSubTypeItem(pattern) - ); + newSubTypeItem = createPatternSubTypeItem(pattern); } + + let patternSubType = getPatternSubType(patternlab, pattern); + patternSubType.patternSubtypeItems.push(newSubTypeItem); + patternSubType.patternSubtypeItems = _.sortBy(patternSubType.patternSubtypeItems, ['order', 'name']); } /** @@ -269,7 +274,7 @@ var ui_builder = function () { function addPatternItem(patternlab, pattern, isViewAllVariant) { var patternType = getPatternType(patternlab, pattern); if (!patternType) { - plutils.logRed('Could not find patternType' + pattern.patternType + '. This is a critical error.'); + plutils.error('Could not find patternType' + pattern.patternType + '. This is a critical error.'); console.trace(); process.exit(1); } @@ -284,13 +289,15 @@ var ui_builder = function () { patternType.patternItems.push({ patternPartial: 'viewall-' + pattern.patternGroup + '-all', patternName: 'View All', - patternPath: encodeURI(pattern.patternType + '/index.html') + patternPath: encodeURI(pattern.patternType + '/index.html'), + order: -Number.MAX_SAFE_INTEGER }); } } else { patternType.patternItems.push(createPatternSubTypeItem(pattern)); } + patternType.patternItems = _.sortBy(patternType.patternItems, ['order', 'name']); } // function getPatternItems(patternlab, patternType) { @@ -302,20 +309,54 @@ var ui_builder = function () { // } /** - * Sorts patterns based on name. - * Will be expanded to use explicit order in the near future + * Sorts patterns based on order property found within pattern markdown, falling back on name. * @param patternsArray - patterns to sort * @returns sorted patterns */ function sortPatterns(patternsArray) { return patternsArray.sort(function (a, b) { - if (a.name > b.name) { - return 1; + let aOrder = parseInt(a.order, 10); + let bOrder = parseInt(b.order, 10); + + if (aOrder === NaN) { + aOrder = Number.MAX_SAFE_INTEGER; + } + + if (bOrder === NaN) { + aOrder = Number.MAX_SAFE_INTEGER; } - if (a.name < b.name) { + + //alwasy return a docPattern first + if (a.isDocPattern && !b.isDocPattern) { return -1; } + + if (!a.isDocPattern && b.isDocPattern) { + return 1; + } + + //use old alphabetical ordering if we have nothing else to use + //pattern.order will be Number.MAX_SAFE_INTEGER if never defined by markdown, or markdown parsing fails + if (aOrder === Number.MAX_SAFE_INTEGER && bOrder === Number.MAX_SAFE_INTEGER) { + + if (a.name > b.name) { + return 1; + } + if (a.name < b.name) { + return -1; + } + } + + //if we get this far, we can sort safely + if (aOrder && bOrder) { + if (aOrder > bOrder) { + return 1; + } + if (aOrder < bOrder) { + return -1; + } + } return 0; }); } @@ -330,7 +371,7 @@ var ui_builder = function () { patternGroups: {} }; - _.forEach(sortPatterns(patternlab.patterns), function (pattern) { + _.forEach(patternlab.patterns, function (pattern) { //ignore patterns we can omit from rendering directly pattern.omitFromStyleguide = isPatternExcluded(pattern, patternlab); @@ -459,8 +500,8 @@ var ui_builder = function () { //render the footer needed for the viewall template var footerHTML = buildFooterHTML(patternlab, 'viewall-' + patternPartial); - //render the viewall template - var subtypePatterns = _.values(patternSubtypes); + //render the viewall template by finding these smallest subtype-grouped patterns + var subtypePatterns = sortPatterns(_.values(patternSubtypes)); //determine if we should write at this time by checking if these are flat patterns or grouped patterns p = _.find(subtypePatterns, function (pat) { @@ -536,7 +577,7 @@ var ui_builder = function () { output += 'var viewAllPaths = ' + JSON.stringify(patternlab.viewAllPaths) + ';' + eol; //plugins - output += 'var plugins = ' + JSON.stringify(patternlab.plugins) + ';' + eol; + output += 'var plugins = ' + JSON.stringify(patternlab.plugins || []) + ';' + eol; //smaller config elements output += 'var defaultShowPatternInfo = ' + (patternlab.config.defaultShowPatternInfo ? patternlab.config.defaultShowPatternInfo : 'false') + ';' + eol; diff --git a/core/lib/utilities.js b/core/lib/utilities.js index 8e1488814..28166a830 100644 --- a/core/lib/utilities.js +++ b/core/lib/utilities.js @@ -1,129 +1,153 @@ "use strict"; -var fs = require('fs-extra'), - path = require('path'); +const fs = require('fs-extra'); +const path = require('path'); +const chalk = require('chalk'); +const EventEmitter = require('events').EventEmitter; -var util = { - /** - * Shuffles an array in place. - * http://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array-in-javascript - * - * @param {Array} o - * @returns {Array} o - */ - shuffle: function (o) { - /*eslint-disable curly*/ - for (var j, x, i = o.length; i; j = Math.floor(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x); - return o; +/** + * @name log + * @desc tiny event-based logger + * @type {*} + */ +const log = Object.assign({ + debug(msg) { + this.emit('debug', chalk.green(msg)); }, - - /** - * Logs a message to the console with green text. - * - * @param {string} message - * @returns {string} message - */ - logGreen: function (message) { - console.log('\x1b[32m', message, '\x1b[0m'); + info(msg) { + this.emit('info', msg); }, - - /** - * Logs a message to the console with orange text. - * - * @param {string} message - * @returns {string} message - */ - logOrange: function (message) { - console.log('\x1b[33m', message, '\x1b[0m'); + warning(msg) { + this.emit('warning', chalk.yellow(msg)); }, + error(msg) { + this.emit('error', chalk.red(msg)); + } +}, EventEmitter.prototype); - /** - * Logs a message to the console with red text. - * - * @param {string} message - * @returns {string} message - */ - logRed: function (message) { - console.log('\x1b[41m', message, '\x1b[0m'); - }, +/** + * @func debug + * @desc Coloured debug log + * @param {*} msg - The variadic messages to log out. + * @return {void} + */ +const debug = log.debug.bind(log); + +/** + * @func warning + * @desc Coloured error log + * @param {*} e - The variadic messages to log out. + * @return {void} + */ +const warning = log.warning.bind(log); + +/** + * @func error + * @desc Coloured error log + * @param {*} e - The variadic messages to log out. + * @return {void} + */ +const error = log.error.bind(log); /** - * Recursively merge properties of two objects. + * Shuffles an array in place. + * http://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array-in-javascript * - * @param {Object} obj1 If obj1 has properties obj2 doesn't, add to obj2. - * @param {Object} obj2 This object's properties have priority over obj1. - * @returns {Object} obj2 + * @param {Array} o + * @returns {Array} o */ - mergeData: function (obj1, obj2) { - /*eslint-disable no-param-reassign, guard-for-in*/ - if (typeof obj2 === 'undefined') { - obj2 = {}; - } - for (var p in obj1) { - try { - // Only recurse if obj1[p] is an object. - if (obj1[p].constructor === Object) { - // Requires 2 objects as params; create obj2[p] if undefined. - if (typeof obj2[p] === 'undefined') { - obj2[p] = {}; - } - obj2[p] = util.mergeData(obj1[p], obj2[p]); +const shuffle = function (o) { + /*eslint-disable curly*/ + for (var j, x, i = o.length; i; j = Math.floor(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x); + return o; +}; - // Pop when recursion meets a non-object. If obj1[p] is a non-object, - // only copy to undefined obj2[p]. This way, obj2 maintains priority. - } else if (typeof obj2[p] === 'undefined') { - obj2[p] = obj1[p]; - } - } catch (e) { - // Property in destination object not set; create it and set its value. +/** + * Recursively merge properties of two objects. + * + * @param {Object} obj1 If obj1 has properties obj2 doesn't, add to obj2. + * @param {Object} obj2 This object's properties have priority over obj1. + * @returns {Object} obj2 + */ +const mergeData = function (obj1, obj2) { + /*eslint-disable no-param-reassign, guard-for-in*/ + if (typeof obj2 === 'undefined') { + obj2 = {}; + } + for (var p in obj1) { + try { + // Only recurse if obj1[p] is an object. + if (obj1[p].constructor === Object) { + // Requires 2 objects as params; create obj2[p] if undefined. if (typeof obj2[p] === 'undefined') { - obj2[p] = obj1[p]; + obj2[p] = {}; } + obj2[p] = mergeData(obj1[p], obj2[p]); + + // Pop when recursion meets a non-object. If obj1[p] is a non-object, + // only copy to undefined obj2[p]. This way, obj2 maintains priority. + } else if (typeof obj2[p] === 'undefined') { + obj2[p] = obj1[p]; + } + } catch (e) { + // Property in destination object not set; create it and set its value. + if (typeof obj2[p] === 'undefined') { + obj2[p] = obj1[p]; } } - return obj2; - }, + } + return obj2; +}; - /** - * Determines whether or not an object is empty. - * - * @param {Object} obj - * @returns {Boolean} - */ - isObjectEmpty: function (obj) { - for (var prop in obj) { - if (obj.hasOwnProperty(prop)) { return false; } - } - return true; - }, +/** + * Determines whether or not an object is empty. + * + * @param {Object} obj + * @returns {Boolean} + */ +const isObjectEmpty = function (obj) { + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { return false; } + } + return true; +}; - /** - * Recursively delete the contents of directory. - * Adapted from https://gist.github.com/tkihira/2367067 - * - * @param {string} dir - directory to empty - * @param {string} cleanDir - already empty directory - * @returns {undefined} - */ - emptyDirectory: function (dir, cleanDir) { - var list = fs.readdirSync(dir); - for (var i = 0; i < list.length; i++) { - var filename = path.join(dir, list[i]); - var stat = fs.statSync(filename); +/** + * Recursively delete the contents of directory. + * Adapted from https://gist.github.com/tkihira/2367067 + * + * @param {string} dir - directory to empty + * @param {string} cleanDir - already empty directory + * @returns {undefined} + */ +const emptyDirectory = function (dir, cleanDir) { + var list = fs.readdirSync(dir); + for (var i = 0; i < list.length; i++) { + var filename = path.join(dir, list[i]); + var stat = fs.statSync(filename); - if (filename === "." || filename === "..") { - // pass these files - } else if (stat.isDirectory()) { - this.emptyDirectory(filename); - } else { - // rm fiilename - fs.unlinkSync(filename); - } - } - if (cleanDir) { - fs.rmdirSync(dir); + if (filename === "." || filename === "..") { + // pass these files + } else if (stat.isDirectory()) { + this.emptyDirectory(filename); + } else { + // rm fiilename + fs.unlinkSync(filename); } } + if (cleanDir) { + fs.rmdirSync(dir); + } +}; + +module.exports = { + debug, + warning, + error, + log, + shuffle, + mergeData, + isObjectEmpty, + emptyDirectory }; -module.exports = util; diff --git a/core/scripts/postinstall.js b/core/scripts/postinstall.js index 53636e1d0..019697667 100644 --- a/core/scripts/postinstall.js +++ b/core/scripts/postinstall.js @@ -38,10 +38,10 @@ try { } } - u.logGreen('Pattern Lab postinstall complete.'); + u.debug('Pattern Lab postinstall complete.'); } catch (ex) { console.log(ex); - u.logOrange('An error occurred during Pattern Lab Node postinstall.'); - u.logOrange('Pattern Lab postinstall completed with errors.'); + u.warning('An error occurred during Pattern Lab Node postinstall.'); + u.warning('Pattern Lab postinstall completed with errors.'); } diff --git a/package.json b/package.json index 29cc5d61c..4ea578ac4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "patternlab-node", "description": "Pattern Lab is a collection of tools to help you create atomic design systems. This is the node command line interface (CLI).", - "version": "2.6.2", + "version": "2.7.0", "main": "./core/lib/patternlab.js", "dependencies": { "diveSync": "^0.3.0", @@ -13,9 +13,11 @@ "lodash": "~4.13.1", "markdown-it": "^6.0.1", "node-fetch": "^1.6.0", - "patternengine-node-mustache": "^1.0.0" + "patternengine-node-mustache": "^1.0.0", + "graphlib": "^2.1.1" }, "devDependencies": { + "chalk": "^1.1.3", "eslint": "^3.5.0", "rewire": "^2.5.2", "tap": "^7.1.2" diff --git a/patternlab-config.json b/patternlab-config.json index ffc197121..70ff944c7 100644 --- a/patternlab-config.json +++ b/patternlab-config.json @@ -60,5 +60,6 @@ "rawTemplate": "", "markupOnly": ".markup-only" }, - "cleanOutputHtml": true + "cleanOutputHtml": true, + "exportToGraphViz": false } diff --git a/test/engine_handlebars_tests.js b/test/engine_handlebars_tests.js index cf1fa533e..58a7085f6 100644 --- a/test/engine_handlebars_tests.js +++ b/test/engine_handlebars_tests.js @@ -5,6 +5,7 @@ var tap = require('tap'); var path = require('path'); var pa = require('../core/lib/pattern_assembler'); var Pattern = require('../core/lib/object_factory').Pattern; +var PatternGraph = require('../core/lib/pattern_graph').PatternGraph; var testPatternsPath = path.resolve(__dirname, 'files', '_handlebars-test-patterns'); var eol = require('os').EOL; @@ -22,6 +23,7 @@ if (!engineLoader.handlebars) { // apparatus. function fakePatternLab() { var fpl = { + graph: PatternGraph.empty(), partials: {}, patterns: [], footer: '', @@ -222,3 +224,42 @@ tap.test('hidden handlebars patterns can be called by their nice names', functio test.equals(util.sanitized(testPattern.render()), util.sanitized('Here\'s the hidden atom: [I\'m the hidden atom\n]\n')); test.end(); }); + +tap.test('@partial-block template should render without throwing (@geoffp repo issue #3)', function(test) { + test.plan(1); + + var patternPath = path.join('00-atoms', '00-global', '10-at-partial-block.hbs'); + + // do all the normal processing of the pattern + var patternlab = new fakePatternLab(); + var assembler = new pa(); + var atPartialBlockPattern = assembler.process_pattern_iterative(patternPath, patternlab); + assembler.process_pattern_recursive(patternPath, patternlab); + + var results = '{{> @partial-block }}' + eol + 'It worked!' + eol; + test.equal(atPartialBlockPattern.render(), results); + test.end(); +}) + +tap.test('A template calling a @partial-block template should render correctly', function(test) { + test.plan(1); + + // pattern paths + var pattern1Path = path.join('00-atoms', '00-global', '10-at-partial-block.hbs'); + var pattern2Path = path.join('00-molecules', '00-global', '10-call-at-partial-block.hbs'); + + // set up environment + var patternlab = new fakePatternLab(); // environment + var assembler = new pa(); + + // do all the normal processing of the pattern + assembler.process_pattern_iterative(pattern1Path, patternlab); + var callAtPartialBlockPattern = assembler.process_pattern_iterative(pattern2Path, patternlab); + assembler.process_pattern_recursive(pattern1Path, patternlab); + assembler.process_pattern_recursive(pattern2Path, patternlab); + + // test + var results = 'Hello World!' + eol + 'It worked!' + eol; + test.equals(callAtPartialBlockPattern.render(), results); + test.end(); +}) diff --git a/test/engine_mustache_tests.js b/test/engine_mustache_tests.js index ea93ca723..30d2c3048 100644 --- a/test/engine_mustache_tests.js +++ b/test/engine_mustache_tests.js @@ -3,8 +3,8 @@ var tap = require('tap'); var path = require('path'); -var pa = require('../core/lib/pattern_assembler'); var Pattern = require('../core/lib/object_factory').Pattern; +var PatternGraph = require('../core/lib/pattern_graph').PatternGraph; var testPatternsPath = path.resolve(__dirname, 'files', '_patterns'); var eol = require('os').EOL; @@ -22,6 +22,7 @@ if (!engineLoader.mustache) { // apparatus. function fakePatternLab() { var fpl = { + graph: PatternGraph.empty(), partials: {}, patterns: [], footer: '', diff --git a/test/engine_twig_tests.js b/test/engine_twig_tests.js index 5a8351f7a..d41b70901 100644 --- a/test/engine_twig_tests.js +++ b/test/engine_twig_tests.js @@ -6,6 +6,7 @@ var tap = require('tap'); var path = require('path'); var pa = require('../core/lib/pattern_assembler'); var Pattern = require('../core/lib/object_factory').Pattern; +var PatternGraph = require('../core/lib/pattern_graph').PatternGraph; var eol = require('os').EOL; // don't run these tests unless twig is installed @@ -22,6 +23,7 @@ if (!engineLoader.twig) { // apparatus. function fakePatternLab() { var fpl = { + graph: PatternGraph.empty(), partials: {}, patterns: [], footer: '', diff --git a/test/engine_underscore_tests.js b/test/engine_underscore_tests.js index 8c245bdf4..7e2342449 100644 --- a/test/engine_underscore_tests.js +++ b/test/engine_underscore_tests.js @@ -3,6 +3,7 @@ var tap = require('tap'); var path = require('path'); var pa = require('../core/lib/pattern_assembler'); +var PatternGraph = require('../core/lib/pattern_graph').PatternGraph; var testPatternsPath = path.resolve(__dirname, 'files', '_underscore-test-patterns'); var eol = require('os').EOL; @@ -20,6 +21,7 @@ if (!engineLoader.underscore) { // apparatus. function fakePatternLab() { var fpl = { + graph: PatternGraph.empty(), partials: {}, patterns: [], footer: '', diff --git a/test/files/_handlebars-test-patterns/00-atoms/00-global/10-at-partial-block.hbs b/test/files/_handlebars-test-patterns/00-atoms/00-global/10-at-partial-block.hbs new file mode 100644 index 000000000..59e0522cd --- /dev/null +++ b/test/files/_handlebars-test-patterns/00-atoms/00-global/10-at-partial-block.hbs @@ -0,0 +1,2 @@ +{{> @partial-block }} +It worked! diff --git a/test/files/_handlebars-test-patterns/00-molecules/00-global/10-call-at-partial-block.hbs b/test/files/_handlebars-test-patterns/00-molecules/00-global/10-call-at-partial-block.hbs new file mode 100644 index 000000000..d653c1196 --- /dev/null +++ b/test/files/_handlebars-test-patterns/00-molecules/00-global/10-call-at-partial-block.hbs @@ -0,0 +1,3 @@ +{{#> atoms-at-partial-block }} +Hello World! +{{/atoms-at-partial-block}} diff --git a/test/files/_patterns/patternType1/patternType2/black.mustache b/test/files/_patterns/patternType1/patternSubType2/black.mustache similarity index 100% rename from test/files/_patterns/patternType1/patternType2/black.mustache rename to test/files/_patterns/patternType1/patternSubType2/black.mustache diff --git a/test/files/_patterns/patternType1/patternType2/grey.mustache b/test/files/_patterns/patternType1/patternSubType2/grey.mustache similarity index 100% rename from test/files/_patterns/patternType1/patternType2/grey.mustache rename to test/files/_patterns/patternType1/patternSubType2/grey.mustache diff --git a/test/files/_patterns/patternType1/patternType2/white.mustache b/test/files/_patterns/patternType1/patternSubType2/white.mustache similarity index 100% rename from test/files/_patterns/patternType1/patternType2/white.mustache rename to test/files/_patterns/patternType1/patternSubType2/white.mustache diff --git a/test/lineage_hunter_tests.js b/test/lineage_hunter_tests.js index aea24758d..d1206c1e1 100644 --- a/test/lineage_hunter_tests.js +++ b/test/lineage_hunter_tests.js @@ -6,6 +6,7 @@ var lh = require('../core/lib/lineage_hunter'); var pa = require('../core/lib/pattern_assembler'); var of = require('../core/lib/object_factory'); var Pattern = require('../core/lib/object_factory').Pattern; +var PatternGraph = require('../core/lib/pattern_graph').PatternGraph; var fs = require('fs-extra'); var path = require('path'); @@ -25,6 +26,7 @@ function createFakeEmptyErrorPattern() { function createBasePatternLabObject() { var patterns_dir = './test/files/_patterns/'; var pl = {}; + pl.graph = PatternGraph.empty(), pl.config = { paths: { source: { @@ -60,6 +62,7 @@ tap.test('find_lineage - finds lineage', function (test) { }); var patternlab = { + graph: new PatternGraph(null, 0), patterns: [ Pattern.createEmpty({ "name": "00-atoms-03-images-00-logo", @@ -127,14 +130,21 @@ tap.test('find_lineage - finds lineage', function (test) { } } }; + // BAD: This "patches" the relative path which is unset when using "createEmpty" + patternlab.patterns.forEach(p => p.relPath = p.patternLink); var lineage_hunter = new lh(); lineage_hunter.find_lineage(currentPattern, patternlab); - test.equals(currentPattern.lineageIndex.length, 3); - test.equals(currentPattern.lineageIndex[0], "atoms-logo"); - test.equals(currentPattern.lineageIndex[1], "molecules-primary-nav"); - test.equals(currentPattern.lineageIndex[2], "molecules-search"); + var graphLineageIndex = patternlab.graph.lineageIndex(currentPattern); + + // Ensure compatibility + for (let i of [ currentPattern.lineageIndex, graphLineageIndex ]) { + test.equals(i.length, 3); + test.equals(i[0], "atoms-logo"); + test.equals(i[1], "molecules-primary-nav"); + test.equals(i[2], "molecules-search"); + } test.end(); }); @@ -148,6 +158,7 @@ tap.test('find_lineage - finds lineage with spaced pattern parameters', function }); var patternlab = { + graph: new PatternGraph(null, 0), patterns: [ Pattern.create( '00-atoms/05-alerts/00-error.mustache', @@ -173,6 +184,21 @@ tap.test('find_lineage - finds lineage with spaced pattern parameters', function test.equals(patternlab.patterns[0].lineageRIndex.length, 1); test.equals(patternlab.patterns[0].lineageR[0].lineagePattern, 'molecules-error'); + // Same as above, but as graph based variant + var graph = patternlab.graph; + // Test if there is an edge from molecule-toast-error to atoms-alerts-error + test.same( + graph.hasLink(currentPattern, patternlab.patterns[0]), + true, "There is an edge from the test-error molecule to the alerts-error atom"); + var currentPatternLineageIndex = graph.lineageIndex(currentPattern); + + test.equals(currentPatternLineageIndex.length, 1); + test.equals(currentPatternLineageIndex[0], "atoms-error"); + + var patternlabPattern0_lineageRIndex = graph.lineageRIndex(patternlab.patterns[0]); + test.equals(patternlabPattern0_lineageRIndex.length, 1); + test.equals(patternlabPattern0_lineageRIndex[0], 'molecules-error'); + test.end(); }); @@ -228,7 +254,8 @@ tap.test('cascade_pattern_states promotes a lower pattern state up to the consum //assert var consumerPatternReturned = pattern_assembler.getPartial('test-foo', pl); - test.equals(consumerPatternReturned.lineage[0].lineageState, 'inreview'); + let lineage = pl.graph.lineage(consumerPatternReturned); + test.equals(lineage[0].lineageState, 'inreview'); test.end(); }); @@ -255,7 +282,8 @@ tap.test('cascade_pattern_states sets the pattern state on any lineage patterns //assert var consumedPatternReturned = pattern_assembler.getPartial('test-bar', pl); - test.equals(consumedPatternReturned.lineageR[0].lineageState, 'inreview'); + let lineageR = pl.graph.lineageR(consumedPatternReturned); + test.equals(lineageR[0].lineageState, 'inreview'); test.end(); }); @@ -298,6 +326,7 @@ tap.test('find_lineage - finds lineage with unspaced pattern parameters', functi }); var patternlab = { + graph: PatternGraph.empty(), patterns: [ Pattern.createEmpty({ "name": "01-atoms-05-alerts-00-error", @@ -314,7 +343,7 @@ tap.test('find_lineage - finds lineage with unspaced pattern parameters', functi "patternPartial": "atoms-error", "patternState": "", "lineage": [], - "lineageIndex": [], + "currentPatternLineageIndex": [], "lineageR": [], "lineageRIndex": [] }) @@ -336,6 +365,14 @@ tap.test('find_lineage - finds lineage with unspaced pattern parameters', functi test.equals(patternlab.patterns[0].lineageRIndex.length, 1); test.equals(patternlab.patterns[0].lineageR[0].lineagePattern, 'molecules-error'); + var currentPatternLineageIndex = patternlab.graph.lineageIndex(currentPattern); + test.equals(currentPatternLineageIndex.length, 1); + test.equals(currentPatternLineageIndex[0], "atoms-error"); + + var pattern0LineageRIndex = patternlab.graph.lineageRIndex(patternlab.patterns[0]); + test.equals(pattern0LineageRIndex.length, 1); + test.equals(pattern0LineageRIndex[0], 'molecules-error'); + test.end(); }); @@ -361,6 +398,7 @@ tap.test('find_lineage - finds lineage with spaced styleModifier', function (tes "lineageRIndex": [] }); var patternlab = { + graph: new PatternGraph(null, 0), patterns: [ Pattern.createEmpty({ "name": "01-atoms-05-alerts-00-error", @@ -422,6 +460,7 @@ tap.test('find_lineage - finds lineage with unspaced styleModifier', function (t "lineageRIndex": [] }); var patternlab = { + graph: PatternGraph.empty(), patterns: [ Pattern.createEmpty({ "name": "01-atoms-05-alerts-00-error", @@ -483,6 +522,7 @@ tap.test('find_lineage - finds lineage with fuzzy partial with styleModifier', f "lineageRIndex": [] }); var patternlab = { + graph: PatternGraph.empty(), patterns: [ Pattern.createEmpty({ "name": "01-atoms-05-alerts-00-error", @@ -530,6 +570,7 @@ tap.test('find_lineage - does not apply lineage twice', function (test) { "extendedTemplate": "{{>atoms-error(message: 'That\\'s no moon...')}}" }); var patternlab = { + graph: PatternGraph.empty(), patterns: [ Pattern.createEmpty({ "name": "01-atoms-05-alerts-00-error", @@ -569,5 +610,14 @@ tap.test('find_lineage - does not apply lineage twice', function (test) { test.equals(patternlab.patterns[0].lineageRIndex.length, 1); test.equals(patternlab.patterns[0].lineageR[0].lineagePattern, 'molecules-error'); + var graph = patternlab.graph; + + var currentPatternLineageIndex = graph.lineageIndex(currentPattern); + test.equals(currentPatternLineageIndex.length, 1); + test.equals(currentPatternLineageIndex[0], "atoms-error"); + var patternZeroLineageR = graph.lineageR(patternlab.patterns[0]); + test.equals(patternZeroLineageR.length, 1); + test.equals(patternZeroLineageR[0].patternPartial, 'molecules-error'); + test.end(); }); diff --git a/test/list_item_hunter_tests.js b/test/list_item_hunter_tests.js index 1add24827..74b21cd74 100644 --- a/test/list_item_hunter_tests.js +++ b/test/list_item_hunter_tests.js @@ -4,6 +4,7 @@ var tap = require('tap'); var lih = require('../core/lib/list_item_hunter'); var Pattern = require('../core/lib/object_factory').Pattern; +var PatternGraph = require('../core/lib/pattern_graph').PatternGraph; var extend = require('util')._extend; var pa = require('../core/lib/pattern_assembler'); var pattern_assembler = new pa(); @@ -24,6 +25,7 @@ function createFakePatternLab(customProps) { //NOTE: These listitems are faked so that pattern_assembler.combine_listitems has already clobbered them. var pl = { + graph: PatternGraph.empty(), "listitems": { "1": [ { diff --git a/test/pattern_assembler_tests.js b/test/pattern_assembler_tests.js index 6f9bafa31..e3dc5bdfe 100644 --- a/test/pattern_assembler_tests.js +++ b/test/pattern_assembler_tests.js @@ -4,9 +4,15 @@ var tap = require('tap'); var pa = require('../core/lib/pattern_assembler'); var Pattern = require('../core/lib/object_factory').Pattern; +var CompileState = require('../core/lib/object_factory').CompileState; +var PatternGraph = require('../core/lib/pattern_graph').PatternGraph; var path = require('path'); - +function emptyPatternLab() { + return { + graph: PatternGraph.empty() + } +} tap.test('process_pattern_recursive recursively includes partials', function(test) { @@ -18,9 +24,11 @@ tap.test('process_pattern_recursive recursively includes partials', function(tes var plMain = require('../core/lib/patternlab'); var pattern_assembler = new pa(); var patterns_dir = './test/files/_patterns'; - var patternlab = {}; + var public_dir = './test/public'; + var patternlab = emptyPatternLab(); patternlab.config = fs.readJSONSync('./patternlab-config.json'); patternlab.config.paths.source.patterns = patterns_dir; + patternlab.config.paths.public = public_dir; patternlab.config.outputFileSuffixes = {rendered: ''}; //patternlab.data = fs.readJSONSync(path.resolve(patternlab.config.paths.source.data, 'data.json')); @@ -70,7 +78,7 @@ tap.test('processPatternRecursive - correctly replaces all stylemodifiers when m var pattern_assembler = new pa(); var patterns_dir = './test/files/_patterns'; - var pl = {}; + var pl = emptyPatternLab(); pl.config = { paths: { source: { @@ -114,7 +122,7 @@ tap.test('processPatternRecursive - correctly replaces multiple stylemodifier cl var pattern_assembler = new pa(); var patterns_dir = './test/files/_patterns'; - var pl = {}; + var pl = emptyPatternLab(); pl.config = { paths: { source: { @@ -159,7 +167,7 @@ tap.test('processPatternRecursive - correctly ignores a partial without a style var pattern_assembler = new pa(); var patterns_dir = './test/files/_patterns'; - var pl = {}; + var pl = emptyPatternLab(); pl.config = { paths: { source: { @@ -202,7 +210,7 @@ tap.test('processPatternRecursive - correctly ignores bookended partials without var pattern_assembler = new pa(); var patterns_dir = './test/files/_patterns'; - var pl = {}; + var pl = emptyPatternLab(); pl.config = { paths: { source: { @@ -246,7 +254,7 @@ tap.test('processPatternRecursive - correctly ignores a partial without a style var pattern_assembler = new pa(); var patterns_dir = './test/files/_patterns'; - var pl = {}; + var pl = emptyPatternLab(); pl.config = { paths: { source: { @@ -291,7 +299,7 @@ tap.test('processPatternRecursive - correctly ignores bookended partials without var pattern_assembler = new pa(); var patterns_dir = './test/files/_patterns'; - var pl = {}; + var pl = emptyPatternLab(); pl.config = { paths: { source: { @@ -336,7 +344,7 @@ tap.test('processPatternRecursive - does not pollute previous patterns when a la var pattern_assembler = new pa(); var patterns_dir = './test/files/_patterns'; - var pl = {}; + var pl = emptyPatternLab(); pl.config = { paths: { source: { @@ -390,7 +398,7 @@ tap.test('processPatternRecursive - ensure deep-nesting works', function(test) { var pattern_assembler = new pa(); var patterns_dir = './test/files/_patterns'; - var pl = {}; + var pl = emptyPatternLab(); pl.config = { paths: { source: { @@ -498,7 +506,7 @@ tap.test('parseDataLinks - replaces found link.* data for their expanded links', var plMain = require('../core/lib/patternlab'); var pattern_assembler = new pa(); var patterns_dir = './test/files/_patterns/'; - var patternlab = {}; + var patternlab = emptyPatternLab(); //THIS IS BAD patternlab.config = fs.readJSONSync('./patternlab-config.json'); patternlab.config.paths.source.patterns = patterns_dir; @@ -567,7 +575,7 @@ tap.test('parseDataLinks - replaces found link.* data for their expanded links', tap.test('get_pattern_by_key - returns the fuzzy result when no others found', function(test) { //arrange var pattern_assembler = new pa(); - var patternlab = {}; + var patternlab = emptyPatternLab();; patternlab.patterns = []; patternlab.patterns.push({ @@ -586,7 +594,7 @@ tap.test('get_pattern_by_key - returns the fuzzy result when no others found', f tap.test('get_pattern_by_key - returns the exact key if found', function(test) { //arrange var pattern_assembler = new pa(); - var patternlab = {}; + var patternlab = emptyPatternLab();; patternlab.patterns = []; patternlab.patterns.push({ @@ -609,7 +617,7 @@ tap.test('get_pattern_by_key - returns the exact key if found', function(test) { tap.test('addPattern - adds pattern extended template to patternlab partial object', function(test) { //arrange var pattern_assembler = new pa(); - var patternlab = {}; + var patternlab = emptyPatternLab(); patternlab.patterns = []; patternlab.partials = {}; patternlab.data = {link: {}}; @@ -633,7 +641,7 @@ tap.test('addPattern - adds pattern extended template to patternlab partial obje tap.test('addPattern - adds pattern template to patternlab partial object if extendedtemplate does not exist yet', function(test){ //arrange var pattern_assembler = new pa(); - var patternlab = {}; + var patternlab = emptyPatternLab(); patternlab.patterns = []; patternlab.partials = {}; patternlab.data = {link: {}}; @@ -654,6 +662,78 @@ tap.test('addPattern - adds pattern template to patternlab partial object if ext test.end(); }); +tap.test('findModifiedPatterns - finds patterns modified since a given date', function(test){ + //arrange + var pattern_assembler = new pa(); + var patternlab = emptyPatternLab(); + patternlab.partials = {}; + patternlab.data = {link: {}}; + patternlab.config = { debug: false }; + patternlab.config.outputFileSuffixes = {rendered : ''}; + + var pattern = new Pattern('00-test/01-bar.mustache'); + pattern.extendedTemplate = undefined; + pattern.template = 'bar'; + pattern.lastModified = new Date("2016-01-31").getTime(); + // Initially the compileState is clean, + // but we would change this after detecting that the file was modified + pattern.compileState = CompileState.CLEAN; + patternlab.patterns = [pattern]; + + var lastCompilationRun = new Date("2016-01-01").getTime(); + var p = pattern_assembler.find_modified_patterns(lastCompilationRun, patternlab); + + test.same(p.length, 1, "The pattern was modified after the last compilation"); + + lastCompilationRun = new Date("2016-12-31").getTime(); + p = pattern_assembler.find_modified_patterns(lastCompilationRun, patternlab); + test.same(p.length, 0, "Pattern was already compiled and hasn't been modified since last compile"); + test.end(); +}) + +tap.test('findModifiedPatterns - finds patterns when modification date is missing', function(test){ + //arrange + var pattern_assembler = new pa(); + var patternlab = emptyPatternLab(); + patternlab.partials = {}; + patternlab.data = {link: {}}; + patternlab.config = { debug: false }; + patternlab.config.outputFileSuffixes = {rendered : ''}; + + var pattern = new Pattern('00-test/01-bar.mustache'); + pattern.extendedTemplate = undefined; + pattern.template = 'bar'; + pattern.lastModified = undefined; + patternlab.patterns = [pattern]; + + let p = pattern_assembler.find_modified_patterns(1000, patternlab); + test.same(p.length, 1); + test.end(); +}); + +// This is the case when we want to force recompilation +tap.test('findModifiedPatterns - finds patterns via compile state', function(test){ + //arrange + var pattern_assembler = new pa(); + var patternlab = emptyPatternLab(); + patternlab.partials = {}; + patternlab.data = {link: {}}; + patternlab.config = { debug: false }; + patternlab.config.outputFileSuffixes = {rendered : ''}; + + var pattern = new Pattern('00-test/01-bar.mustache'); + pattern.extendedTemplate = undefined; + pattern.template = 'bar'; + pattern.lastModified = 100000; + pattern.compileState = CompileState.NEEDS_REBUILD; + patternlab.patterns = [pattern]; + + let p = pattern_assembler.find_modified_patterns(1000, patternlab); + test.same(p.length, 1); + test.end(); +}); + + tap.test('hidden patterns can be called by their nice names', function(test){ var util = require('./util/test_utils.js'); diff --git a/test/pattern_graph_tests.js b/test/pattern_graph_tests.js new file mode 100644 index 000000000..a1ced6bd2 --- /dev/null +++ b/test/pattern_graph_tests.js @@ -0,0 +1,393 @@ +"use strict"; + + +var PatternGraph = require('../core/lib/pattern_graph').PatternGraph; +var VERSION = require('../core/lib/pattern_graph').PATTERN_GRAPH_VERSION; +var Pattern = require('../core/lib/object_factory').Pattern; +var CompileState = require('../core/lib/object_factory').CompileState; +var tap = require('tap'); + +var patternlab = { + config: { + paths: { + public: { + root: "./test/public" + } + } + } +}; + +var mockGraph = function () { + return PatternGraph.empty(); +}; + +tap.test("checkVersion - Current version returns true", (test) => { + test.same(PatternGraph.checkVersion({"version": VERSION}), true); + test.end(); +}); + +tap.test("checkVersion - Older version returns false", (test) => { + test.same(PatternGraph.checkVersion({"version": VERSION - 1}), false); + test.end(); +}); + +tap.test("Loading an empty graph works", (test) => { + var g = PatternGraph.loadFromFile(patternlab, "does not exist"); + tap.equal(g.graph.nodes().length, 0,"foo"); + test.end(); +}); + +tap.test("PatternGraph.fromJson() - Loading a graph from JSON", (test) => { + var graph = PatternGraph.loadFromFile(patternlab, "testDependencyGraph.json"); + test.same(graph.timestamp, 1337); + test.same(graph.graph.nodes(), ["atom-foo", "molecule-foo"]); + test.same(graph.graph.edges(), [ {v:"molecule-foo", w: "atom-foo"}]); + test.end(); +}); + +tap.test("PatternGraph.fromJson() - Loading a graph from JSON using an older version throws error", (test) => { + test.throws(function () { + PatternGraph.fromJson({version: 0}); + }, {}, /Version of graph on disk.*/g); + + test.end(); +}); + +tap.test("toJson() - Storing a graph to JSON correctly", (test) => { + var graph = mockGraph(); + graph.timestamp = 1337; + var atomFoo = Pattern.create("atom-foo", null , {compileState:CompileState.CLEAN}); + var moleculeFoo = Pattern.create("molecule-foo", null, {compileState:CompileState.CLEAN}); + graph.add(atomFoo); + graph.add(moleculeFoo); + graph.link(moleculeFoo, atomFoo); + test.same(graph.toJson(), + {"version": VERSION, "timestamp":1337,"graph":{"options":{"directed":true,"multigraph":false,"compound":false},"nodes":[{"v":"atom-foo","value":{"compileState":"clean"}},{"v":"molecule-foo","value":{"compileState":"clean"}}],"edges":[{"v":"molecule-foo","w":"atom-foo","value":{}}]}}); + // For generating the above output:console.log(JSON.stringify(graph.toJson())); + test.end(); +}); + +tap.test("Storing and loading a graph from JSON return the identical graph", (test) => { + var oldGraph = mockGraph(); + oldGraph.timestamp = 1337; + var atomFoo = Pattern.create("atom-foo", null , {compileState:CompileState.CLEAN}); + var moleculeFoo = Pattern.create("molecule-foo", null, {compileState:CompileState.CLEAN}); + oldGraph.add(atomFoo); + oldGraph.add(moleculeFoo); + oldGraph.link(moleculeFoo, atomFoo); + + // act + var newGraph = PatternGraph.fromJson(oldGraph.toJson()); + + // assert + test.same(newGraph.version, VERSION); + test.same(newGraph.timestamp, 1337); + test.same(newGraph.graph.nodes(), ["atom-foo", "molecule-foo"]); + test.same(newGraph.graph.edges(), [ {w: "atom-foo", v:"molecule-foo"}]); + // The graph is a new object + test.notEqual(newGraph, oldGraph); + test.end(); +}); + + +tap.test("clone()", (test) => { + var oldGraph = mockGraph(); + oldGraph.timestamp = 1337; + var atomFoo = Pattern.create("atom-foo", null , {compileState:CompileState.CLEAN}); + var moleculeFoo = Pattern.create("molecule-foo", null, {compileState:CompileState.CLEAN}); + oldGraph.add(atomFoo); + oldGraph.add(moleculeFoo); + oldGraph.link(moleculeFoo, atomFoo); + + // act + var newGraph = oldGraph.clone(); + + // assert + test.same(newGraph.version, VERSION); + test.same(newGraph.timestamp, 1337); + test.same(newGraph.graph.nodes(), ["atom-foo", "molecule-foo"]); + test.same(newGraph.graph.edges(), [ {w: "atom-foo", v:"molecule-foo"}]); + // The graph is a new object + test.notEqual(newGraph, oldGraph); + test.end(); +}); + +tap.test("Adding a node", (test) => { + var g = mockGraph(); + var pattern = Pattern.create("atom-foo", null, {compileState:CompileState.CLEAN}); + g.add(pattern); + test.same({compileState:CompileState.CLEAN}, g.node("atom-foo"), "Data were set correctly"); + var actual = g.nodes(); + test.same(actual, ["atom-foo"]); + test.end(); +}); + +tap.test("Adding a node twice", (test) => { + var g = mockGraph(); + var pattern = Pattern.create("atom-foo", null, {compileState:CompileState.CLEAN}); + g.add(pattern); + g.add(pattern); + var actual = g.nodes(); + test.same(actual, ["atom-foo"]); + test.end(); +}); + +tap.test("Adding two nodes", (test) => { + var g = mockGraph(); + var atomFoo = Pattern.create("atom-foo", {compileState:CompileState.CLEAN}); + var moleculeFoo = Pattern.create("molecule-foo", {compileState:CompileState.CLEAN}); + g.add(atomFoo); + g.add(moleculeFoo); + var actual = g.nodes(); + test.same(actual, ["atom-foo", "molecule-foo"]); + test.end(); +}); + +tap.test("Adding two nodes with only different subpattern types", (test) => { + var g = mockGraph(); + var atomFoo = Pattern.create("00-atoms/00-foo/baz.html", {compileState:CompileState.CLEAN}); + var moleculeFoo = Pattern.create("00-atoms/00-bar/baz.html", {compileState:CompileState.CLEAN}); + g.add(atomFoo); + g.add(moleculeFoo); + var actual = g.nodes(); + test.same(actual, ["00-atoms/00-foo/baz.html", "00-atoms/00-bar/baz.html"]); + test.end(); +}); + +tap.test("Linking two nodes", (test) => { + var g = mockGraph(); + var atomFoo = Pattern.create("atom-foo", null , {compileState:CompileState.CLEAN}); + var moleculeFoo = Pattern.create("molecule-foo", null, {compileState:CompileState.CLEAN}); + g.add(atomFoo); + g.add(moleculeFoo); + g.link(moleculeFoo, atomFoo); + test.same(g.graph.edges(), [{v:"molecule-foo",w:"atom-foo"}], "There is an edge from v to w"); + test.end(); +}); + + +tap.test("remove() - Removing a node", (test) => { + var g = mockGraph(); + var atomFoo = Pattern.create("atom-foo", null , {compileState:CompileState.CLEAN}); + var moleculeFoo = Pattern.create("molecule-foo", null, {compileState:CompileState.CLEAN}); + g.add(atomFoo); + g.add(moleculeFoo); + test.same(g.nodes(), ["atom-foo", "molecule-foo"]); + g.remove(moleculeFoo); + test.same(g.graph.nodes(), ["atom-foo"], "The molecule was removed from the graph"); + test.same(g.patterns.allPatterns()[0].relPath, "atom-foo", "The molecule was removed from the known patterns"); + test.end(); +}); + +tap.test("filter() - Removing nodes via filter", (test) => { + var g = mockGraph(); + var atomFoo = Pattern.create("atom-foo", null , {compileState:CompileState.CLEAN}); + var moleculeFoo = Pattern.create("molecule-foo", null, {compileState:CompileState.CLEAN}); + g.add(atomFoo); + g.add(moleculeFoo); + test.same(g.nodes(), ["atom-foo", "molecule-foo"]); + g.filter(n => n != "molecule-foo"); + test.same(g.graph.nodes(), ["atom-foo"], "The molecule was removed from the graph"); + test.same(g.patterns.allPatterns()[0].relPath, "atom-foo", "The molecule was removed from the known patterns"); + test.end(); +}); + +// Prevents nodes from escaping the scope, at the same time have some default graph for lineage to +// test on +(function () { + var atomFoo = Pattern.create("00-atom/xy/foo", null , {compileState:CompileState.CLEAN}); + var atomIsolated = Pattern.create("00-atom/xy/isolated", null , {compileState:CompileState.CLEAN}); + var moleculeFoo = Pattern.create("01-molecule/xy/foo", null, {compileState:CompileState.CLEAN}); + var moleculeBar = Pattern.create("01-molecule/xy/bar", null, {compileState:CompileState.CLEAN}); + var organismFoo = Pattern.create("02-organism/xy/foo", null, {compileState:CompileState.CLEAN}); + var organismBar = Pattern.create("02-organism/xy/bar", null, {compileState:CompileState.CLEAN}); + + var g = mockGraph(); + + // Included nowhere + g.add(atomIsolated); + + g.add(atomFoo); + g.add(moleculeFoo); + g.add(moleculeBar); + g.add(organismFoo); + g.add(organismBar); + // single molecule + g.link(organismFoo, moleculeFoo); + + // include two molecules + g.link(organismBar, moleculeFoo); + g.link(organismBar, moleculeBar); + + // both include atomFoo + g.link(moleculeFoo, atomFoo); + g.link(moleculeBar, atomFoo); + + tap.test("lineage() - Calculate the lineage of a node", (test) => { + test.same(g.lineage(organismFoo).map(p => p.relPath), ["01-molecule/xy/foo"]); + test.same(g.lineage(organismBar).map(p => p.relPath), ["01-molecule/xy/foo","01-molecule/xy/bar"]); + test.same(g.lineage(moleculeFoo).map(p => p.relPath), ["00-atom/xy/foo"]); + test.same(g.lineage(moleculeBar).map(p => p.relPath), ["00-atom/xy/foo"]); + test.same(g.lineage(atomFoo).map(p => p.relPath), []); + test.same(g.lineage(atomIsolated).map(p => p.relPath), []); + test.end(); + }); + + tap.test("lineageIndex() - Calculate the lineage of a node", (test) => { + test.same(g.lineageIndex(organismFoo), ["molecule-foo"]); + test.same(g.lineageIndex(organismBar), ["molecule-foo", "molecule-bar"]); + test.same(g.lineageIndex(moleculeFoo), ["atom-foo"]); + test.same(g.lineageIndex(moleculeBar), ["atom-foo"]); + test.same(g.lineageIndex(atomFoo), []); + test.same(g.lineageIndex(atomIsolated), []); + test.end(); + }); + + tap.test("lineageR() - Calculate the reverse lineage of a node", (test) => { + test.same(g.lineageR(organismFoo).map(p => p.relPath), []); + test.same(g.lineageR(organismBar).map(p => p.relPath), []); + test.same(g.lineageR(moleculeFoo).map(p => p.relPath), ["02-organism/xy/foo", "02-organism/xy/bar"]); + test.same(g.lineageR(moleculeBar).map(p => p.relPath), ["02-organism/xy/bar"]); + test.same(g.lineageR(atomFoo).map(p => p.relPath), ["01-molecule/xy/foo", "01-molecule/xy/bar"]); + test.same(g.lineageR(atomIsolated).map(p => p.relPath), []); + test.end(); + }); + + tap.test("lineageRIndex() - Calculate the lineage index of a node", (test) => { + test.same(g.lineageRIndex(organismFoo), []); + test.same(g.lineageRIndex(organismBar), []); + test.same(g.lineageRIndex(moleculeFoo), ["organism-foo", "organism-bar"]); + test.same(g.lineageRIndex(moleculeBar), ["organism-bar"]); + test.same(g.lineageRIndex(atomFoo), ["molecule-foo", "molecule-bar"]); + test.same(g.lineageRIndex(atomIsolated), []); + test.end(); + }); +})(); + +(function () { + + function TestGraph() { + + function csAt(args, idx) { + return {compileState: args[idx] || CompileState.CLEAN} + } + var i = 0; + var atomFoo = this.atomFoo = Pattern.create("00-atom/xy/foo", null, csAt(arguments,i++)); + var atomIsolated = this.atomIsolated = Pattern.create("00-atom/xy/isolated", null, csAt(arguments,i++)); + var moleculeFoo = this.moleculeFoo = Pattern.create("01-molecule/xy/foo", null, csAt(arguments,i++)); + var moleculeBar = this.moleculeBar = Pattern.create("01-molecule/xy/bar", null, csAt(arguments,i++)); + var organismFoo = this.organismFoo = Pattern.create("02-organism/xy/foo", null, csAt(arguments,i++)); + var organismBar = this.organismBar = Pattern.create("02-organism/xy/bar", null, csAt(arguments,i++)); + + var graph = this.graph = mockGraph(); + + // Included nowhere + graph.add(atomIsolated); + + graph.add(atomFoo); + graph.add(moleculeFoo); + graph.add(moleculeBar); + graph.add(organismFoo); + graph.add(organismBar); + // single molecule + graph.link(organismFoo, moleculeFoo); + + // include two molecules + graph.link(organismBar, moleculeFoo); + graph.link(organismBar, moleculeBar); + + // both include atomFoo + graph.link(moleculeFoo, atomFoo); + graph.link(moleculeBar, atomFoo); + + + } + + tap.test("compileOrder() - A clean graph results in no nodes to recompile", (test) => { + var g = new TestGraph(); + var co = g.graph.compileOrder(); + test.equals(0, co.length); + test.end(); + }); + + tap.test("compileOrder() - Recompile isolated atoms does not do anything else", (test) => { + var g = new TestGraph( + // atomFoo + CompileState.CLEAN, + // atomIsolated + CompileState.NEEDS_REBUILD); + + var co = g.graph.compileOrder(); + test.same([g.atomIsolated], co, "Only recompile atomIsolated"); + co.forEach( p=> test.same(p.compileState, CompileState.NEEDS_REBUILD, + "All patterns are marked for rebuilding")); + + test.end(); + }); + + tap.test("compileOrder() - Changing a linked atom bubbles back to the organisms", (test) => { + // Almost every pattern - except atomIsolated - has a transitive dependency on atomFoo + var g = new TestGraph(CompileState.NEEDS_REBUILD); + var co = g.graph.compileOrder(); + + test.equals(5, co.length); + test.same([g.atomFoo, g.moleculeFoo, g.organismFoo, g.moleculeBar, g.organismBar], co, + "Recompile everything except atomIsolated"); + co.forEach( p=> test.same(p.compileState, CompileState.NEEDS_REBUILD, + "All patterns are marked for rebuilding")); + + test.end(); + }); + + + tap.test("compileOrder() - Changing a molecule leaves atoms untouched", (test) => { + // Bubble up from molecules to organisms, leaving atoms unchanged as they were not modified + var g = new TestGraph(null, null, CompileState.NEEDS_REBUILD); + var co = g.graph.compileOrder(); + + test.same([g.moleculeFoo, g.organismFoo, g.organismBar], co, + "Recompile moleculeFoo and transitive dependencies"); + co.forEach( p=> test.same(p.compileState, CompileState.NEEDS_REBUILD, + "All patterns are marked for rebuilding")); + test.end(); + }); + + tap.test("compileOrder() - Changing an organism leaves atoms and molecules untouched", (test) => { + // Almost every pattern - except atomIsolated - has a transitive dependency on atomFoo + var g = new TestGraph( + // atoms + null, null, + // molecules + null, null, + // organismFoo + CompileState.NEEDS_REBUILD); + var co = g.graph.compileOrder(); + + test.same([g.organismFoo], co); + test.same(co[0].compileState, CompileState.NEEDS_REBUILD, + "All patterns are marked for rebuilding"); + test.end(); + }); + + + tap.test("compileOrder() - Recompile everything", (test) => { + // Almost every pattern - except atomIsolated - has a transitive dependency on atomFoo + // Also recompile atomIsolated + var g = new TestGraph( CompileState.NEEDS_REBUILD, CompileState.NEEDS_REBUILD); + var compileOrder = g.graph.compileOrder(); + + test.same([ + g.atomIsolated, + g.atomFoo, + g.moleculeFoo, + g.organismFoo, + g.moleculeBar, + g.organismBar + ], + compileOrder, + "Recompile everything except atomIsolated"); + compileOrder.forEach( p=> test.same(p.compileState, CompileState.NEEDS_REBUILD, + "All patterns are marked for rebuilding")); + test.end(); + }); +})(); diff --git a/test/pattern_registry_tests.js b/test/pattern_registry_tests.js new file mode 100644 index 000000000..60bf87644 --- /dev/null +++ b/test/pattern_registry_tests.js @@ -0,0 +1,66 @@ +"use strict"; + +var PatternRegistry = require('./../core/lib/pattern_registry'); + +var tap = require('tap'); + +// #540 Copied from pattern_assembler_tests +tap.test('get_pattern_by_key - returns the fuzzy result when no others found', function(test) { + var pattern_registry = new PatternRegistry(); + + var pattern = { + key: 'character-han-solo', + patternPartial: 'character-han-solo', + subdir: 'character', + fileName: 'han-solo' + }; + pattern_registry.put(pattern); + + //act + var result = pattern_registry.getPartial('character-han'); + //assert + test.equals(result, pattern); + test.end(); +}); + +// #540 Copied from pattern_assembler_tests +tap.test('remove - remove an existing pattern', function(test) { + var pattern_registry = new PatternRegistry(); + + var pattern = { + key: 'character-han-solo', + patternPartial: 'character-han-solo', + subdir: 'character', + fileName: 'han-solo' + }; + pattern_registry.put(pattern); + + //act + pattern_registry.remove('character-han-solo'); + test.same(null, pattern_registry.get('character-han-solo')); + test.end(); +}); + +// #540 Copied from pattern_assembler_tests +tap.test('getPartial - returns the exact key if found', function(test) { + //arrange + var pattern_registry = new PatternRegistry(); + let patterns = [{ + key: 'molecules-primary-nav-jagged', + patternPartial: 'molecules-primary-nav-jagged', + subdir: 'molecules', + fileName: 'primary-nav-jagged' + }, { + key: 'molecules-primary-nav', + patternPartial: 'molecules-primary-nav', + subdir: 'molecules', + fileName: 'molecules-primary-nav' + }]; + patterns.forEach((p) => pattern_registry.put(p)); + + //act + var result = pattern_registry.getPartial('molecules-primary-nav'); + //assert + test.equals(result, patterns[1]); + test.end(); +}); diff --git a/test/pseudopattern_hunter_tests.js b/test/pseudopattern_hunter_tests.js index 80bce5b61..007138475 100644 --- a/test/pseudopattern_hunter_tests.js +++ b/test/pseudopattern_hunter_tests.js @@ -6,18 +6,24 @@ var path = require('path'); var pha = require('../core/lib/pseudopattern_hunter'); var pa = require('../core/lib/pattern_assembler'); var Pattern = require('../core/lib/object_factory').Pattern; +var PatternGraph = require('../core/lib/pattern_graph').PatternGraph; var fs = require('fs-extra'); var pattern_assembler = new pa(); var pseudopattern_hunter = new pha(); var patterns_dir = './test/files/_patterns/'; +var public_patterns_dir = './test/public/patterns'; function stubPatternlab() { var pl = {}; + pl.graph = PatternGraph.empty(); pl.config = { paths: { source: { patterns: patterns_dir + }, + public: { + patterns: public_patterns_dir } } }; @@ -27,7 +33,7 @@ function stubPatternlab() { pl.patterns = []; pl.partials = {}; pl.config.patternStates = {}; - pl.config.outputFileSuffixes = { rendered: ''} + pl.config.outputFileSuffixes = { rendered: ''}; return pl; } diff --git a/test/public/testDependencyGraph.json b/test/public/testDependencyGraph.json new file mode 100644 index 000000000..47d86ede1 --- /dev/null +++ b/test/public/testDependencyGraph.json @@ -0,0 +1 @@ +{"version":1,"timestamp":1337,"graph":{"options":{"directed":true,"multigraph":false,"compound":false},"nodes":[{"v":"atom-foo","value":{"compileState":"clean"}},{"v":"molecule-foo","value":{"compileState":"clean"}}],"edges":[{"v":"molecule-foo","w":"atom-foo","value":{}}]}} diff --git a/test/ui_builder_tests.js b/test/ui_builder_tests.js index b140452dd..38137c692 100644 --- a/test/ui_builder_tests.js +++ b/test/ui_builder_tests.js @@ -2,6 +2,7 @@ var tap = require('tap'); var rewire = require("rewire"); +var _ = require('lodash'); var eol = require('os').EOL; var Pattern = require('../core/lib/object_factory').Pattern; var extend = require('util')._extend; @@ -120,8 +121,8 @@ tap.test('groupPatterns - creates pattern groups correctly', function (test) { }); patternlab.patterns.push( - new Pattern('00-test/foo.mustache'), new Pattern('00-test/bar.mustache'), + new Pattern('00-test/foo.mustache'), new Pattern('patternType1/patternSubType1/blue.mustache'), new Pattern('patternType1/patternSubType1/red.mustache'), new Pattern('patternType1/patternSubType1/yellow.mustache'), @@ -134,7 +135,6 @@ tap.test('groupPatterns - creates pattern groups correctly', function (test) { //act var result = ui.groupPatterns(patternlab); - //assert test.equals(result.patternGroups.patternType1.patternSubType1.blue.patternPartial, 'patternType1-blue'); test.equals(result.patternGroups.patternType1.patternSubType1.red.patternPartial, 'patternType1-red'); test.equals(result.patternGroups.patternType1.patternSubType1.yellow.patternPartial, 'patternType1-yellow'); @@ -142,14 +142,113 @@ tap.test('groupPatterns - creates pattern groups correctly', function (test) { test.equals(result.patternGroups.patternType1.patternSubType2.grey.patternPartial, 'patternType1-grey'); test.equals(result.patternGroups.patternType1.patternSubType2.white.patternPartial, 'patternType1-white'); - test.equals(patternlab.patternTypes[0].patternItems[0].patternPartial, 'test-bar'); - test.equals(patternlab.patternTypes[0].patternItems[1].patternPartial, 'test-foo'); + test.equals(patternlab.patternTypes[0].patternItems[0].patternPartial, 'test-bar', 'first pattern item should be test-bar'); + test.equals(patternlab.patternTypes[0].patternItems[1].patternPartial, 'test-foo', 'second pattern item should be test-foo'); //todo: patternlab.patternTypes[0].patternItems[1] looks malformed test.end(); }); +tap.test('groupPatterns - orders patterns when provided from md', function (test) { + //arrange + var patternlab = createFakePatternLab({ + patterns: [], + patternGroups: {}, + subtypePatterns: {} + }); + + patternlab.patterns.push( + new Pattern('patternType1/patternSubType1/blue.mustache'), + new Pattern('patternType1/patternSubType1/red.mustache'), + new Pattern('patternType1/patternSubType1/yellow.mustache') + ); + ui.resetUIBuilderState(patternlab); + + patternlab.patterns[1].order = 1; + + //act + ui.groupPatterns(patternlab); + + let patternType = _.find(patternlab.patternTypes, ['patternType', 'patternType1']); + let patternSubType = _.find(patternType.patternTypeItems, ['patternSubtype', 'patternSubType1']); + var items = patternSubType.patternSubtypeItems; + + //zero is viewall + test.equals(items[1].patternPartial, 'patternType1-red'); + test.equals(items[2].patternPartial, 'patternType1-blue'); + test.equals(items[3].patternPartial, 'patternType1-yellow'); + + test.end(); +}); + +tap.test('groupPatterns - retains pattern order from name when order provided from md is malformed', function (test) { + //arrange + var patternlab = createFakePatternLab({ + patterns: [], + patternGroups: {}, + subtypePatterns: {} + }); + + patternlab.patterns.push( + new Pattern('patternType1/patternSubType1/blue.mustache'), + new Pattern('patternType1/patternSubType1/red.mustache'), + new Pattern('patternType1/patternSubType1/yellow.mustache') + ); + ui.resetUIBuilderState(patternlab); + + patternlab.patterns[1].order = 'notanumber!'; + + //act + ui.groupPatterns(patternlab); + + let patternType = _.find(patternlab.patternTypes, ['patternType', 'patternType1']); + let patternSubType = _.find(patternType.patternTypeItems, ['patternSubtype', 'patternSubType1']); + var items = patternSubType.patternSubtypeItems; + + //zero is viewall + test.equals(items[1].patternPartial, 'patternType1-blue'); + test.equals(items[2].patternPartial, 'patternType1-red'); + test.equals(items[3].patternPartial, 'patternType1-yellow'); + + test.end(); +}); + +tap.test('groupPatterns - sorts viewall subtype pattern to the beginning', function (test) { + //arrange + var patternlab = createFakePatternLab({ + patterns: [], + patternGroups: {}, + subtypePatterns: {} + }); + + patternlab.patterns.push( + new Pattern('patternType1/patternSubType1/blue.mustache'), + new Pattern('patternType1/patternSubType1/red.mustache'), + new Pattern('patternType1/patternSubType1/yellow.mustache') + ); + ui.resetUIBuilderState(patternlab); + + patternlab.patterns[0].order = 1; + patternlab.patterns[1].order = 3; + patternlab.patterns[2].order = 2; + + //act + ui.groupPatterns(patternlab); + + let patternType = _.find(patternlab.patternTypes, ['patternType', 'patternType1']); + let patternSubType = _.find(patternType.patternTypeItems, ['patternSubtype', 'patternSubType1']); + var items = patternSubType.patternSubtypeItems; + + //zero is viewall + test.equals(items[0].patternPartial, 'viewall-patternType1-patternSubType1'); + test.equals(items[1].patternPartial, 'patternType1-blue'); + test.equals(items[2].patternPartial, 'patternType1-yellow'); + test.equals(items[3].patternPartial, 'patternType1-red'); + + test.end(); +}); + tap.test('groupPatterns - creates documentation patterns for each type and subtype if not exists', function (test) { //arrange var patternlab = createFakePatternLab({ diff --git a/test/util/test_utils.js b/test/util/test_utils.js index 652c52d5e..3f72ee826 100644 --- a/test/util/test_utils.js +++ b/test/util/test_utils.js @@ -1,5 +1,7 @@ "use strict"; +var PatternGraph = require('./../../core/lib/pattern_graph').PatternGraph; + module.exports = { // fake pattern lab constructor: @@ -7,6 +9,7 @@ module.exports = { // apparatus. fakePatternLab: (testPatternsPath) => { var fpl = { + graph: PatternGraph.empty(), partials: {}, patterns: [], footer: '',