builder/lib/tasks/jsdoc/generateJsdoc.js

  1. import {getLogger} from "@ui5/logger";
  2. const log = getLogger("builder:tasks:jsdoc:generateJsdoc");
  3. import path from "node:path";
  4. import os from "node:os";
  5. import fs from "graceful-fs";
  6. import {mkdirp, rmrf} from "../../utils/fs.js";
  7. import {promisify} from "node:util";
  8. const mkdtemp = promisify(fs.mkdtemp);
  9. import jsdocGenerator from "../../processors/jsdoc/jsdocGenerator.js";
  10. import {createAdapter} from "@ui5/fs/resourceFactory";
  11. /**
  12. * @public
  13. * @module @ui5/builder/tasks/jsdoc/generateJsdoc
  14. */
  15. /**
  16. *
  17. * @public
  18. * @typedef {object} GenerateJsdocOptions
  19. * @property {string|string[]} pattern Pattern to locate the files to be processed
  20. * @property {string} projectName Project name
  21. * @property {string} namespace Namespace to build (e.g. <code>some/project/name</code>)
  22. * @property {string} version Project version
  23. */
  24. /**
  25. * Task to execute a JSDoc build for UI5 projects
  26. *
  27. * @public
  28. * @function default
  29. * @static
  30. *
  31. * @param {object} parameters Parameters
  32. * @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files
  33. * @param {@ui5/fs/AbstractReader} parameters.dependencies Reader or Collection to read dependency files
  34. * @param {module:@ui5/builder/tasks/jsdoc/generateJsdoc~GenerateJsdocOptions} parameters.options Options
  35. * @param {@ui5/project/build/helpers/TaskUtil|object} [parameters.taskUtil] TaskUtil
  36. * @returns {Promise<undefined>} Promise resolving with <code>undefined</code> once data has been written
  37. */
  38. export default async function generateJsdoc({
  39. taskUtil,
  40. workspace,
  41. dependencies,
  42. options = {}
  43. }) {
  44. const {projectName, namespace, version, pattern} =
  45. /** @type {module:@ui5/builder/tasks/jsdoc/generateJsdoc~GenerateJsdocOptions} */ (options);
  46. if (!projectName || !namespace || !version || !pattern) {
  47. throw new Error("[generateJsdoc]: One or more mandatory options not provided");
  48. }
  49. const {sourcePath: resourcePath, targetPath, tmpPath, cleanup} =
  50. await utils.createTmpDirs(projectName);
  51. taskUtil?.registerCleanupTask(cleanup);
  52. const [writtenResourcesCount] = await Promise.all([
  53. utils.writeResourcesToDir({
  54. workspace,
  55. pattern,
  56. targetPath: resourcePath
  57. }),
  58. utils.writeDependencyApisToDir({
  59. dependencies,
  60. targetPath: path.join(tmpPath, "dependency-apis")
  61. })
  62. ]);
  63. if (writtenResourcesCount === 0) {
  64. log.info(`Failed to find any input resources for project ${projectName} using pattern ` +
  65. `${pattern}. Skipping JSDoc generation...`);
  66. return;
  67. }
  68. const createdResources = await jsdocGenerator({
  69. sourcePath: resourcePath,
  70. targetPath,
  71. tmpPath,
  72. options: {
  73. projectName,
  74. namespace,
  75. version,
  76. variants: ["apijson"]
  77. }
  78. });
  79. await Promise.all(createdResources.map((resource) => {
  80. return workspace.write(resource);
  81. }));
  82. }
  83. const utils = {
  84. /**
  85. * Create temporary directories for JSDoc generation processor
  86. *
  87. * @private
  88. * @param {string} projectName Project name used for naming the temporary working directory
  89. * @returns {Promise<object>} Promise resolving with sourcePath, targetPath and tmpPath strings
  90. */
  91. createTmpDirs: async function(projectName) {
  92. const tmpDirPath = await utils.createTmpDir(projectName);
  93. const sourcePath = path.join(tmpDirPath, "src"); // dir will be created by writing project resources below
  94. await mkdirp(sourcePath);
  95. const targetPath = path.join(tmpDirPath, "target"); // dir will be created by jsdoc itself
  96. await mkdirp(targetPath);
  97. const tmpPath = path.join(tmpDirPath, "tmp"); // dir needs to be created by us
  98. await mkdirp(tmpPath);
  99. return {
  100. sourcePath,
  101. targetPath,
  102. tmpPath,
  103. cleanup: async () => {
  104. return rmrf(tmpDirPath);
  105. }
  106. };
  107. },
  108. /**
  109. * Create a temporary directory on the host system
  110. *
  111. * @private
  112. * @param {string} projectName Project name used for naming the temporary directory
  113. * @returns {Promise<string>} Promise resolving with path of the temporary directory
  114. */
  115. createTmpDir: async function(projectName) {
  116. const sanitizedProjectName = projectName.replace(/[^A-Za-z0-9]/g, "");
  117. const tmpRootPath = path.join(os.tmpdir(), "ui5-tooling");
  118. await mkdirp(tmpRootPath);
  119. // Appending minus sign also because node docs advise to "avoid trailing X characters in prefix"
  120. return mkdtemp(path.join(tmpRootPath, `jsdoc-${sanitizedProjectName}-`));
  121. },
  122. /**
  123. * Write resources from workspace matching the given pattern to the given fs destination
  124. *
  125. * @private
  126. * @param {object} parameters Parameters
  127. * @param {@ui5/fs/DuplexCollection} parameters.workspace DuplexCollection to read and write files
  128. * @param {string} parameters.pattern Pattern to match resources in workspace against
  129. * @param {string} parameters.targetPath Path to write the resources to
  130. * @returns {Promise<number>} Promise resolving with number of resources written to given directory
  131. */
  132. writeResourcesToDir: async function({workspace, pattern, targetPath}) {
  133. const fsTarget = createAdapter({
  134. fsBasePath: targetPath,
  135. virBasePath: "/resources/"
  136. });
  137. const allResources = await workspace.byGlob(pattern);
  138. // write all resources to the tmp folder
  139. await Promise.all(allResources.map((resource) => fsTarget.write(resource)));
  140. return allResources.length;
  141. },
  142. /**
  143. * Write api.json files of dependencies to given target path in a flat structure
  144. *
  145. * @private
  146. * @param {object} parameters Parameters
  147. * @param {@ui5/fs/AbstractReader} parameters.dependencies Reader or Collection to read dependency files
  148. * @param {string} parameters.targetPath Path to write the resources to
  149. * @returns {Promise<number>} Promise resolving with number of resources written to given directory
  150. */
  151. writeDependencyApisToDir: async function({dependencies, targetPath}) {
  152. const depApis = await dependencies.byGlob("/test-resources/**/designtime/api.json");
  153. // Clone resources before changing their path
  154. const apis = await Promise.all(depApis.map((resource) => resource.clone()));
  155. for (let i = 0; i < apis.length; i++) {
  156. apis[i].setPath(`/api-${i}.json`);
  157. }
  158. const fsTarget = createAdapter({
  159. fsBasePath: targetPath,
  160. virBasePath: "/"
  161. });
  162. await Promise.all(apis.map((resource) => fsTarget.write(resource)));
  163. return apis.length;
  164. }
  165. };
  166. // Export utils for testing only
  167. /* istanbul ignore else */
  168. if (process.env.NODE_ENV === "test") {
  169. generateJsdoc._utils = utils;
  170. }