UNPKG

@backstage/plugin-techdocs-node

Version:

Common node.js functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli

328 lines (322 loc) • 10.7 kB
'use strict'; var backendPluginApi = require('@backstage/backend-plugin-api'); var errors = require('@backstage/errors'); var node_child_process = require('node:child_process'); var fs = require('fs-extra'); var gitUrlParse = require('git-url-parse'); var yaml = require('js-yaml'); var path = require('node:path'); var node_stream = require('node:stream'); var helpers = require('../publish/helpers.cjs.js'); function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; } var fs__default = /*#__PURE__*/_interopDefaultCompat(fs); var gitUrlParse__default = /*#__PURE__*/_interopDefaultCompat(gitUrlParse); var yaml__default = /*#__PURE__*/_interopDefaultCompat(yaml); var path__default = /*#__PURE__*/_interopDefaultCompat(path); function getGeneratorKey(entity) { if (!entity) { throw new Error("No entity provided"); } return "techdocs"; } const runCommand = async ({ command, args, options, logStream = new node_stream.PassThrough() }) => { await new Promise((resolve, reject) => { const process = node_child_process.spawn(command, args, options); process.stdout.on("data", (stream) => { logStream.write(stream); }); process.stderr.on("data", (stream) => { logStream.write(stream); }); process.on("error", (error) => { return reject(error); }); process.on("close", (code) => { if (code !== 0) { return reject(`Command ${command} failed, exit code: ${code}`); } return resolve(); }); }); }; const getRepoUrlFromLocationAnnotation = (parsedLocationAnnotation, scmIntegrations, docsFolder = "docs") => { const { type: locationType, target } = parsedLocationAnnotation; if (locationType === "url") { const integration = scmIntegrations.byUrl(target); if (integration && ["github", "gitlab", "bitbucketServer", "harness"].includes( integration.type )) { const { filepathtype } = gitUrlParse__default.default(target); if (filepathtype === "") { return { repo_url: target }; } const sourceFolder = integration.resolveUrl({ url: `./${docsFolder}`, base: target.endsWith("/") ? target : `${target}/` }); return { repo_url: target, edit_uri: integration.resolveEditUrl(sourceFolder) }; } } return {}; }; class UnknownTag { data; type; constructor(data, type) { this.data = data; this.type = type; } } const MKDOCS_SCHEMA = yaml.DEFAULT_SCHEMA.extend([ new yaml.Type("", { kind: "scalar", multi: true, representName: (o) => o.type, represent: (o) => o.data ?? "", instanceOf: UnknownTag, construct: (data, type) => new UnknownTag(data, type) }), new yaml.Type("tag:", { kind: "mapping", multi: true, representName: (o) => o.type, represent: (o) => o.data ?? "", instanceOf: UnknownTag, construct: (data, type) => new UnknownTag(data, type) }), new yaml.Type("", { kind: "sequence", multi: true, representName: (o) => o.type, represent: (o) => o.data ?? "", instanceOf: UnknownTag, construct: (data, type) => new UnknownTag(data, type) }) ]); const generateMkdocsYml = async (inputDir, siteOptions) => { try { const mkdocsYmlPath = path__default.default.join(inputDir, "mkdocs.yml"); const defaultSiteName = siteOptions?.name ?? "Documentation Site"; const defaultMkdocsContent = { site_name: defaultSiteName, docs_dir: "docs", plugins: ["techdocs-core"] }; await fs__default.default.writeFile( mkdocsYmlPath, yaml__default.default.dump(defaultMkdocsContent, { schema: MKDOCS_SCHEMA }) ); } catch (error) { throw new errors.ForwardedError("Could not generate mkdocs.yml file", error); } }; const getMkdocsYml = async (inputDir, options) => { let mkdocsYmlPath; let mkdocsYmlFileString; try { if (options?.mkdocsConfigFileName) { mkdocsYmlPath = path__default.default.join(inputDir, options.mkdocsConfigFileName); if (!await fs__default.default.pathExists(mkdocsYmlPath)) { throw new Error(`The specified file ${mkdocsYmlPath} does not exist`); } mkdocsYmlFileString = await fs__default.default.readFile(mkdocsYmlPath, "utf8"); return { path: mkdocsYmlPath, content: mkdocsYmlFileString, configIsTemporary: false }; } mkdocsYmlPath = path__default.default.join(inputDir, "mkdocs.yaml"); if (await fs__default.default.pathExists(mkdocsYmlPath)) { mkdocsYmlFileString = await fs__default.default.readFile(mkdocsYmlPath, "utf8"); return { path: mkdocsYmlPath, content: mkdocsYmlFileString, configIsTemporary: false }; } mkdocsYmlPath = path__default.default.join(inputDir, "mkdocs.yml"); if (await fs__default.default.pathExists(mkdocsYmlPath)) { mkdocsYmlFileString = await fs__default.default.readFile(mkdocsYmlPath, "utf8"); return { path: mkdocsYmlPath, content: mkdocsYmlFileString, configIsTemporary: false }; } await generateMkdocsYml(inputDir, options); mkdocsYmlFileString = await fs__default.default.readFile(mkdocsYmlPath, "utf8"); } catch (error) { throw new errors.ForwardedError( "Could not read MkDocs YAML config file mkdocs.yml or mkdocs.yaml or default for validation", error ); } return { path: mkdocsYmlPath, content: mkdocsYmlFileString, configIsTemporary: true }; }; const ALLOWED_MKDOCS_KEYS = /* @__PURE__ */ new Set([ // Site information "site_name", "site_url", "site_description", "site_author", // Repository "repo_url", "repo_name", "edit_uri", "edit_uri_template", // Build directories "docs_dir", "site_dir", // Documentation layout "nav", "exclude_docs", "not_in_nav", // Build settings "theme", "plugins", "markdown_extensions", "extra", "extra_css", "extra_templates", // Preview controls "use_directory_urls", "strict", "dev_addr", "watch", // Metadata "copyright", "remote_branch", "remote_name", "validation", // Deprecated "google_analytics" ]); const validateMkdocsYaml = async (inputDir, mkdocsYmlFileString) => { const mkdocsYml = yaml__default.default.load(mkdocsYmlFileString, { schema: MKDOCS_SCHEMA }); if (mkdocsYml === null || typeof mkdocsYml !== "object") { return void 0; } const parsedMkdocsYml = mkdocsYml; if (parsedMkdocsYml.docs_dir && !backendPluginApi.isChildPath(inputDir, path.resolve(inputDir, parsedMkdocsYml.docs_dir))) { throw new Error( `docs_dir configuration value in mkdocs can't be an absolute directory or start with ../ for security reasons. Use relative paths instead which are resolved relative to your mkdocs.yml file location.` ); } return parsedMkdocsYml.docs_dir; }; const validateDocsDirectory = async (docsDir, inputDir) => { const files = await helpers.getFileTreeRecursively(docsDir); for (const file of files) { if (!backendPluginApi.isChildPath(inputDir, file)) { throw new errors.NotAllowedError( `Path ${file} is not allowed to refer to a location outside ${inputDir}` ); } } }; const patchIndexPreBuild = async ({ inputDir, logger, docsDir = "docs" }) => { const docsPath = path__default.default.join(inputDir, docsDir); const indexMdPath = path__default.default.join(docsPath, "index.md"); if (await fs__default.default.pathExists(indexMdPath)) { return; } logger.warn(`${path__default.default.join(docsDir, "index.md")} not found.`); const fallbacks = [ path__default.default.join(docsPath, "README.md"), path__default.default.join(docsPath, "readme.md"), path__default.default.join(inputDir, "README.md"), path__default.default.join(inputDir, "readme.md") ]; if (!backendPluginApi.isChildPath(inputDir, docsPath)) { throw new errors.NotAllowedError( `Target path ${docsPath} is not allowed to refer to a location outside ${inputDir}` ); } await fs__default.default.ensureDir(docsPath); for (const filePath of fallbacks) { if (!backendPluginApi.isChildPath(inputDir, filePath)) { throw new errors.NotAllowedError( `Source path ${filePath} is not allowed to refer to a location outside ${inputDir}` ); } try { await fs__default.default.copyFile(filePath, indexMdPath); return; } catch (error) { logger.warn(`${path__default.default.relative(inputDir, filePath)} not found.`); } } logger.warn( `Could not find any techdocs' index file. Please make sure at least one of ${[ indexMdPath, ...fallbacks ].join(" ")} exists.` ); }; const createOrUpdateMetadata = async (techdocsMetadataPath, logger) => { const techdocsMetadataDir = techdocsMetadataPath.split(path__default.default.sep).slice(0, -1).join(path__default.default.sep); try { await fs__default.default.access(techdocsMetadataPath, fs__default.default.constants.F_OK); } catch (err) { await fs__default.default.writeJson(techdocsMetadataPath, JSON.parse("{}")); } let json; try { json = await fs__default.default.readJson(techdocsMetadataPath); } catch (err) { const message = `Invalid JSON at ${techdocsMetadataPath} with error ${errors.toError(err).message}`; logger.error(message); throw new Error(message); } json.build_timestamp = Date.now(); try { json.files = (await helpers.getFileTreeRecursively(techdocsMetadataDir)).map( (file) => file.replace(`${techdocsMetadataDir}${path__default.default.sep}`, "") ); } catch (err) { json.files = []; logger.warn( `Unable to add files list to metadata: ${errors.toError(err).message}` ); } await fs__default.default.writeJson(techdocsMetadataPath, json); return; }; const storeEtagMetadata = async (techdocsMetadataPath, etag) => { const json = await fs__default.default.readJson(techdocsMetadataPath); json.etag = etag; await fs__default.default.writeJson(techdocsMetadataPath, json); }; exports.ALLOWED_MKDOCS_KEYS = ALLOWED_MKDOCS_KEYS; exports.MKDOCS_SCHEMA = MKDOCS_SCHEMA; exports.createOrUpdateMetadata = createOrUpdateMetadata; exports.generateMkdocsYml = generateMkdocsYml; exports.getGeneratorKey = getGeneratorKey; exports.getMkdocsYml = getMkdocsYml; exports.getRepoUrlFromLocationAnnotation = getRepoUrlFromLocationAnnotation; exports.patchIndexPreBuild = patchIndexPreBuild; exports.runCommand = runCommand; exports.storeEtagMetadata = storeEtagMetadata; exports.validateDocsDirectory = validateDocsDirectory; exports.validateMkdocsYaml = validateMkdocsYaml; //# sourceMappingURL=helpers.cjs.js.map