Skip to content

Commit b0bde03

Browse files
Merge pull request #569 from pattern-lab/features/incremental-build
Features/incremental build
2 parents abddf62 + 967e768 commit b0bde03

25 files changed

+1623
-148
lines changed

.eslintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"env": {
33
"node": true,
4-
"builtin": true
4+
"builtin": true,
5+
"es6": true
56
},
67
"parserOptions": {
78
"ecmaVersion": 6,

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ Thumbs.db
88
source/css/style.css.map
99
.idea/
1010
public
11+
!test/patterns/public/.gitkeep
12+
!test/patterns/testDependencyGraph.json

core/lib/changes_hunter.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"use strict";
2+
const fs = require("fs-extra"),
3+
CompileState = require('./object_factory').CompileState;
4+
5+
/**
6+
* For detecting changed patterns.
7+
* @constructor
8+
*/
9+
let ChangesHunter = function () {
10+
};
11+
12+
ChangesHunter.prototype = {
13+
14+
/**
15+
* Checks the build state of a pattern by comparing the modification date of the rendered output
16+
* file with the {@link Pattern.lastModified}. If the pattern was modified after the last
17+
* time it has been rendered, it is flagged for rebuilding via {@link CompileState.NEEDS_REBUILD}.
18+
*
19+
* @param {Pattern} pattern
20+
* @param patternlab
21+
*
22+
* @see {@link CompileState}
23+
*/
24+
checkBuildState: function (pattern, patternlab) {
25+
26+
//write the compiled template to the public patterns directory
27+
let renderedTemplatePath =
28+
patternlab.config.paths.public.patterns + pattern.getPatternLink(patternlab, 'rendered');
29+
30+
if (!pattern.compileState) {
31+
pattern.compileState = CompileState.NEEDS_REBUILD;
32+
}
33+
34+
try {
35+
// Prevent error message if file does not exist
36+
fs.accessSync(renderedTemplatePath, fs.F_OK);
37+
let outputLastModified = fs.statSync(renderedTemplatePath).mtime.getTime();
38+
39+
if (pattern.lastModified && outputLastModified > pattern.lastModified) {
40+
pattern.compileState = CompileState.CLEAN;
41+
}
42+
} catch (e) {
43+
// Output does not exist yet, needs recompile
44+
}
45+
46+
let node = patternlab.graph.node(pattern);
47+
48+
// Make the pattern known to the PatternGraph and remember its compileState
49+
if (!node) {
50+
patternlab.graph.add(pattern);
51+
} else {
52+
// Works via object reference, so we directly manipulate the node data here
53+
node.compileState = pattern.compileState;
54+
}
55+
56+
57+
},
58+
59+
/**
60+
* Updates {Pattern#lastModified} to the files modification date if the file was modified
61+
* after {Pattern#lastModified}.
62+
*
63+
* @param {Pattern} currentPattern
64+
* @param {string} file
65+
*/
66+
checkLastModified: function (currentPattern, file) {
67+
if (file) {
68+
try {
69+
let stat = fs.statSync(file);
70+
71+
// Needs recompile whenever one of the patterns files (template, json, pseudopatterns) changed
72+
currentPattern.lastModified =
73+
Math.max(stat.mtime.getTime(), currentPattern.lastModified || 0);
74+
} catch (e) {
75+
// Ignore, not a regular file
76+
}
77+
}
78+
}
79+
};
80+
81+
module.exports = ChangesHunter;

core/lib/lineage_hunter.js

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
"use strict";
22

3+
var extend = require("util")._extend;
4+
35
var lineage_hunter = function () {
46

57
var pa = require('./pattern_assembler');
68

79
function findlineage(pattern, patternlab) {
10+
// As we are adding edges from pattern to ancestor patterns, ensure it is known to the graph
11+
patternlab.graph.add(pattern);
812

913
var pattern_assembler = new pa();
1014

@@ -28,6 +32,11 @@ var lineage_hunter = function () {
2832
l.lineageState = ancestorPattern.patternState;
2933
}
3034

35+
patternlab.graph.add(ancestorPattern);
36+
37+
// Confusing: pattern includes "ancestorPattern", not the other way round
38+
patternlab.graph.link(pattern, ancestorPattern);
39+
3140
pattern.lineage.push(l);
3241

3342
//also, add the lineageR entry if it doesn't exist
@@ -44,62 +53,71 @@ var lineage_hunter = function () {
4453
}
4554

4655
ancestorPattern.lineageR.push(lr);
56+
extend(patternlab.graph.node(ancestorPattern), lr);
4757
}
4858
}
4959
});
5060
}
5161
}
5262

53-
function setPatternState(direction, pattern, targetPattern) {
54-
// if the request came from the past, apply target pattern state to current pattern lineage
63+
/**
64+
* Apply the target pattern state either to any predecessors or successors of the given
65+
* pattern in the pattern graph.
66+
* @param direction Either 'fromPast' or 'fromFuture'
67+
* @param pattern {Pattern}
68+
* @param targetPattern {Pattern}
69+
* @param graph {PatternGraph}
70+
*/
71+
function setPatternState(direction, pattern, targetPattern, graph) {
72+
var index = null;
5573
if (direction === 'fromPast') {
56-
for (var i = 0; i < pattern.lineageIndex.length; i++) {
57-
if (pattern.lineageIndex[i] === targetPattern.patternPartial) {
58-
pattern.lineage[i].lineageState = targetPattern.patternState;
59-
}
60-
}
74+
index = graph.lineage(pattern);
6175
} else {
62-
//the request came from the future, apply target pattern state to current pattern reverse lineage
63-
for (var i = 0; i < pattern.lineageRIndex.length; i++) {
64-
if (pattern.lineageRIndex[i] === targetPattern.patternPartial) {
65-
pattern.lineageR[i].lineageState = targetPattern.patternState;
66-
}
76+
index = graph.lineageR(pattern);
77+
}
78+
79+
// if the request came from the past, apply target pattern state to current pattern lineage
80+
for (var i = 0; i < index.length; i++) {
81+
if (index[i].patternPartial === targetPattern.patternPartial) {
82+
index[i].lineageState = targetPattern.patternState;
6783
}
6884
}
6985
}
7086

7187

7288
function cascadePatternStates(patternlab) {
7389

74-
var pattern_assembler = new pa();
75-
7690
for (var i = 0; i < patternlab.patterns.length; i++) {
7791
var pattern = patternlab.patterns[i];
7892

7993
//for each pattern with a defined state
8094
if (pattern.patternState) {
95+
var lineage = patternlab.graph.lineage(pattern);
8196

82-
if (pattern.lineageIndex && pattern.lineageIndex.length > 0) {
97+
if (lineage && lineage.length > 0) {
8398

8499
//find all lineage - patterns being consumed by this one
85-
for (var h = 0; h < pattern.lineageIndex.length; h++) {
86-
var lineagePattern = pattern_assembler.getPartial(pattern.lineageIndex[h], patternlab);
87-
setPatternState('fromFuture', lineagePattern, pattern);
100+
for (var h = 0; h < lineage.length; h++) {
101+
// Not needed, the graph already knows the concrete pattern
102+
// var lineagePattern = pattern_assembler.getPartial(lineageIndex[h], patternlab);
103+
setPatternState('fromFuture', lineage[h], pattern, patternlab.graph);
88104
}
89105
}
90-
91-
if (pattern.lineageRIndex && pattern.lineageRIndex.length > 0) {
106+
var lineageR = patternlab.graph.lineageR(pattern);
107+
if (lineageR && lineageR.length > 0) {
92108

93109
//find all reverse lineage - that is, patterns consuming this one
94-
for (var j = 0; j < pattern.lineageRIndex.length; j++) {
110+
for (var j = 0; j < lineageR.length; j++) {
95111

96-
var lineageRPattern = pattern_assembler.getPartial(pattern.lineageRIndex[j], patternlab);
112+
var lineageRPattern = lineageR[j];
97113

98114
//only set patternState if pattern.patternState "is less than" the lineageRPattern.patternstate
99115
//or if lineageRPattern.patternstate (the consuming pattern) does not have a state
100116
//this makes patternlab apply the lowest common ancestor denominator
101-
if (lineageRPattern.patternState === '' || (patternlab.config.patternStateCascade.indexOf(pattern.patternState)
102-
< patternlab.config.patternStateCascade.indexOf(lineageRPattern.patternState))) {
117+
let patternStateCascade = patternlab.config.patternStateCascade;
118+
let patternStateIndex = patternStateCascade.indexOf(pattern.patternState);
119+
let patternReverseStateIndex = patternStateCascade.indexOf(lineageRPattern.patternState);
120+
if (lineageRPattern.patternState === '' || (patternStateIndex < patternReverseStateIndex)) {
103121

104122
if (patternlab.config.debug) {
105123
console.log('Found a lower common denominator pattern state: ' + pattern.patternState + ' on ' + pattern.patternPartial + '. Setting reverse lineage pattern ' + lineageRPattern.patternPartial + ' from ' + (lineageRPattern.patternState === '' ? '<<blank>>' : lineageRPattern.patternState));
@@ -108,9 +126,9 @@ var lineage_hunter = function () {
108126
lineageRPattern.patternState = pattern.patternState;
109127

110128
//take this opportunity to overwrite the lineageRPattern's lineage state too
111-
setPatternState('fromPast', lineageRPattern, pattern);
129+
setPatternState('fromPast', lineageRPattern, pattern, patternlab.graph);
112130
} else {
113-
setPatternState('fromPast', pattern, lineageRPattern);
131+
setPatternState('fromPast', pattern, lineageRPattern, patternlab.graph);
114132
}
115133
}
116134
}

core/lib/object_factory.js

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,21 @@ var Pattern = function (relPath, data, patternlab) {
7373
this.isPseudoPattern = false;
7474
this.order = Number.MAX_SAFE_INTEGER;
7575
this.engine = patternEngines.getEngineForPattern(this);
76+
77+
/**
78+
* Determines if this pattern needs to be recompiled.
79+
*
80+
* @ee {@link CompileState}*/
81+
this.compileState = null;
82+
83+
/**
84+
* Timestamp in milliseconds when the pattern template or auxilary file (e.g. json) were modified.
85+
* If multiple files are affected, this is the timestamp of the most recent change.
86+
*
87+
* @see {@link pattern}
88+
*/
89+
this.lastModified = null;
90+
7691
};
7792

7893
// Pattern methods
@@ -141,7 +156,16 @@ Pattern.prototype = {
141156
// factory: creates an empty Pattern for miscellaneous internal use, such as
142157
// by list_item_hunter
143158
Pattern.createEmpty = function (customProps, patternlab) {
144-
var pattern = new Pattern('', null, patternlab);
159+
let relPath = '';
160+
if (customProps) {
161+
if (customProps.relPath) {
162+
relPath = customProps.relPath;
163+
} else if (customProps.subdir && customProps.filename) {
164+
relPath = customProps.subdir + path.sep + customProps.filename;
165+
}
166+
}
167+
168+
var pattern = new Pattern(relPath, null, patternlab);
145169
return extend(pattern, customProps);
146170
};
147171

@@ -153,6 +177,13 @@ Pattern.create = function (relPath, data, customProps, patternlab) {
153177
return extend(newPattern, customProps);
154178
};
155179

180+
var CompileState = {
181+
NEEDS_REBUILD: "needs rebuild",
182+
BUILDING: "building",
183+
CLEAN: "clean"
184+
};
185+
156186
module.exports = {
157-
Pattern: Pattern
187+
Pattern: Pattern,
188+
CompileState: CompileState
158189
};

core/lib/pattern_assembler.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
var path = require('path'),
44
fs = require('fs-extra'),
55
Pattern = require('./object_factory').Pattern,
6+
CompileState = require('./object_factory').CompileState,
67
pph = require('./pseudopattern_hunter'),
78
mp = require('./markdown_parser'),
89
plutils = require('./utilities'),
@@ -11,9 +12,11 @@ var path = require('path'),
1112
lih = require('./list_item_hunter'),
1213
smh = require('./style_modifier_hunter'),
1314
ph = require('./parameter_hunter'),
15+
ch = require('./changes_hunter'),
1416
JSON5 = require('json5');
1517

1618
var markdown_parser = new mp();
19+
var changes_hunter = new ch();
1720

1821
var pattern_assembler = function () {
1922
// HELPER FUNCTIONS
@@ -123,7 +126,7 @@ var pattern_assembler = function () {
123126
} else {
124127
patternlab.partials[pattern.patternPartial] = pattern.patternDesc;
125128
}
126-
129+
patternlab.graph.add(pattern);
127130
patternlab.patterns.push(pattern);
128131

129132
}
@@ -355,14 +358,22 @@ var pattern_assembler = function () {
355358
parsePatternMarkdown(currentPattern, patternlab);
356359

357360
//add the raw template to memory
358-
currentPattern.template = fs.readFileSync(path.resolve(patternsPath, relPath), 'utf8');
361+
var templatePath = path.resolve(patternsPath, currentPattern.relPath);
362+
363+
currentPattern.template = fs.readFileSync(templatePath, 'utf8');
359364

360365
//find any stylemodifiers that may be in the current pattern
361366
currentPattern.stylePartials = currentPattern.findPartialsWithStyleModifiers();
362367

363368
//find any pattern parameters that may be in the current pattern
364369
currentPattern.parameteredPartials = currentPattern.findPartialsWithPatternParameters();
365370

371+
[templatePath, jsonFilename, listJsonFileName].forEach(file => {
372+
changes_hunter.checkLastModified(currentPattern, file);
373+
});
374+
375+
changes_hunter.checkBuildState(currentPattern, patternlab);
376+
366377
//add currentPattern to patternlab.patterns array
367378
addPattern(currentPattern, patternlab);
368379

@@ -393,6 +404,15 @@ var pattern_assembler = function () {
393404
decomposePattern(currentPattern, patternlab);
394405
}
395406

407+
function findModifiedPatterns(lastModified, patternlab) {
408+
return patternlab.patterns.filter(p => {
409+
if (p.compileState !== CompileState.CLEAN || ! p.lastModified) {
410+
return true;
411+
}
412+
return p.lastModified >= lastModified;
413+
});
414+
}
415+
396416
function expandPartials(foundPatternPartials, list_item_hunter, patternlab, currentPattern) {
397417

398418
var style_modifier_hunter = new smh(),
@@ -506,6 +526,9 @@ var pattern_assembler = function () {
506526
}
507527

508528
return {
529+
find_modified_patterns: function (lastModified, patternlab) {
530+
return findModifiedPatterns(lastModified, patternlab);
531+
},
509532
find_pattern_partials: function (pattern) {
510533
return pattern.findPartials();
511534
},

0 commit comments

Comments
 (0)