writer.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. /*
  2. * apidoc
  3. * https://apidocjs.com
  4. *
  5. * Copyright (c) 2013 inveris OHG
  6. * Authors: Peter Rottmann <rottmann@inveris.de>
  7. * Nicolas CARPi @ Deltablot
  8. * Licensed under the MIT license.
  9. */
  10. /**
  11. * Write output files
  12. */
  13. class Writer {
  14. constructor (api, app, cacheBustingQueryParam = `v=${Date.now()}`) {
  15. this.api = api;
  16. this.log = app.log;
  17. this.opt = app.options;
  18. this.cacheBustingQueryParam = String(cacheBustingQueryParam);
  19. this.fs = require('fs-extra');
  20. this.path = require('path');
  21. }
  22. // The public method
  23. write () {
  24. if (this.opt.dryRun) {
  25. this.log.info('Dry run mode enabled: no files created.');
  26. return new Promise((resolve, reject) => { return resolve(); });
  27. }
  28. this.log.verbose('Writing files...');
  29. if (this.opt.single) {
  30. return this.createSingleFile();
  31. }
  32. return this.createOutputFiles();
  33. }
  34. /**
  35. * Find assets from node_modules folder and return its path
  36. * Argument is the path relative to node_modules folder
  37. */
  38. findAsset (assetPath) {
  39. try {
  40. const path = require.resolve(assetPath);
  41. return path;
  42. } catch {
  43. this.log.error('Could not find where dependencies of apidoc live!');
  44. }
  45. }
  46. createOutputFiles () {
  47. this.createDir(this.opt.dest);
  48. // create index.html
  49. this.log.verbose('Copying template index.html to: ' + this.opt.dest);
  50. this.fs.writeFileSync(this.path.join(this.opt.dest, 'index.html'), this.getIndexContent());
  51. // create assets folder
  52. const assetsPath = this.path.resolve(this.path.join(this.opt.dest + 'assets'));
  53. this.createDir(assetsPath);
  54. // add the fonts
  55. this.log.verbose('Copying fonts to: ' + assetsPath);
  56. this.fs.copySync(this.path.join(this.opt.template, 'fonts'), assetsPath);
  57. // save the parsed api file
  58. if (this.opt.writeJson) {
  59. const jsonFile = this.path.join(assetsPath, 'api-data.json');
  60. this.log.verbose('Saving parsed API to: ' + jsonFile);
  61. this.fs.writeFileSync(jsonFile, this.api.data);
  62. }
  63. // CSS from dependencies
  64. this.log.verbose('Copying bootstrap css to: ' + assetsPath);
  65. this.fs.copySync(this.findAsset('bootstrap/dist/css/bootstrap.min.css'), this.path.join(assetsPath, 'bootstrap.min.css'));
  66. this.fs.copySync(this.findAsset('bootstrap/dist/css/bootstrap.min.css.map'), this.path.join(assetsPath, 'bootstrap.min.css.map'));
  67. this.log.verbose('Copying prism css to: ' + assetsPath);
  68. this.fs.copySync(this.findAsset('prismjs/themes/prism-tomorrow.css'), this.path.join(assetsPath, 'prism.css'));
  69. this.fs.copySync(this.findAsset('prismjs/plugins/toolbar/prism-toolbar.css'), this.path.join(assetsPath, 'prism-toolbar.css'));
  70. this.fs.copySync(this.findAsset('prismjs/plugins/diff-highlight/prism-diff-highlight.css'), this.path.join(assetsPath, 'prism-diff-highlight.css'));
  71. this.log.verbose('Copying main css to: ' + assetsPath);
  72. // main.css
  73. this.fs.copySync(this.path.join(this.opt.template, 'src', 'css', 'main.css'), this.path.join(assetsPath, 'main.css'));
  74. // images
  75. this.fs.copySync(this.path.join(this.opt.template, 'img'), assetsPath);
  76. return this.runWebpack(this.path.resolve(assetsPath));
  77. }
  78. /**
  79. * Run webpack in a promise
  80. */
  81. runWebpack (outputPath) {
  82. this.log.verbose('Running webpack bundler');
  83. return new Promise((resolve, reject) => {
  84. // run webpack to create the bundle file in assets
  85. const webpackConfig = require(this.path.resolve(this.path.join(this.opt.template, 'src', 'webpack.config.js')));
  86. const webpack = require('webpack');
  87. // set output
  88. webpackConfig.output.path = outputPath;
  89. this.log.debug('webpack output folder: ' + webpackConfig.output.path);
  90. // set data
  91. const plugins = [
  92. new webpack.DefinePlugin({
  93. API_DATA: this.api.data,
  94. API_PROJECT: this.api.project,
  95. }),
  96. ];
  97. webpackConfig.plugins = plugins;
  98. // if the --debug flag is passed, produce unminified bundle with inline map
  99. let mode = 'production';
  100. // https://webpack.js.org/configuration/devtool/ - constistent type
  101. let devtool = '';
  102. if (this.opt.debug) {
  103. mode = 'development';
  104. devtool = 'inline-source-map';
  105. }
  106. webpackConfig.mode = mode;
  107. webpackConfig.devtool = devtool || false;
  108. const compiler = webpack(webpackConfig);
  109. compiler.run((err, stats) => {
  110. if (err) {
  111. this.log.error('Webpack failure:', err);
  112. return reject(err);
  113. }
  114. this.log.debug('Generated bundle with hash: ' + stats.hash);
  115. return resolve(outputPath);
  116. });
  117. });
  118. }
  119. /**
  120. * Get index.html content as string with placeholder values replaced
  121. */
  122. getIndexContent () {
  123. const projectInfo = JSON.parse(this.api.project);
  124. const title = projectInfo.title || projectInfo.name || 'Loading...';
  125. const description = projectInfo.description || projectInfo.name || 'API Documentation';
  126. const indexHtml = this.fs.readFileSync(this.path.join(this.opt.template, 'index.html'), 'utf8');
  127. return indexHtml.toString()
  128. // replace titles, descriptions and cache busting query params
  129. .replace(/__API_NAME__/g, title)
  130. .replace(/__API_DESCRIPTION__/g, description)
  131. .replace(/__API_CACHE_BUSTING_QUERY_PARAM__/g, this.cacheBustingQueryParam);
  132. }
  133. createSingleFile () {
  134. // dest is a file path, so get the folder with dirname
  135. this.createDir(this.path.dirname(this.opt.dest));
  136. // get all css content
  137. const bootstrapCss = this.fs.readFileSync(this.findAsset('bootstrap/dist/css/bootstrap.min.css'), 'utf8');
  138. const prismCss = this.fs.readFileSync(this.findAsset('prismjs/themes/prism-tomorrow.css'), 'utf8');
  139. const mainCss = this.fs.readFileSync(this.path.join(this.opt.template, 'src', 'css', 'main.css'), 'utf8');
  140. const tmpPath = '/tmp/apidoc-tmp';
  141. // TODO add favicons in base64 in the html
  142. this.createDir(tmpPath);
  143. return this.runWebpack(tmpPath).then(tmpPath => {
  144. const mainBundle = this.fs.readFileSync(this.path.join(tmpPath, 'main.bundle.js'), 'utf8');
  145. // modify index html for single page use
  146. const indexContent = this.getIndexContent()
  147. // remove link to css normally in assets
  148. .replace(/<link href="assets[^>]*>/g, '')
  149. // remove call to main bundle in assets
  150. .replace(/<script src="assets[^>]*><\/script>/, '');
  151. // concatenate all the content (html + css + javascript bundle)
  152. const finalContent = `${indexContent}
  153. <style>${bootstrapCss} ${prismCss} ${mainCss}</style>
  154. <script>${mainBundle}</script>`;
  155. // create target file
  156. const finalPath = this.path.join(this.opt.dest, 'index.html');
  157. // make sure destination exists
  158. this.createDir(this.opt.dest);
  159. this.log.verbose(`Generating self-contained single file: ${finalPath}`);
  160. this.fs.writeFileSync(finalPath, finalContent);
  161. });
  162. }
  163. /**
  164. * Write a JSON file
  165. *
  166. * @param {string} dest Destination path
  167. * @param {string} data Content of the file
  168. */
  169. writeJsonFile (dest, data) {
  170. this.log.verbose(`Writing json file: ${dest}`);
  171. this.fs.writeFileSync(dest, data + '\n');
  172. }
  173. /**
  174. * Write js file
  175. *
  176. * @param {string} dest Destination path
  177. * @param {string} data Content of the file
  178. */
  179. writeJSFile (dest, data) {
  180. this.log.verbose(`Writing js file: ${dest}`);
  181. switch (this.opt.mode) {
  182. case 'amd':
  183. case 'es':
  184. this.fs.writeFileSync(dest, 'export default ' + data + ';\n');
  185. break;
  186. case 'commonJS':
  187. this.fs.writeFileSync(dest, 'module.exports = ' + data + ';\n');
  188. break;
  189. default:
  190. this.fs.writeFileSync(dest, 'define(' + data + ');' + '\n');
  191. }
  192. }
  193. /**
  194. * Create a directory
  195. *
  196. * @param {string} dir Path of the directory to create
  197. */
  198. createDir (dir) {
  199. if (!this.fs.existsSync(dir)) {
  200. this.log.verbose('Creating dir: ' + dir);
  201. this.fs.mkdirsSync(dir);
  202. }
  203. }
  204. }
  205. module.exports = Writer;