UNPKG

8.06 kBJavaScriptView Raw
1var fs = require('fs-extra');
2var path = require('path');
3var spawn = require('child_process').spawn;
4
5var async = require('async');
6var walk = require('walk').walk;
7var isWindows = process.platform.indexOf('win') === 0;
8
9var sourceDir = path.join(__dirname, '..', 'src');
10var externsDir = path.join(__dirname, '..', 'externs');
11var externsPaths = [
12 path.join(externsDir, 'olx.js'),
13 path.join(externsDir, 'geojson.js')
14];
15var infoPath = path.join(__dirname, '..', 'build', 'info.json');
16
17/**
18 * Get checked path of a binary.
19 * @param {string} binaryName Binary name of the binary path to find.
20 * @return {string} Path.
21 */
22function getBinaryPath(binaryName) {
23 if (isWindows) {
24 binaryName += '.cmd';
25 }
26
27 var jsdocResolved = require.resolve('jsdoc/jsdoc.js');
28 var expectedPaths = [
29 path.join(__dirname, '..', 'node_modules', '.bin', binaryName),
30 path.resolve(path.join(path.dirname(jsdocResolved), '..', '.bin', binaryName))
31 ];
32
33 for (var i = 0; i < expectedPaths.length; i++) {
34 var expectedPath = expectedPaths[i];
35 if (fs.existsSync(expectedPath)) {
36 return expectedPath;
37 }
38 }
39
40 throw Error('JsDoc binary was not found in any of the expected paths: ' + expectedPaths);
41}
42
43var jsdoc = getBinaryPath('jsdoc');
44
45var jsdocConfig = path.join(
46 __dirname, '..', 'config', 'jsdoc', 'info', 'conf.json');
47
48
49/**
50 * Get the mtime of the info file.
51 * @param {function(Error, Date)} callback Callback called with any
52 * error and the mtime of the info file (zero date if it doesn't exist).
53 */
54function getInfoTime(callback) {
55 fs.stat(infoPath, function(err, stats) {
56 if (err) {
57 if (err.code === 'ENOENT') {
58 callback(null, new Date(0));
59 } else {
60 callback(err);
61 }
62 } else {
63 callback(null, stats.mtime);
64 }
65 });
66}
67
68
69/**
70 * Test whether any externs are newer than the provided date.
71 * @param {Date} date Modification time of info file.
72 * @param {function(Error, Date, boolen)} callback Called with any
73 * error, the mtime of the info file (zero date if it doesn't exist), and
74 * whether any externs are newer than that date.
75 */
76function getNewerExterns(date, callback) {
77 var newer = false;
78 var walker = walk(externsDir);
79 walker.on('file', function(root, stats, next) {
80 var sourcePath = path.join(root, stats.name);
81 externsPaths.forEach(function(path) {
82 if (sourcePath === path && stats.mtime > date) {
83 newer = true;
84 }
85 });
86 next();
87 });
88 walker.on('errors', function() {
89 callback(new Error('Trouble walking ' + externsDir));
90 });
91 walker.on('end', function() {
92 callback(null, date, newer);
93 });
94}
95
96
97/**
98 * Generate a list of all .js paths in the source directory if any are newer
99 * than the provided date.
100 * @param {Date} date Modification time of info file.
101 * @param {boolean} newer Whether any externs are newer than date.
102 * @param {function(Error, Array.<string>)} callback Called with any
103 * error and the array of source paths (empty if none newer).
104 */
105function getNewer(date, newer, callback) {
106 var paths = [].concat(externsPaths);
107
108 var walker = walk(sourceDir);
109 walker.on('file', function(root, stats, next) {
110 var sourcePath = path.join(root, stats.name);
111 if (/\.js$/.test(sourcePath)) {
112 paths.push(sourcePath);
113 if (stats.mtime > date) {
114 newer = true;
115 }
116 }
117 next();
118 });
119 walker.on('errors', function() {
120 callback(new Error('Trouble walking ' + sourceDir));
121 });
122 walker.on('end', function() {
123
124 /**
125 * Windows has restrictions on length of command line, so passing all the
126 * changed paths to a task will fail if this limit is exceeded.
127 * To get round this, if this is Windows and there are newer files, just
128 * pass the sourceDir to the task so it can do the walking.
129 */
130 if (isWindows) {
131 paths = [sourceDir].concat(externsPaths);
132 }
133
134 callback(null, newer ? paths : []);
135 });
136}
137
138
139/**
140 * Parse the JSDoc output.
141 * @param {string} output JSDoc output
142 * @return {Object} Symbol and define info.
143 */
144function parseOutput(output) {
145 if (!output) {
146 throw new Error('Expected JSON output');
147 }
148
149 var info;
150 try {
151 info = JSON.parse(String(output));
152 } catch (err) {
153 throw new Error('Failed to parse output as JSON: ' + output);
154 }
155 if (!Array.isArray(info.symbols)) {
156 throw new Error('Expected symbols array: ' + output);
157 }
158 if (!Array.isArray(info.defines)) {
159 throw new Error('Expected defines array: ' + output);
160 }
161
162 return info;
163}
164
165
166/**
167 * Spawn JSDoc.
168 * @param {Array.<string>} paths Paths to source files.
169 * @param {function(Error, string)} callback Callback called with any error and
170 * the JSDoc output (new metadata). If provided with an empty list of paths
171 * the callback will be called with null.
172 */
173function spawnJSDoc(paths, callback) {
174 if (paths.length === 0) {
175 process.nextTick(function() {
176 callback(null, null);
177 });
178 return;
179 }
180
181 var output = '';
182 var errors = '';
183 var cwd = path.join(__dirname, '..');
184 var child = spawn(jsdoc, ['-c', jsdocConfig].concat(paths), {cwd: cwd});
185
186 child.stdout.on('data', function(data) {
187 output += String(data);
188 });
189
190 child.stderr.on('data', function(data) {
191 errors += String(data);
192 });
193
194 child.on('exit', function(code) {
195 if (code) {
196 callback(new Error(errors || 'JSDoc failed with no output'));
197 } else {
198 var info;
199 try {
200 info = parseOutput(output);
201 } catch (err) {
202 callback(err);
203 return;
204 }
205 callback(null, info);
206 }
207 });
208}
209
210
211/**
212 * Given the path to a source file, get the list of provides.
213 * @param {string} srcPath Path to source file.
214 * @param {function(Error, Array.<string>)} callback Called with a list of
215 * provides or any error.
216 */
217var getProvides = async.memoize(function(srcPath, callback) {
218 fs.readFile(srcPath, function(err, data) {
219 if (err) {
220 callback(err);
221 return;
222 }
223 var provides = [];
224 var matcher = /goog\.provide\('(.*)'\)/;
225 String(data).split('\n').forEach(function(line) {
226 var match = line.match(matcher);
227 if (match) {
228 provides.push(match[1]);
229 }
230 });
231 callback(null, provides);
232 });
233});
234
235
236/**
237 * Add provides data to new symbols.
238 * @param {Object} info Symbols and defines metadata.
239 * @param {function(Error, Object)} callback Updated metadata.
240 */
241function addSymbolProvides(info, callback) {
242 if (!info) {
243 process.nextTick(function() {
244 callback(null, null);
245 });
246 return;
247 }
248
249 function addProvides(symbol, callback) {
250 getProvides(symbol.path, function(err, provides) {
251 if (err) {
252 callback(err);
253 return;
254 }
255 symbol.provides = provides;
256 callback(null, symbol);
257 });
258 }
259
260 async.map(info.symbols, addProvides, function(err, newSymbols) {
261 info.symbols = newSymbols;
262 callback(err, info);
263 });
264}
265
266
267/**
268 * Write symbol and define metadata to the info file.
269 * @param {Object} info Symbol and define metadata.
270 * @param {function(Error)} callback Callback.
271 */
272function writeInfo(info, callback) {
273 if (info) {
274 var str = JSON.stringify(info, null, ' ');
275 fs.outputFile(infoPath, str, callback);
276 } else {
277 process.nextTick(function() {
278 callback(null);
279 });
280 }
281}
282
283
284/**
285 * Determine if source files have been changed, run JSDoc and write updated
286 * info if there are any changes.
287 *
288 * @param {function(Error)} callback Called when the info file has been written
289 * (or an error occurs).
290 */
291function main(callback) {
292 async.waterfall([
293 getInfoTime,
294 getNewerExterns,
295 getNewer,
296 spawnJSDoc,
297 addSymbolProvides,
298 writeInfo
299 ], callback);
300}
301
302
303/**
304 * If running this module directly, read the config file and call the main
305 * function.
306 */
307if (require.main === module) {
308 main(function(err) {
309 if (err) {
310 process.stderr.write(err.message + '\n');
311 process.exit(1);
312 } else {
313 process.exit(0);
314 }
315 });
316}
317
318
319/**
320 * Export main function.
321 */
322module.exports = main;