Skip to content

Commit f854836

Browse files
committed
Initial commit.
0 parents  commit f854836

File tree

5 files changed

+11324
-0
lines changed

5 files changed

+11324
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules/
2+
.idea/

README.md

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# esdoc-webpack-plugin
2+
3+
Run [`esdoc`](https://esdoc.org) with webpack!
4+
5+
## Installation
6+
```
7+
npm install --save-dev esdoc-webpack-plugin
8+
```
9+
10+
## Configuration
11+
12+
`esdoc-webpack-plugin` has a handful of configuration options for itself, and
13+
also accepts options to pass directly to esdoc itself, though it will try to
14+
read your configuration file if you've got one and fill in any gaps.
15+
16+
```js
17+
// webpack.config.js
18+
const ESDocPlugin = require('esdoc-webpack-plugin');
19+
20+
// ...
21+
22+
plugins: [
23+
new ESDocPlugin({
24+
cwd: '.'
25+
showOutput: false,
26+
source: './src',
27+
destination: './docs',
28+
})
29+
]
30+
31+
// ...
32+
```
33+
34+
### Options
35+
36+
Option | Type | Purpose
37+
--------------- | ------- | --------------------------------------------------------------
38+
conf | string | The config filename to look for.
39+
cwd | string | Where to start looking for the esdoc executable.
40+
preserveTmpFile | boolean | The plugin creates a temporary file to use for configuration during esdoc runtime based on options from webpack and your config. Set this to true if you want to keep it around.
41+
showOutput | boolean | Prints esdoc output if true.
42+

index.js

+289
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
/**
2+
* @file A webpack plugin to run esdoc on compile / recompile.
3+
*/
4+
5+
const webpack = require('webpack');
6+
const path = require('path');
7+
const spawn = require('child_process').spawn;
8+
const fse = require('fs-extra');
9+
const chalk = require('chalk');
10+
11+
const validateOptions = require('schema-utils');
12+
13+
// Schema for options object.
14+
const schema = {
15+
type: 'object',
16+
properties: {
17+
conf: {
18+
type: 'string',
19+
},
20+
cwd: {
21+
type: 'string',
22+
},
23+
preserveTmpFile: {
24+
type: 'boolean',
25+
},
26+
showOutput: {
27+
type: 'boolean',
28+
}
29+
},
30+
};
31+
32+
const isWindows = /^win/.test(process.platform);
33+
34+
const PLUGIN_NAME = 'ESDocPlugin';
35+
36+
const ESDOC_FILES = isWindows ? [] : [
37+
'node_modules/.bin/esdoc',
38+
'node_modules/esdoc/esdoc.js',
39+
];
40+
41+
/**
42+
* Look for files in directories.
43+
*/
44+
const lookupFile = (files, dirs) => {
45+
let found = null;
46+
47+
[].concat(files).some(function (filename) {
48+
return [].concat(dirs).some(function (dirname) {
49+
var file = path.resolve(path.join(dirname, filename));
50+
51+
if (fse.existsSync(file)) {
52+
return found = file;
53+
}
54+
});
55+
});
56+
57+
return found;
58+
};
59+
60+
const getLongestCommonSharedDirectory = (s) => {
61+
let k = s[0].Length;
62+
for (let i = 1; i < s.length; i++) {
63+
k = Math.Min(k, s[i].length);
64+
for (let j = 0; j < k; j++) {
65+
if (s[i][j] != s[0][j]) {
66+
k = j;
67+
break;
68+
}
69+
}
70+
}
71+
const fullPath = s[0].substring(0, k);
72+
return fullPath.substring(0, fullPath.lastIndexOf('/'));
73+
}
74+
75+
/**
76+
* Reads the esdoc config
77+
*
78+
* @param {string} filepath - The path to the file.
79+
* @returns {any}
80+
*/
81+
const readConfigFile = (filepath) => {
82+
delete require.cache[filepath];
83+
return require(filepath);
84+
};
85+
86+
/**
87+
* Converts milliseconds to minutes:seconds.
88+
*
89+
* @param {number} millis - A millisecond value.
90+
* @returns {string} - A string in the format mm:ss.
91+
*/
92+
const millisToMinutesAndSeconds = (millis) => {
93+
var minutes = Math.floor(millis / 60000);
94+
var seconds = ((millis % 60000) / 1000).toFixed(0);
95+
return minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
96+
};
97+
98+
/**
99+
* Defines the main ESDocPlugin.
100+
*
101+
* @class
102+
* @type {WebpackPlugin}
103+
* @todo Running webpack in watch mode causes compile to happen twice.
104+
* @todo Validate constructor options.
105+
* @todo Cleanly merge options passed to the constructor with default options. Lodash's merge is nice, but I don't want another dep.
106+
* @todo Test it.
107+
* @todo Try setting some params from the webpack plugin instance.
108+
* @todo Handle cases where we can't find the config file.
109+
*/
110+
module.exports = class Plugin {
111+
constructor(opts = {source: './src', destination: './docs'}) {
112+
validateOptions(schema, opts, 'ESDoc webpack plugin');
113+
const defaultOptions = {
114+
conf: '.esdoc.json', // Default config file name.
115+
cwd: opts.cwd || './', // Default path for lookup.
116+
preserveTmpFile: true, // Keep the generated temporary settings file?
117+
showOutput: false, // Show all the output from esdoc?
118+
// esdoc option defaults, just in case.
119+
source: './src',
120+
destination: './docs',
121+
excludes: ['\\.config\\.js', '\\.babel\\.js'],
122+
plugins: [{
123+
name: 'esdoc-standard-plugin',
124+
}],
125+
};
126+
127+
// Merge options
128+
// opts passed to the constructor will override default values.
129+
this.options = {...defaultOptions, ...opts};
130+
131+
if (this.options.showOutput) {
132+
console.log(chalk.yellow('ESDocPlugin:'), 'Options', this.options);
133+
}
134+
}
135+
136+
apply(compiler) {
137+
const self = this;
138+
const options = self.options;
139+
const cwd = process.cwd();
140+
const givenDirectory = options.cwd;
141+
let preserveTmpFile = options.preserveTmpFile;
142+
let esdocConfig = path.resolve(givenDirectory, options.conf);
143+
const esdocConfigDir = path.dirname(esdocConfig);
144+
const files = [];
145+
let cmd;
146+
let obj = {};
147+
let tmpFile;
148+
let esdocArgs;
149+
let esdoc;
150+
let esdocErrors = [];
151+
152+
compiler.hooks.watchRun.tapAsync(PLUGIN_NAME, (compiler, callback) => {
153+
console.log(chalk.yellow('ESDocPlugin'), chalk.magenta('Watching for changes...'));
154+
callback();
155+
});
156+
157+
const promiseEsdoc = (esdoc, cmd, esdocArgs, esdocConfigDir, esdocErrors, tmpFile) => new Promise((resolve, reject) => {
158+
esdoc = spawn(cmd, esdocArgs, {
159+
cwd: esdocConfigDir,
160+
});
161+
if (obj.showOutput) {
162+
// Collect the socket output from esdoc, turning the buffer into something readable.
163+
console.log(chalk.yellow('ESDocPlugin:'), 'Beginning output.');
164+
let received = '';
165+
esdoc.stdout.on('data', (data) => {
166+
received += data;
167+
const messages = received.split('\n');
168+
if (messages.length > 1) {
169+
let printed = '';
170+
for (let message of messages) {
171+
if (message !== '') {
172+
let split = (message.toString().split(':'));
173+
console.log(`${chalk.blue(split[0])}: ${chalk.green(split[1])}`);
174+
received = '';
175+
}
176+
}
177+
}
178+
});
179+
}
180+
esdoc.stderr.on('data', (data) => esdocErrors.push(data.toString()));
181+
esdoc.on('close', (closeCode) => {
182+
// Remove that tmp file if we have one and we aren't keeping it.
183+
if (tmpFile && !preserveTmpFile) {
184+
console.log(chalk.yellow('ESDocPlugin:'), 'Removing temporary esdoc config file...');
185+
fse.unlinkSync(tmpFile);
186+
tmpFile = null;
187+
}
188+
if (esdocErrors.length > 0) {
189+
esdocErrors.forEach((value) => console.error(value));
190+
reject(new Error(chalk.yellow('ESDocPlugin:'), 'Exited with code ' + code));
191+
} else {
192+
console.log(chalk.yellow('ESDocPlugin:'), 'Emitted files to output directory.');
193+
resolve(true);
194+
}
195+
});
196+
});
197+
198+
compiler.hooks.emit.tapAsync(PLUGIN_NAME, (compilation, callback) => {
199+
console.log(chalk.yellow('ESDocPlugin:'), 'Compiling...');
200+
console.log('EMITTING');
201+
202+
// Look for esdoc and when we find it, set it to cmd.
203+
cmd = lookupFile(ESDOC_FILES, [
204+
// config dir
205+
esdocConfigDir,
206+
// given dir
207+
givenDirectory,
208+
// called from
209+
cwd,
210+
// Here
211+
__dirname,
212+
]);
213+
// Wait a second... is esdoc installed?
214+
if (!cmd) {
215+
callback(new Error(chalk.yellow('ESDocPlugin:'), 'esdoc was not found.'));
216+
}
217+
// See if esdocConfig exists, if it does, set it to obj, otherwise have an exception.
218+
if (fse.existsSync(esdocConfig)) {
219+
try {
220+
obj = readConfigFile(esdocConfig);
221+
} catch (exception) {
222+
callback(exception);
223+
return;
224+
}
225+
}
226+
227+
// If we have a config file, use it. Otherwise handle it.
228+
if (obj.source && obj.includes) {
229+
console.log(chalk.yellow('ESDocPlugin:'), 'Pulling data from the configuration file.');
230+
// Merge the configuration file with the options object sent from webpack.
231+
// If a user decided to set some options when they called `new Plugin()`,
232+
// and still pointed to a config file, we can assume the instance settings
233+
// they passed should take priority.
234+
// Some of the keys that end up in here may not be useful.
235+
obj = {...obj, ...options}; // lodash would be better for this because it can do deep merges, but I just don't want it.
236+
}
237+
else {
238+
console.log(chalk.yellow('ESDocPlugin:'), 'Provided configuration either not found or does not contain an includes key. Generating from the bundles.')
239+
// If our options object doesn't have includes, let's generate them from the bundles.
240+
compilation.fileDependencies.forEach((filepath, i) => {
241+
// Excludes this expression from out file path collection.
242+
var exception = /\/node_modules\//.test(filepath);
243+
var inclusion = /index.js$/.test(filepath);
244+
245+
// Collect all our js files.
246+
if (!exception && inclusion) {
247+
files.push(filepath);
248+
}
249+
});
250+
251+
// Get the shared parent directory of all our files, that's the src.
252+
obj.source = getLongestCommonSharedDirectory(files);
253+
obj = {...obj, ...options};
254+
}
255+
256+
// Since we're generating config, we'll store it in a tmp file to pass to the esdoc executable.
257+
tmpFile = esdocConfig + '.tmp';
258+
console.log(chalk.yellow('ESDocPlugin:'), 'Writing temporary file at: ', tmpFile);
259+
fse.writeFileSync(tmpFile, JSON.stringify(obj));
260+
esdocConfig = tmpFile;
261+
262+
console.log(chalk.yellow('ESDocPlugin:'), 'Using esdoc located at', cmd);
263+
264+
// Esdoc doesn't actually have a lot of cli arguments.
265+
// Here we just point it to our config file.
266+
esdocArgs = ['-c', esdocConfig];
267+
268+
callback();
269+
});
270+
271+
// Report when finished.
272+
compiler.hooks.done.tap(PLUGIN_NAME, (stats) => {
273+
// @TODO: Really this run of esdoc as a child process should probably happen in the emit hook,
274+
// but if it's there, watching makes emit trigger twice on startup. I'm guessing the
275+
// reason this happens is that emit finishes before the subprocess has totally exited,
276+
// so when it finally does end, the esdoc process creates/modifies files in the plugin
277+
// output directory get created and compilation starts all over again.
278+
// I need a way to ignore the output files or directory for this plugin during watch, we
279+
// don't care if something happens there.
280+
promiseEsdoc(esdoc, cmd, esdocArgs, esdocConfigDir, esdocErrors, tmpFile)
281+
.then(response => {
282+
console.log(chalk.yellow('ESDocPlugin:'), 'Finished compiling.');
283+
console.log(chalk.yellow('ESDocPlugin:'), 'Total run time ', chalk.green(millisToMinutesAndSeconds(stats.endTime - stats.startTime)));
284+
})
285+
});
286+
287+
288+
}
289+
};

0 commit comments

Comments
 (0)