UNPKG

8.11 kBJavaScriptView Raw
1/**
2 * @license MIT
3 * Copyright (c) 2016 Craig Monro (cmroanirgo)
4 **/
5
6/*
7* This is the api file for 'build'
8*/
9"use strict";
10
11var l = require('ergo-utils').log.module('ergo-api-build');
12var _ = require('ergo-utils')._;
13var fs = require('ergo-utils').fs.extend(require('fs-extra'));
14var path = require('path');
15var Promise = require('bluebird');
16var plugin_api = require('./plugin')
17var context = require('../lib/context');
18var FileInfo = require('../lib/fileinfo');
19const ignore = require('ignore');
20
21// promisify a few funcs we need
22"dirExists,ensureDir,emptyDir,emptyDir,readFile,writeFile".split(',').forEach(function(fn) {
23 fs[fn] = Promise.promisify(fs[fn])
24});
25
26function __load_ergoignoreFilter(dir) { // loads the file, if found OR returns an empty one
27 const fname = '.ergoignore';
28 return fs
29 .readFile(path.join(dir, fname ), 'utf8')
30 .catch(function(err) {
31 l.vvlog(".ergoignore not found in '"+dir+"'");
32 return ''; // ignore missing file, etc.
33 })
34 .then(function(data) {
35 if (data.length>0)
36 l.vlog("Loaded ignore file: '"+path.join(dir,fname)+"'");
37 return ignore().add([fname, '.git', 'node_modules']).add(data.toString()).createFilter();
38 });
39}
40
41function _walk(dir, fn, walkDirs) {
42return Promise.coroutine(function *() {
43 var ignoreFilter = yield __load_ergoignoreFilter(dir);
44 var filterFn = function(item) {
45 var relItem = path.relative(dir, item)
46 return ignoreFilter(relItem);
47 }
48 var p = Promise.resolve();
49 var walkerP = new Promise(function(resolve) { // I'd love to know how to not use the promise anti-pattern here!
50
51 function resolvall(result) {
52 p.then(function() {
53 l.vvlog("Directory traversal is complete. Result: " + result)
54 resolve(true)
55 });
56 }
57
58 fs.walk(dir, {filter:filterFn})
59 .on('data', function (item) {
60 var stats = item.stats; // don't follow symlinks!
61 if (stats.isFile() || (walkDirs && stats.isDirectory() && item.path!=dir)) {
62 p = p.then(function() {
63 return fn(item);
64 })
65 }
66 else if (!stats.isDirectory())
67 l.vlogd("skipping " + item.path)
68 })
69 .on('end', function () {
70 // logging doesn't work here :( ????
71 // l.vlog('********** Finished walking **************')
72 resolvall("OK");
73 })
74 .on('error', function(e) {
75 //l.vlogw("Failed to walk properly: \n" + _.niceStackTrace(e))
76 resolvall("Failed to walk properly: \n" + _.niceStackTrace(e));
77 })
78 return true;
79 });
80 yield walkerP;
81 yield p;
82})();
83};
84
85/*
86function _getDirLastWriteTime(dir)
87{
88return Promise.coroutine(function *() {
89 if (!(yield fs.dirExists(dir)))
90 return new Date(1970,1,1);
91
92 var dlatest = 0;
93 yield _walk(dir, null, function(item) {
94 if (item.stats.mtime>dlatest)
95 dlatest = item.stats.mtime;
96 })
97 return dlatest;
98})();
99}
100*/
101
102
103/*
104### Race Conditions for data availability
105
106There are possible race conditions. eg:
107
108* blog.tem.html, followed by
109* blog/blog post.md
110
111The render chain for both is:
112
113* template_man, simpletag
114* header_read, header_add, marked, template_man, simpletag
115
116However, if we render each in order, then `blog.tem.html` will try to render before `header_add` has been reached in the other.
117There are 2 solutions to this:
118
1191. 'right align' all rendering, padding with a 'dummy_render', such that the render chains are:
120 * dummy , dummy , dummy , template_man, simpletag
121 * header_read , header_add , marked , template_man, simpletag
122 (Which just happens to work, in this case)
1232. A more tricky 'alignment' such that all eg 'template_man', will be rendered at the same time
124
125Option 1. has been chosen, for now...aka _rightAlignRenderers():
126*/
127function _rightAlignRenderers(context) {
128 var dummy_renderer = plugin_api.findRendererByName("dummy");
129
130 // find the length of the longest chain.
131 var longest = 0;
132 for (var i=0; i<context.files.length; i++)
133 {
134 var fi = context.files[i];
135 longest = Math.max(longest, fi.renderers.length);
136 }
137 // inject the dummy renderer to the left of the existing renderers
138 for (var i=0; i<context.files.length; i++)
139 {
140 var fi = context.files[i];
141 if (fi.renderers.length<longest)
142 fi.renderers = (new Array(longest - fi.renderers.length)).fill(dummy_renderer).concat(fi.renderers);
143 }
144}
145
146
147
148function _loadAll(context) {
149 return Promise.coroutine( function *() {
150 for (var i=0; i<context.files.length; i++)
151 {
152 var fi = context.files[i];
153 yield fi.loadOrCopy(context);
154 }
155 return true;
156 })();
157}
158
159function _saveAll(context) {
160 return Promise.coroutine( function *() {
161 for (var i=0; i<context.files.length; i++)
162 {
163 var fi = context.files[i];
164 yield fi.save(context);
165 }
166
167 yield plugin_api.saveAll(context)
168 return true;
169 })();
170}
171
172function _renderAll(context) {
173 return Promise.coroutine( function *() {
174 l.vlog("Loading...")
175 yield _loadAll(context)
176
177 _rightAlignRenderers(context);
178
179 var keep_rendering = true;
180 l.vlog("Rendering...")
181 while(keep_rendering) {
182 keep_rendering = false;
183 for (var i=0; i<context.files.length; i++)
184 {
185 var fi = context.files[i];
186 if (fi.renderNext(context))
187 keep_rendering = true;
188 }
189 }
190 l.vlog("Saving...")
191 yield _saveAll(context);
192 return true;
193 })();
194}
195
196
197
198
199module.exports = function(options) {
200return Promise.coroutine(function *() {
201 l.log("Building...")
202 options = options || {};
203 var context = require('./config').getContextSync(options.working_dir);
204 context.mergeRuntimeOptions(options);
205
206 // load the default plugins, markdown, textile and simple
207 var plugins_to_load = context.config.plugins || "{default}"
208 _.toRealArray(plugins_to_load, ',').forEach(function(name) {
209 plugin_api.loadPlugin(name, context)
210 });
211
212 l.vvlogd("Context is:\n"+l.dump(context));
213
214 if (!(yield fs.dirExists(context.getSourcePath())))
215 throw new Error("Missing source path: "+context.getSourcePath());
216
217 // (We'll deal with missing layouts/partials as they arise, since they may not actually be needed)
218 yield fs.ensureDir(context.getOutPath());
219
220 var rebuild = options.clean;
221 /* This has no real effect. A file will only write if it actually changes anyhow.
222 var _lastBuildTime = yield _getDirLastWriteTime(context.getOutPath());
223 if (!rebuild && (yield _getDirLastWriteTime(context.getPartialsPath()))>_lastBuildTime) {
224 l.log("Partials directory has changed. Rebuilding...")
225 rebuild = true;
226 }
227 if (!rebuild && (yield _getDirLastWriteTime(context.getLayoutsPath()))>_lastBuildTime) {
228 l.log("Layouts directory has changed. Rebuilding...")
229 rebuild = true;
230 }*/
231
232 if (rebuild) {
233 //yield fs.emptyDir(context.getOutPath()); Removed. We know obey .ergoignore
234
235 //var _destIgnoreFn = yield _get_fileFilter(context.getOutPath());
236 var _deleteFile = function(item) {
237 l.vlog("Removing '"+item.path+"'...");
238 fs.remove(item.path);
239 }
240 l.log("Cleaning '"+context.getOutPath()+"'...")
241 yield _walk(context.getOutPath(), _deleteFile, true);
242 }
243
244 var _addFile = function(item) {
245 if (!fs.isInDir(context.getOutPath(), item.path)) // don't allow anything in the output folder to be added.
246 return context.addFile(item.path, item.stats)
247 .then(function() {
248 return true;
249 })
250 return Promise.resolve(false);
251 }
252
253
254 l.log("Reading '"+context.getSourcePath()+"'...")
255
256 if (fs.isInDir(context.getSourcePath(), context.getPartialsPath()))
257 l.logw("Partials folder is inside the source folder. This can be problematic")
258 else
259 yield _walk(context.getPartialsPath(), _addFile); // load the partials, if not already done
260
261 if (fs.isInDir(context.getSourcePath(), context.getLayoutsPath()))
262 l.logw("Layouts folder is inside the source folder. This can be problematic")
263 else
264 yield _walk(context.getLayoutsPath(), _addFile); // load the layouts, if not already done
265
266 if (fs.isInDir(context.getSourcePath(), context.getThemePath()))
267 l.logw("Theme folder is inside the source folder. This can be problematic")
268 else {
269 yield _walk(context.getThemePath(), _addFile); // load the themes, if not already done
270 }
271
272 yield _walk(context.getSourcePath(), _addFile);
273
274 // Now that all the files are ready, we can do something about loading/rendering/saving them
275 yield _renderAll(context);
276
277 l.log("Done");
278 return true;
279})();
280}
\No newline at end of file