@techdocs/cli 1.13.0 → 1.14.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/commands/generate/generate.cjs.js +9 -3
  3. package/dist/commands/generate/generate.cjs.js.map +1 -1
  4. package/dist/commands/index.cjs.js +17 -25
  5. package/dist/commands/index.cjs.js.map +1 -1
  6. package/dist/commands/serve/mkdocs.cjs.js +18 -7
  7. package/dist/commands/serve/mkdocs.cjs.js.map +1 -1
  8. package/dist/commands/serve/serve.cjs.js +26 -20
  9. package/dist/commands/serve/serve.cjs.js.map +1 -1
  10. package/dist/commands/serve/utils.cjs.js +11 -0
  11. package/dist/commands/serve/utils.cjs.js.map +1 -1
  12. package/dist/embedded-app/.config-schema.json +160 -160
  13. package/dist/embedded-app/index.html +1 -1
  14. package/dist/embedded-app/index.html.tmpl +1 -1
  15. package/dist/embedded-app/static/123.62e39427e387.chunk.js +2 -0
  16. package/dist/embedded-app/static/123.62e39427e387.chunk.js.map +1 -0
  17. package/dist/embedded-app/static/1659.834cbdaa7cf5.chunk.js +2 -0
  18. package/dist/embedded-app/static/1659.834cbdaa7cf5.chunk.js.map +1 -0
  19. package/dist/embedded-app/static/4587.9743bb6b94a7.chunk.js +2 -0
  20. package/dist/embedded-app/static/4587.9743bb6b94a7.chunk.js.map +1 -0
  21. package/dist/embedded-app/static/{4609.afdaf67e3d37.chunk.js → 4609.cac0ebf6bb92.chunk.js} +2 -2
  22. package/dist/embedded-app/static/{4609.afdaf67e3d37.chunk.js.map → 4609.cac0ebf6bb92.chunk.js.map} +1 -1
  23. package/dist/embedded-app/static/{5124.bf809bc06c78.chunk.js → 5124.7da4bbeaafd0.chunk.js} +2 -2
  24. package/dist/embedded-app/static/{5124.bf809bc06c78.chunk.js.map → 5124.7da4bbeaafd0.chunk.js.map} +1 -1
  25. package/dist/embedded-app/static/5877.dcbb54defb88.chunk.js +2 -0
  26. package/dist/embedded-app/static/5877.dcbb54defb88.chunk.js.map +1 -0
  27. package/dist/embedded-app/static/main.7dc68125121e.js.map +1 -1
  28. package/dist/embedded-app/static/{runtime.fa776ba09ef1.js → runtime.1a8d862d04e5.js} +2 -2
  29. package/dist/embedded-app/static/{runtime.fa776ba09ef1.js.map → runtime.1a8d862d04e5.js.map} +1 -1
  30. package/dist/index.cjs.js +1 -1
  31. package/dist/lib/{engineConfig.cjs.js → engines/mkdocs.cjs.js} +5 -24
  32. package/dist/lib/engines/mkdocs.cjs.js.map +1 -0
  33. package/dist/lib/engines/registry.cjs.js +29 -0
  34. package/dist/lib/engines/registry.cjs.js.map +1 -0
  35. package/dist/lib/engines/zensical.cjs.js +119 -0
  36. package/dist/lib/engines/zensical.cjs.js.map +1 -0
  37. package/dist/lib/httpServer.cjs.js +4 -2
  38. package/dist/lib/httpServer.cjs.js.map +1 -1
  39. package/dist/lib/livereload.cjs.js +9 -6
  40. package/dist/lib/livereload.cjs.js.map +1 -1
  41. package/dist/lib/prepareServeConfig.cjs.js +17 -0
  42. package/dist/lib/prepareServeConfig.cjs.js.map +1 -0
  43. package/dist/lib/server.cjs.js +2 -1
  44. package/dist/lib/server.cjs.js.map +1 -1
  45. package/dist/{package.json.cjs.js → packages/techdocs-cli/package.json.cjs.js} +1 -1
  46. package/dist/plugins/techdocs-zensical-internal/src/TechdocsZensicalGenerator.cjs.js +178 -0
  47. package/dist/plugins/techdocs-zensical-internal/src/TechdocsZensicalGenerator.cjs.js.map +1 -0
  48. package/dist/plugins/techdocs-zensical-internal/src/zensicalHelpers.cjs.js +185 -0
  49. package/dist/plugins/techdocs-zensical-internal/src/zensicalHelpers.cjs.js.map +1 -0
  50. package/package.json +13 -8
  51. package/dist/embedded-app/static/123.b7794a0eee64.chunk.js +0 -2
  52. package/dist/embedded-app/static/123.b7794a0eee64.chunk.js.map +0 -1
  53. package/dist/embedded-app/static/1659.b8b0e908d038.chunk.js +0 -2
  54. package/dist/embedded-app/static/1659.b8b0e908d038.chunk.js.map +0 -1
  55. package/dist/embedded-app/static/4587.fa2bf602f965.chunk.js +0 -2
  56. package/dist/embedded-app/static/4587.fa2bf602f965.chunk.js.map +0 -1
  57. package/dist/embedded-app/static/5877.a2e28865fc9e.chunk.js +0 -2
  58. package/dist/embedded-app/static/5877.a2e28865fc9e.chunk.js.map +0 -1
  59. package/dist/lib/engineConfig.cjs.js.map +0 -1
  60. /package/dist/{package.json.cjs.js.map → packages/techdocs-cli/package.json.cjs.js.map} +0 -0
@@ -0,0 +1,119 @@
1
+ 'use strict';
2
+
3
+ var path = require('node:path');
4
+ var node_crypto = require('node:crypto');
5
+ var fs = require('fs-extra');
6
+ var WebSocket = require('ws');
7
+ var livereload = require('../livereload.cjs.js');
8
+ var TechdocsZensicalGenerator = require('../../plugins/techdocs-zensical-internal/src/TechdocsZensicalGenerator.cjs.js');
9
+ var zensicalHelpers = require('../../plugins/techdocs-zensical-internal/src/zensicalHelpers.cjs.js');
10
+
11
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
12
+
13
+ var path__default = /*#__PURE__*/_interopDefaultCompat(path);
14
+ var fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
15
+ var WebSocket__default = /*#__PURE__*/_interopDefaultCompat(WebSocket);
16
+
17
+ let ws = null;
18
+ let pending = [];
19
+ function settleAll(settle) {
20
+ const queued = pending;
21
+ pending = [];
22
+ queued.forEach(settle);
23
+ }
24
+ function getWs(targetAddress) {
25
+ if (ws && (ws.readyState === WebSocket__default.default.OPEN || ws.readyState === WebSocket__default.default.CONNECTING)) {
26
+ return ws;
27
+ }
28
+ const socket = new WebSocket__default.default(targetAddress.replace(/^http/, "ws"));
29
+ ws = socket;
30
+ socket.on("message", (data) => {
31
+ const filePath = data.toString();
32
+ settleAll((waiter) => waiter.resolve(filePath));
33
+ });
34
+ const fail = (error) => {
35
+ if (ws === socket) {
36
+ ws = null;
37
+ settleAll((waiter) => waiter.reject(error));
38
+ }
39
+ };
40
+ socket.on("close", () => fail(new Error("Zensical WebSocket closed")));
41
+ socket.on("error", fail);
42
+ return socket;
43
+ }
44
+ function waitForReload(targetAddress) {
45
+ let waiter;
46
+ const reload = new Promise((resolve, reject) => {
47
+ waiter = { resolve, reject };
48
+ pending.push(waiter);
49
+ getWs(targetAddress);
50
+ });
51
+ const cancel = () => {
52
+ pending = pending.filter((w) => w !== waiter);
53
+ };
54
+ return { reload, cancel };
55
+ }
56
+ const LIVERELOAD_EPOCH = "1";
57
+ const LIVERELOAD_REQUEST_ID = "1";
58
+ const RELOAD_EPOCH = "999999";
59
+ const ZENSICAL_WS_SCRIPT_RE = /<script type="module">[\s\S]*?new WebSocket[\s\S]*?<\/script>/;
60
+ const zensicalConfig = {
61
+ binary: "zensical",
62
+ defaultImage: TechdocsZensicalGenerator.TechdocsZensicalGenerator.defaultDockerImage,
63
+ serveCommand: "serve",
64
+ // e.g. "Serving /path/to/site on http://127.0.0.1:8000"
65
+ startupLogPattern: /Serving .+ on https?:\/\//,
66
+ buildArgs(_outputDir, configFile) {
67
+ return ["build", "-f", configFile];
68
+ },
69
+ serveArgs(port, options) {
70
+ const addr = options.useDocker ? `0.0.0.0:${port}` : `127.0.0.1:${port}`;
71
+ return [
72
+ "serve",
73
+ "--dev-addr",
74
+ addr,
75
+ ...options.configFile ? ["-f", options.configFile] : []
76
+ ];
77
+ },
78
+ createGenerator(config, options) {
79
+ return TechdocsZensicalGenerator.TechdocsZensicalGenerator.fromConfig(config, options);
80
+ },
81
+ async prepareServeConfig(configPath, logger) {
82
+ const siteDir = `.techdocs-zensical-serve-${node_crypto.randomUUID()}`;
83
+ const patchedPath = await zensicalHelpers.patchZensicalConfig(configPath, siteDir, logger);
84
+ return {
85
+ configPath: patchedPath,
86
+ cleanup() {
87
+ fs__default.default.removeSync(patchedPath);
88
+ fs__default.default.removeSync(path__default.default.join(path__default.default.dirname(configPath), siteDir));
89
+ }
90
+ };
91
+ },
92
+ transformHtml(html) {
93
+ return livereload.injectLiveReloadElement(
94
+ html.replace(ZENSICAL_WS_SCRIPT_RE, ""),
95
+ LIVERELOAD_EPOCH,
96
+ LIVERELOAD_REQUEST_ID
97
+ );
98
+ },
99
+ handleReloadRequest(_req, res, targetAddress, onError) {
100
+ livereload.setCorsHeaders(res);
101
+ const { reload, cancel } = waitForReload(targetAddress);
102
+ res.on("close", () => {
103
+ if (!res.writableEnded) {
104
+ cancel();
105
+ }
106
+ });
107
+ reload.then(() => {
108
+ res.statusCode = 200;
109
+ res.end(RELOAD_EPOCH);
110
+ }).catch((error) => {
111
+ onError(error);
112
+ res.statusCode = 503;
113
+ res.end("0");
114
+ });
115
+ }
116
+ };
117
+
118
+ exports.zensicalConfig = zensicalConfig;
119
+ //# sourceMappingURL=zensical.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zensical.cjs.js","sources":["../../../src/lib/engines/zensical.ts"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport http from 'node:http';\nimport path from 'node:path';\nimport { randomUUID } from 'node:crypto';\nimport fs from 'fs-extra';\nimport WebSocket from 'ws';\nimport {\n TechdocsZensicalGenerator,\n patchZensicalConfig,\n} from '@internal/techdocs-zensical';\nimport { injectLiveReloadElement, setCorsHeaders } from '../livereload';\nimport { EngineConfig } from './types';\n\nlet ws: WebSocket | null = null;\ntype Waiter = {\n resolve: (filePath: string) => void;\n reject: (e: Error) => void;\n};\nlet pending: Waiter[] = [];\n\nfunction settleAll(settle: (waiter: Waiter) => void) {\n const queued = pending;\n pending = [];\n queued.forEach(settle);\n}\n\nfunction getWs(targetAddress: string): WebSocket {\n if (\n ws &&\n (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)\n ) {\n return ws;\n }\n\n const socket = new WebSocket(targetAddress.replace(/^http/, 'ws'));\n ws = socket;\n\n socket.on('message', (data: WebSocket.Data) => {\n const filePath = data.toString();\n settleAll(waiter => waiter.resolve(filePath));\n });\n\n const fail = (error: Error) => {\n if (ws === socket) {\n ws = null;\n settleAll(waiter => waiter.reject(error));\n }\n };\n socket.on('close', () => fail(new Error('Zensical WebSocket closed')));\n socket.on('error', fail);\n\n return socket;\n}\n\n/**\n * Resolves on Zensical's next reload message. Call the returned cancel\n * function to stop waiting, e.g. when the browser tab goes away.\n */\nfunction waitForReload(targetAddress: string): {\n reload: Promise<string>;\n cancel: () => void;\n} {\n let waiter: Waiter | undefined;\n const reload = new Promise<string>((resolve, reject) => {\n waiter = { resolve, reject };\n pending.push(waiter);\n getWs(targetAddress);\n });\n const cancel = () => {\n pending = pending.filter(w => w !== waiter);\n };\n return { reload, cancel };\n}\n\nconst LIVERELOAD_EPOCH = '1';\nconst LIVERELOAD_REQUEST_ID = '1';\nconst RELOAD_EPOCH = '999999';\n\n// Zensical injects a <script type=\"module\"> that opens a WebSocket back to\n// the dev server for livereload. When proxied through the embedded preview\n// app, that script would try to connect to the wrong port. We strip it and\n// replace it with the <live-reload> custom element that the preview app's\n// LiveReloadAddon understands (HTTP long-poll at /.livereload).\nconst ZENSICAL_WS_SCRIPT_RE =\n /<script type=\"module\">[\\s\\S]*?new WebSocket[\\s\\S]*?<\\/script>/;\n\nexport const zensicalConfig: EngineConfig = {\n binary: 'zensical',\n defaultImage: TechdocsZensicalGenerator.defaultDockerImage,\n serveCommand: 'serve',\n // e.g. \"Serving /path/to/site on http://127.0.0.1:8000\"\n startupLogPattern: /Serving .+ on https?:\\/\\//,\n buildArgs(_outputDir: string, configFile: string): string[] {\n return ['build', '-f', configFile];\n },\n serveArgs(port: string, options): string[] {\n const addr = options.useDocker ? `0.0.0.0:${port}` : `127.0.0.1:${port}`;\n return [\n 'serve',\n '--dev-addr',\n addr,\n ...(options.configFile ? ['-f', options.configFile] : []),\n ];\n },\n createGenerator(config, options) {\n return TechdocsZensicalGenerator.fromConfig(config, options);\n },\n async prepareServeConfig(configPath, logger) {\n // Same patching the backend generator applies, so preview == published.\n const siteDir = `.techdocs-zensical-serve-${randomUUID()}`;\n const patchedPath = await patchZensicalConfig(configPath, siteDir, logger);\n return {\n configPath: patchedPath,\n cleanup() {\n fs.removeSync(patchedPath);\n fs.removeSync(path.join(path.dirname(configPath), siteDir));\n },\n };\n },\n transformHtml(html: string): string {\n return injectLiveReloadElement(\n html.replace(ZENSICAL_WS_SCRIPT_RE, ''),\n LIVERELOAD_EPOCH,\n LIVERELOAD_REQUEST_ID,\n );\n },\n handleReloadRequest(\n _req: http.IncomingMessage,\n res: http.ServerResponse,\n targetAddress: string,\n onError: (error: Error) => void,\n ): void {\n setCorsHeaders(res);\n\n const { reload, cancel } = waitForReload(targetAddress);\n // The response closes without ending when the tab navigates away or is\n // closed; stop waiting so the long-poll doesn't linger until next reload.\n res.on('close', () => {\n if (!res.writableEnded) {\n cancel();\n }\n });\n\n reload\n .then(() => {\n res.statusCode = 200;\n res.end(RELOAD_EPOCH);\n })\n .catch((error: Error) => {\n onError(error);\n // Non-200 makes the preview app's LiveReloadAddon retry after 3s.\n res.statusCode = 503;\n res.end('0');\n });\n },\n};\n"],"names":["WebSocket","TechdocsZensicalGenerator","randomUUID","patchZensicalConfig","fs","path","injectLiveReloadElement","setCorsHeaders"],"mappings":"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"}
@@ -40,6 +40,7 @@ class HTTPServer {
40
40
  };
41
41
  }
42
42
  async serve() {
43
+ const logger = utility.createLogger({ verbose: false });
43
44
  return new Promise((resolve, reject) => {
44
45
  const proxyHandler = this.createProxy();
45
46
  const server = http__default.default.createServer(
@@ -84,7 +85,9 @@ class HTTPServer {
84
85
  request,
85
86
  response,
86
87
  this.docsTargetAddress,
87
- (error) => reject(error)
88
+ // The server is already listening, so rejecting would be a
89
+ // no-op; log instead so engine connection problems are visible.
90
+ (error) => logger.warn(`Live reload unavailable: ${error.message}`)
88
91
  );
89
92
  return;
90
93
  }
@@ -95,7 +98,6 @@ class HTTPServer {
95
98
  });
96
99
  }
97
100
  );
98
- const logger = utility.createLogger({ verbose: false });
99
101
  server.listen(this.backstagePort, () => {
100
102
  if (this.verbose) {
101
103
  logger.info(
@@ -1 +1 @@
1
- {"version":3,"file":"httpServer.cjs.js","sources":["../../src/lib/httpServer.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport serveHandler from 'serve-handler';\nimport http from 'node:http';\nimport httpProxy from 'http-proxy';\nimport { createLogger } from './utility';\nimport { proxyHtmlWithLivereloadInjection } from './livereload';\nimport { EngineConfig } from './engineConfig';\n\nexport default class HTTPServer {\n private readonly proxyEndpoint: string;\n private readonly backstageBundleDir: string;\n private readonly backstagePort: number;\n private readonly docsTargetAddress: string;\n private readonly verbose: boolean;\n private readonly engineConfig: EngineConfig;\n\n constructor(\n backstageBundleDir: string,\n backstagePort: number,\n docsTargetAddress: string,\n verbose: boolean,\n engineConfig: EngineConfig,\n ) {\n this.proxyEndpoint = '/api/techdocs/';\n this.backstageBundleDir = backstageBundleDir;\n this.backstagePort = backstagePort;\n this.docsTargetAddress = docsTargetAddress;\n this.verbose = verbose;\n this.engineConfig = engineConfig;\n }\n\n private createProxy() {\n const proxy = httpProxy.createProxyServer({\n target: this.docsTargetAddress,\n });\n\n return (request: http.IncomingMessage): [httpProxy, string] => {\n // If the request path is prefixed with this.proxyEndpoint, remove it.\n const proxyEndpointPath = new RegExp(`^${this.proxyEndpoint}`, 'i');\n const forwardPath = request.url?.replace(proxyEndpointPath, '') || '';\n\n return [proxy, forwardPath];\n };\n }\n\n public async serve(): Promise<http.Server> {\n return new Promise<http.Server>((resolve, reject) => {\n const proxyHandler = this.createProxy();\n const server = http.createServer(\n (request: http.IncomingMessage, response: http.ServerResponse) => {\n // This endpoint is used by the frontend to issue a cookie for the user.\n // But the MkDocs server doesn't expose it as a the Backstage backend does.\n // So we need to fake it here to prevent 404 errors.\n if (request.url === '/api/techdocs/.backstage/auth/v1/cookie') {\n const oneHourInMilliseconds = 60 * 60 * 1000;\n const expiresAt = new Date(Date.now() + oneHourInMilliseconds);\n const cookie = { expiresAt: expiresAt.toISOString() };\n response.setHeader('Content-Type', 'application/json');\n response.end(JSON.stringify(cookie));\n return;\n }\n\n if (request.url?.startsWith(this.proxyEndpoint)) {\n if (request.url?.endsWith('.html')) {\n proxyHtmlWithLivereloadInjection({\n request,\n response,\n targetAddress: this.docsTargetAddress,\n proxyEndpoint: this.proxyEndpoint,\n transformHtml: html => this.engineConfig.transformHtml(html),\n onError: (error: Error) => reject(error),\n });\n return;\n }\n\n const [proxy, forwardPath] = proxyHandler(request);\n\n proxy.on('error', (error: Error) => {\n reject(error);\n });\n\n response.setHeader('Access-Control-Allow-Origin', '*');\n response.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');\n\n request.url = forwardPath;\n proxy.web(request, response);\n return;\n }\n\n if (request.url === '/.detect') {\n response.setHeader('Content-Type', 'text/plain');\n response.end('techdocs-cli-server');\n return;\n }\n\n if (request.url?.startsWith('/.livereload')) {\n this.engineConfig.handleReloadRequest(\n request,\n response,\n this.docsTargetAddress,\n (error: Error) => reject(error),\n );\n return;\n }\n\n serveHandler(request, response, {\n public: this.backstageBundleDir,\n trailingSlash: true,\n rewrites: [{ source: '**', destination: 'index.html' }],\n });\n },\n );\n\n const logger = createLogger({ verbose: false });\n server.listen(this.backstagePort, () => {\n if (this.verbose) {\n logger.info(\n `[techdocs-preview-bundle] Running local version of Backstage at http://localhost:${this.backstagePort}`,\n );\n }\n resolve(server);\n });\n\n server.on('error', (error: Error) => {\n reject(error);\n });\n });\n }\n}\n"],"names":["httpProxy","http","proxyHtmlWithLivereloadInjection","serveHandler","createLogger"],"mappings":"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"}
1
+ {"version":3,"file":"httpServer.cjs.js","sources":["../../src/lib/httpServer.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport serveHandler from 'serve-handler';\nimport http from 'node:http';\nimport httpProxy from 'http-proxy';\nimport { createLogger } from './utility';\nimport { proxyHtmlWithLivereloadInjection } from './livereload';\nimport { EngineConfig } from './engines';\n\nexport default class HTTPServer {\n private readonly proxyEndpoint: string;\n private readonly backstageBundleDir: string;\n private readonly backstagePort: number;\n private readonly docsTargetAddress: string;\n private readonly verbose: boolean;\n private readonly engineConfig: EngineConfig;\n\n constructor(\n backstageBundleDir: string,\n backstagePort: number,\n docsTargetAddress: string,\n verbose: boolean,\n engineConfig: EngineConfig,\n ) {\n this.proxyEndpoint = '/api/techdocs/';\n this.backstageBundleDir = backstageBundleDir;\n this.backstagePort = backstagePort;\n this.docsTargetAddress = docsTargetAddress;\n this.verbose = verbose;\n this.engineConfig = engineConfig;\n }\n\n private createProxy() {\n const proxy = httpProxy.createProxyServer({\n target: this.docsTargetAddress,\n });\n\n return (request: http.IncomingMessage): [httpProxy, string] => {\n // If the request path is prefixed with this.proxyEndpoint, remove it.\n const proxyEndpointPath = new RegExp(`^${this.proxyEndpoint}`, 'i');\n const forwardPath = request.url?.replace(proxyEndpointPath, '') || '';\n\n return [proxy, forwardPath];\n };\n }\n\n public async serve(): Promise<http.Server> {\n const logger = createLogger({ verbose: false });\n return new Promise<http.Server>((resolve, reject) => {\n const proxyHandler = this.createProxy();\n const server = http.createServer(\n (request: http.IncomingMessage, response: http.ServerResponse) => {\n // This endpoint is used by the frontend to issue a cookie for the user.\n // But the MkDocs server doesn't expose it as a the Backstage backend does.\n // So we need to fake it here to prevent 404 errors.\n if (request.url === '/api/techdocs/.backstage/auth/v1/cookie') {\n const oneHourInMilliseconds = 60 * 60 * 1000;\n const expiresAt = new Date(Date.now() + oneHourInMilliseconds);\n const cookie = { expiresAt: expiresAt.toISOString() };\n response.setHeader('Content-Type', 'application/json');\n response.end(JSON.stringify(cookie));\n return;\n }\n\n if (request.url?.startsWith(this.proxyEndpoint)) {\n if (request.url?.endsWith('.html')) {\n proxyHtmlWithLivereloadInjection({\n request,\n response,\n targetAddress: this.docsTargetAddress,\n proxyEndpoint: this.proxyEndpoint,\n transformHtml: html => this.engineConfig.transformHtml(html),\n onError: (error: Error) => reject(error),\n });\n return;\n }\n\n const [proxy, forwardPath] = proxyHandler(request);\n\n proxy.on('error', (error: Error) => {\n reject(error);\n });\n\n response.setHeader('Access-Control-Allow-Origin', '*');\n response.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');\n\n request.url = forwardPath;\n proxy.web(request, response);\n return;\n }\n\n if (request.url === '/.detect') {\n response.setHeader('Content-Type', 'text/plain');\n response.end('techdocs-cli-server');\n return;\n }\n\n if (request.url?.startsWith('/.livereload')) {\n this.engineConfig.handleReloadRequest(\n request,\n response,\n this.docsTargetAddress,\n // The server is already listening, so rejecting would be a\n // no-op; log instead so engine connection problems are visible.\n (error: Error) =>\n logger.warn(`Live reload unavailable: ${error.message}`),\n );\n return;\n }\n\n serveHandler(request, response, {\n public: this.backstageBundleDir,\n trailingSlash: true,\n rewrites: [{ source: '**', destination: 'index.html' }],\n });\n },\n );\n\n server.listen(this.backstagePort, () => {\n if (this.verbose) {\n logger.info(\n `[techdocs-preview-bundle] Running local version of Backstage at http://localhost:${this.backstagePort}`,\n );\n }\n resolve(server);\n });\n\n server.on('error', (error: Error) => {\n reject(error);\n });\n });\n }\n}\n"],"names":["httpProxy","createLogger","http","proxyHtmlWithLivereloadInjection","serveHandler"],"mappings":"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"}
@@ -21,15 +21,16 @@ function injectLivereloadParameters(html) {
21
21
  return html;
22
22
  }
23
23
  const [, epoch, requestId] = livereloadMatch;
24
- const liveReloadTag = `<${LIVE_RELOAD_ELEMENT} ${LIVE_RELOAD_ATTR_EPOCH}="${epoch}" ${LIVE_RELOAD_ATTR_REQUEST_ID}="${requestId}"></${LIVE_RELOAD_ELEMENT}>`;
24
+ return injectLiveReloadElement(html, epoch, requestId);
25
+ }
26
+ function injectLiveReloadElement(html, epoch, requestId) {
25
27
  const bodyStart = html.match(BODY_START_RE);
26
- const bodyStartIndex = bodyStart?.index ?? 0;
27
- const bodyStartLength = bodyStart?.[0]?.length ?? 0;
28
- if (bodyStartIndex === 0 || bodyStartLength === 0) {
28
+ if (!bodyStart || bodyStart.index === void 0) {
29
29
  return html;
30
30
  }
31
- const bodyEndIndex = bodyStartIndex + bodyStartLength;
32
- return html.slice(0, bodyEndIndex) + liveReloadTag + html.slice(bodyEndIndex);
31
+ const insertAt = bodyStart.index + bodyStart[0].length;
32
+ const liveReloadTag = `<${LIVE_RELOAD_ELEMENT} ${LIVE_RELOAD_ATTR_EPOCH}="${epoch}" ${LIVE_RELOAD_ATTR_REQUEST_ID}="${requestId}"></${LIVE_RELOAD_ELEMENT}>`;
33
+ return html.slice(0, insertAt) + liveReloadTag + html.slice(insertAt);
33
34
  }
34
35
  function setCorsHeaders(response) {
35
36
  response.setHeader("Access-Control-Allow-Origin", "*");
@@ -96,7 +97,9 @@ function proxyMkdocsLivereload(options) {
96
97
  proxy.web(request, response);
97
98
  }
98
99
 
100
+ exports.injectLiveReloadElement = injectLiveReloadElement;
99
101
  exports.injectLivereloadParameters = injectLivereloadParameters;
100
102
  exports.proxyHtmlWithLivereloadInjection = proxyHtmlWithLivereloadInjection;
101
103
  exports.proxyMkdocsLivereload = proxyMkdocsLivereload;
104
+ exports.setCorsHeaders = setCorsHeaders;
102
105
  //# sourceMappingURL=livereload.cjs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"livereload.cjs.js","sources":["../../src/lib/livereload.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport http from 'node:http';\nimport httpProxy from 'http-proxy';\n\n/**\n * Livereload support for techdocs-cli.\n *\n * Context:\n * - MkDocs implements autoreload using a long-poll endpoint `/livereload` and a script that injects\n * a call like: `livereload(epoch, request_id)`, where `epoch` is derived from Python's\n * `time.monotonic()`.\n * - Node.js monotonic clocks (`process.hrtime`/`performance.now`) are not compatible with Python's\n * value across processes and platforms. We therefore CANNOT reliably re-create the same epoch on\n * the frontend or in this CLI, and must read the values produced by MkDocs itself.\n * - The MkDocs script tag is removed by DOM sanitization (DomPurify) in TechDocs, so we can't rely\n * on the script being present in the embedded app. To bridge this, we extract the parameters on\n * the server side while proxying HTML and inject them as a safe custom element that survives\n * sanitization: `<live-reload live-reload-epoch=\"…\" live-reload-request-id=\"…\"/>`.\n * - The frontend addon reads that element and polls `/.livereload` (served by techdocs-cli), which\n * this module maps to MkDocs `/livereload` with permissive CORS headers.\n * - Quality-of-life: if extraction fails or the endpoint is unavailable, normal docs still work.\n *\n * See issue for background and rationale: https://github.com/backstage/backstage/issues/30514\n */\n\nconst LIVE_RELOAD_ELEMENT = 'live-reload';\nconst LIVE_RELOAD_ATTR_EPOCH = 'live-reload-epoch';\nconst LIVE_RELOAD_ATTR_REQUEST_ID = 'live-reload-request-id';\nconst CLI_LIVERELOAD_PATH = '/.livereload';\nconst MKDOCS_LIVERELOAD_PATH = '/livereload';\nconst CONTENT_TYPE_HTML = 'text/html';\nconst HEADER_CONTENT_LENGTH = 'content-length';\n\nconst BODY_START_RE = /<body\\b[^>]*>/;\n// Matches mkdocs injected call livereload(epoch, requestId)\nconst MKDOCS_LIVERELOAD_CALL_RE = /livereload\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\)\\s*;?/;\n\n/**\n * Extract livereload parameters from mkdocs HTML and inject them as a custom element.\n * The injected element will later be read by the frontend addon even after DOM sanitization.\n *\n * Note:\n * - we don't add to <head> because of DomPurify sanitization.\n * - we add close to the body opening to avoid reading too far into the body.\n * - we should use streamed injection to improve performance.\n */\nexport function injectLivereloadParameters(html: string): string {\n const livereloadMatch = html.match(MKDOCS_LIVERELOAD_CALL_RE);\n\n // If we couldn't find livereload parameters, return original HTML untouched.\n if (!livereloadMatch) {\n return html;\n }\n\n const [, epoch, requestId] = livereloadMatch;\n // Insert a minimal custom element that the frontend addon can discover post-sanitization.\n // Note: embedded app needs a custom config to allow the element to survive sanitization.\n const liveReloadTag = `<${LIVE_RELOAD_ELEMENT} ${LIVE_RELOAD_ATTR_EPOCH}=\"${epoch}\" ${LIVE_RELOAD_ATTR_REQUEST_ID}=\"${requestId}\"></${LIVE_RELOAD_ELEMENT}>`;\n\n // Naively find where to insert the livereload tag.\n const bodyStart = html.match(BODY_START_RE);\n const bodyStartIndex = bodyStart?.index ?? 0;\n const bodyStartLength = bodyStart?.[0]?.length ?? 0;\n if (bodyStartIndex === 0 || bodyStartLength === 0) {\n return html;\n }\n const bodyEndIndex = bodyStartIndex + bodyStartLength;\n\n return html.slice(0, bodyEndIndex) + liveReloadTag + html.slice(bodyEndIndex);\n}\n\nfunction setCorsHeaders(response: http.ServerResponse) {\n response.setHeader('Access-Control-Allow-Origin', '*');\n response.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');\n}\n\n/**\n * Proxies a mkdocs HTML response, injecting livereload parameters into the HTML body.\n */\nexport function proxyHtmlWithLivereloadInjection(options: {\n request: http.IncomingMessage;\n response: http.ServerResponse;\n targetAddress: string;\n proxyEndpoint: string;\n transformHtml: (html: string) => string;\n onError: (error: Error) => void;\n}): void {\n const {\n request,\n response,\n targetAddress,\n proxyEndpoint,\n transformHtml,\n onError,\n } = options;\n\n const htmlProxy = httpProxy.createProxyServer({\n target: targetAddress,\n selfHandleResponse: true,\n });\n\n htmlProxy.on('error', onError);\n\n htmlProxy.on('proxyRes', (proxyRes, _req, res) => {\n const contentType = proxyRes.headers['content-type'];\n const contentEncoding = proxyRes.headers['content-encoding'];\n const isHtml =\n contentType &&\n typeof contentType === 'string' &&\n contentType.startsWith(CONTENT_TYPE_HTML);\n if (isHtml && !contentEncoding) {\n const chunks: Buffer[] = [];\n proxyRes.on('data', (chunk: Buffer | string) => {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));\n });\n proxyRes.on('end', () => {\n const body = Buffer.concat(chunks).toString('utf8');\n const modifiedHtml = transformHtml(body);\n res.statusCode = (proxyRes.statusCode as number | undefined) ?? 200;\n Object.keys(proxyRes.headers).forEach(key => {\n if (key.toLowerCase() !== HEADER_CONTENT_LENGTH) {\n res.setHeader(key, proxyRes.headers[key]!);\n }\n });\n setCorsHeaders(res);\n res.end(modifiedHtml);\n });\n } else {\n res.statusCode = (proxyRes.statusCode as number | undefined) ?? 200;\n Object.keys(proxyRes.headers).forEach(key => {\n res.setHeader(key, proxyRes.headers[key]!);\n });\n setCorsHeaders(res);\n proxyRes.pipe(res);\n }\n });\n\n const forwardPath =\n request.url?.replace(new RegExp(`^${proxyEndpoint}`, 'i'), '') || '';\n request.url = forwardPath;\n htmlProxy.web(request, response);\n}\n\n/**\n * Proxies mkdocs livereload long-polling requests, mapping the CLI path to mkdocs path.\n */\nexport function proxyMkdocsLivereload(options: {\n request: http.IncomingMessage;\n response: http.ServerResponse;\n mkdocsTargetAddress: string;\n onError: (error: Error) => void;\n}): void {\n const { request, response, mkdocsTargetAddress, onError } = options;\n\n const proxy = httpProxy.createProxyServer({ target: mkdocsTargetAddress });\n proxy.on('error', onError);\n\n setCorsHeaders(response);\n response.setHeader('Access-Control-Allow-Headers', 'Content-Type');\n\n request.url = request.url?.replace(\n CLI_LIVERELOAD_PATH,\n MKDOCS_LIVERELOAD_PATH,\n );\n proxy.web(request, response);\n}\n"],"names":["httpProxy"],"mappings":"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"}
1
+ {"version":3,"file":"livereload.cjs.js","sources":["../../src/lib/livereload.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport http from 'node:http';\nimport httpProxy from 'http-proxy';\n\n/**\n * Livereload support for techdocs-cli.\n *\n * Context:\n * - MkDocs implements autoreload using a long-poll endpoint `/livereload` and a script that injects\n * a call like: `livereload(epoch, request_id)`, where `epoch` is derived from Python's\n * `time.monotonic()`.\n * - Node.js monotonic clocks (`process.hrtime`/`performance.now`) are not compatible with Python's\n * value across processes and platforms. We therefore CANNOT reliably re-create the same epoch on\n * the frontend or in this CLI, and must read the values produced by MkDocs itself.\n * - The MkDocs script tag is removed by DOM sanitization (DomPurify) in TechDocs, so we can't rely\n * on the script being present in the embedded app. To bridge this, we extract the parameters on\n * the server side while proxying HTML and inject them as a safe custom element that survives\n * sanitization: `<live-reload live-reload-epoch=\"…\" live-reload-request-id=\"…\"/>`.\n * - The frontend addon reads that element and polls `/.livereload` (served by techdocs-cli), which\n * this module maps to MkDocs `/livereload` with permissive CORS headers.\n * - Quality-of-life: if extraction fails or the endpoint is unavailable, normal docs still work.\n *\n * See issue for background and rationale: https://github.com/backstage/backstage/issues/30514\n */\n\nconst LIVE_RELOAD_ELEMENT = 'live-reload';\nconst LIVE_RELOAD_ATTR_EPOCH = 'live-reload-epoch';\nconst LIVE_RELOAD_ATTR_REQUEST_ID = 'live-reload-request-id';\nconst CLI_LIVERELOAD_PATH = '/.livereload';\nconst MKDOCS_LIVERELOAD_PATH = '/livereload';\nconst CONTENT_TYPE_HTML = 'text/html';\nconst HEADER_CONTENT_LENGTH = 'content-length';\n\nconst BODY_START_RE = /<body\\b[^>]*>/;\n// Matches mkdocs injected call livereload(epoch, requestId)\nconst MKDOCS_LIVERELOAD_CALL_RE = /livereload\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\)\\s*;?/;\n\n/**\n * Extract livereload parameters from mkdocs HTML and inject them as a custom element.\n * The injected element will later be read by the frontend addon even after DOM sanitization.\n *\n * Note: we should use streamed injection to improve performance.\n */\nexport function injectLivereloadParameters(html: string): string {\n const livereloadMatch = html.match(MKDOCS_LIVERELOAD_CALL_RE);\n\n // If we couldn't find livereload parameters, return original HTML untouched.\n if (!livereloadMatch) {\n return html;\n }\n\n const [, epoch, requestId] = livereloadMatch;\n return injectLiveReloadElement(html, epoch, requestId);\n}\n\n/**\n * Inserts the `<live-reload>` element right after `<body>` so the preview\n * app's LiveReloadAddon can find it after DOM sanitization.\n *\n * Note:\n * - we don't add to <head> because of DomPurify sanitization.\n * - we add close to the body opening to avoid reading too far into the body.\n */\nexport function injectLiveReloadElement(\n html: string,\n epoch: string,\n requestId: string,\n): string {\n const bodyStart = html.match(BODY_START_RE);\n if (!bodyStart || bodyStart.index === undefined) {\n return html;\n }\n const insertAt = bodyStart.index + bodyStart[0].length;\n // Note: embedded app needs a custom config to allow the element to survive sanitization.\n const liveReloadTag = `<${LIVE_RELOAD_ELEMENT} ${LIVE_RELOAD_ATTR_EPOCH}=\"${epoch}\" ${LIVE_RELOAD_ATTR_REQUEST_ID}=\"${requestId}\"></${LIVE_RELOAD_ELEMENT}>`;\n return html.slice(0, insertAt) + liveReloadTag + html.slice(insertAt);\n}\n\nexport function setCorsHeaders(response: http.ServerResponse) {\n response.setHeader('Access-Control-Allow-Origin', '*');\n response.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');\n}\n\n/**\n * Proxies a mkdocs HTML response, injecting livereload parameters into the HTML body.\n */\nexport function proxyHtmlWithLivereloadInjection(options: {\n request: http.IncomingMessage;\n response: http.ServerResponse;\n targetAddress: string;\n proxyEndpoint: string;\n transformHtml: (html: string) => string;\n onError: (error: Error) => void;\n}): void {\n const {\n request,\n response,\n targetAddress,\n proxyEndpoint,\n transformHtml,\n onError,\n } = options;\n\n const htmlProxy = httpProxy.createProxyServer({\n target: targetAddress,\n selfHandleResponse: true,\n });\n\n htmlProxy.on('error', onError);\n\n htmlProxy.on('proxyRes', (proxyRes, _req, res) => {\n const contentType = proxyRes.headers['content-type'];\n const contentEncoding = proxyRes.headers['content-encoding'];\n const isHtml =\n contentType &&\n typeof contentType === 'string' &&\n contentType.startsWith(CONTENT_TYPE_HTML);\n if (isHtml && !contentEncoding) {\n const chunks: Buffer[] = [];\n proxyRes.on('data', (chunk: Buffer | string) => {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));\n });\n proxyRes.on('end', () => {\n const body = Buffer.concat(chunks).toString('utf8');\n const modifiedHtml = transformHtml(body);\n res.statusCode = (proxyRes.statusCode as number | undefined) ?? 200;\n Object.keys(proxyRes.headers).forEach(key => {\n if (key.toLowerCase() !== HEADER_CONTENT_LENGTH) {\n res.setHeader(key, proxyRes.headers[key]!);\n }\n });\n setCorsHeaders(res);\n res.end(modifiedHtml);\n });\n } else {\n res.statusCode = (proxyRes.statusCode as number | undefined) ?? 200;\n Object.keys(proxyRes.headers).forEach(key => {\n res.setHeader(key, proxyRes.headers[key]!);\n });\n setCorsHeaders(res);\n proxyRes.pipe(res);\n }\n });\n\n const forwardPath =\n request.url?.replace(new RegExp(`^${proxyEndpoint}`, 'i'), '') || '';\n request.url = forwardPath;\n htmlProxy.web(request, response);\n}\n\n/**\n * Proxies mkdocs livereload long-polling requests, mapping the CLI path to mkdocs path.\n */\nexport function proxyMkdocsLivereload(options: {\n request: http.IncomingMessage;\n response: http.ServerResponse;\n mkdocsTargetAddress: string;\n onError: (error: Error) => void;\n}): void {\n const { request, response, mkdocsTargetAddress, onError } = options;\n\n const proxy = httpProxy.createProxyServer({ target: mkdocsTargetAddress });\n proxy.on('error', onError);\n\n setCorsHeaders(response);\n response.setHeader('Access-Control-Allow-Headers', 'Content-Type');\n\n request.url = request.url?.replace(\n CLI_LIVERELOAD_PATH,\n MKDOCS_LIVERELOAD_PATH,\n );\n proxy.web(request, response);\n}\n"],"names":["httpProxy"],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ async function prepareServeConfig(engineConfig, configPath, logger) {
4
+ if (!engineConfig.prepareServeConfig) {
5
+ return configPath;
6
+ }
7
+ const prepared = await engineConfig.prepareServeConfig(configPath, logger);
8
+ process.on("exit", () => prepared.cleanup());
9
+ const exitCodes = { SIGINT: 130, SIGTERM: 143 };
10
+ for (const [signal, code] of Object.entries(exitCodes)) {
11
+ process.once(signal, () => process.exit(code));
12
+ }
13
+ return prepared.configPath;
14
+ }
15
+
16
+ exports.prepareServeConfig = prepareServeConfig;
17
+ //# sourceMappingURL=prepareServeConfig.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prepareServeConfig.cjs.js","sources":["../../src/lib/prepareServeConfig.ts"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { EngineConfig } from './engines';\n\n/**\n * Lets the engine rewrite its config before serving and registers cleanup of\n * any temporary files on exit. Returns the config path to serve.\n */\nexport async function prepareServeConfig(\n engineConfig: EngineConfig,\n configPath: string,\n logger: { info(message: string): void },\n): Promise<string> {\n if (!engineConfig.prepareServeConfig) {\n return configPath;\n }\n\n const prepared = await engineConfig.prepareServeConfig(configPath, logger);\n process.on('exit', () => prepared.cleanup());\n // Signals skip the 'exit' event unless we exit explicitly. Exit with the\n // conventional 128 + signal number.\n const exitCodes = { SIGINT: 130, SIGTERM: 143 } as const;\n for (const [signal, code] of Object.entries(exitCodes)) {\n process.once(signal, () => process.exit(code));\n }\n return prepared.configPath;\n}\n"],"names":[],"mappings":";;AAsBA,eAAsB,kBAAA,CACpB,YAAA,EACA,UAAA,EACA,MAAA,EACiB;AACjB,EAAA,IAAI,CAAC,aAAa,kBAAA,EAAoB;AACpC,IAAA,OAAO,UAAA;AAAA,EACT;AAEA,EAAA,MAAM,QAAA,GAAW,MAAM,YAAA,CAAa,kBAAA,CAAmB,YAAY,MAAM,CAAA;AACzE,EAAA,OAAA,CAAQ,EAAA,CAAG,MAAA,EAAQ,MAAM,QAAA,CAAS,SAAS,CAAA;AAG3C,EAAA,MAAM,SAAA,GAAY,EAAE,MAAA,EAAQ,GAAA,EAAK,SAAS,GAAA,EAAI;AAC9C,EAAA,KAAA,MAAW,CAAC,MAAA,EAAQ,IAAI,KAAK,MAAA,CAAO,OAAA,CAAQ,SAAS,CAAA,EAAG;AACtD,IAAA,OAAA,CAAQ,KAAK,MAAA,EAAQ,MAAM,OAAA,CAAQ,IAAA,CAAK,IAAI,CAAC,CAAA;AAAA,EAC/C;AACA,EAAA,OAAO,QAAA,CAAS,UAAA;AAClB;;"}
@@ -1,11 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  var cliCommon = require('@backstage/cli-common');
4
+ var pluginTechdocsNode = require('@backstage/plugin-techdocs-node');
4
5
 
5
6
  const runEngineServer = (options) => {
6
7
  const port = options.port ?? "8000";
7
8
  const useDocker = options.useDocker ?? true;
8
- const dockerImage = options.dockerImage ?? "spotify/techdocs";
9
+ const dockerImage = options.dockerImage ?? pluginTechdocsNode.TechdocsGenerator.defaultDockerImage;
9
10
  if (useDocker) {
10
11
  return cliCommon.run(
11
12
  [
@@ -1 +1 @@
1
- {"version":3,"file":"server.cjs.js","sources":["../../src/lib/server.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { run, RunChildProcess, RunOnOutput } from '@backstage/cli-common';\n\nexport const runEngineServer = (options: {\n port?: string;\n useDocker?: boolean;\n dockerImage?: string;\n dockerEntrypoint?: string;\n dockerOptions?: string[];\n onStdout?: RunOnOutput;\n onStderr?: RunOnOutput;\n engineBinary: string;\n engineServeArgs: string[];\n}): RunChildProcess => {\n const port = options.port ?? '8000';\n const useDocker = options.useDocker ?? true;\n const dockerImage = options.dockerImage ?? 'spotify/techdocs';\n\n if (useDocker) {\n return run(\n [\n 'docker',\n 'run',\n '--rm',\n '-w',\n '/content',\n '-v',\n `${process.cwd()}:/content`,\n '-p',\n `${port}:${port}`,\n '-it',\n ...(options.dockerEntrypoint\n ? ['--entrypoint', options.dockerEntrypoint]\n : []),\n ...(options.dockerOptions || []),\n dockerImage,\n ...options.engineServeArgs,\n ],\n {\n onStdout: options.onStdout,\n onStderr: options.onStderr,\n },\n );\n }\n\n return run([options.engineBinary, ...options.engineServeArgs], {\n onStdout: options.onStdout,\n onStderr: options.onStderr,\n });\n};\n"],"names":["run"],"mappings":";;;;AAkBO,MAAM,eAAA,GAAkB,CAAC,OAAA,KAUT;AACrB,EAAA,MAAM,IAAA,GAAO,QAAQ,IAAA,IAAQ,MAAA;AAC7B,EAAA,MAAM,SAAA,GAAY,QAAQ,SAAA,IAAa,IAAA;AACvC,EAAA,MAAM,WAAA,GAAc,QAAQ,WAAA,IAAe,kBAAA;AAE3C,EAAA,IAAI,SAAA,EAAW;AACb,IAAA,OAAOA,aAAA;AAAA,MACL;AAAA,QACE,QAAA;AAAA,QACA,KAAA;AAAA,QACA,MAAA;AAAA,QACA,IAAA;AAAA,QACA,UAAA;AAAA,QACA,IAAA;AAAA,QACA,CAAA,EAAG,OAAA,CAAQ,GAAA,EAAK,CAAA,SAAA,CAAA;AAAA,QAChB,IAAA;AAAA,QACA,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,IAAI,CAAA,CAAA;AAAA,QACf,KAAA;AAAA,QACA,GAAI,QAAQ,gBAAA,GACR,CAAC,gBAAgB,OAAA,CAAQ,gBAAgB,IACzC,EAAC;AAAA,QACL,GAAI,OAAA,CAAQ,aAAA,IAAiB,EAAC;AAAA,QAC9B,WAAA;AAAA,QACA,GAAG,OAAA,CAAQ;AAAA,OACb;AAAA,MACA;AAAA,QACE,UAAU,OAAA,CAAQ,QAAA;AAAA,QAClB,UAAU,OAAA,CAAQ;AAAA;AACpB,KACF;AAAA,EACF;AAEA,EAAA,OAAOA,cAAI,CAAC,OAAA,CAAQ,cAAc,GAAG,OAAA,CAAQ,eAAe,CAAA,EAAG;AAAA,IAC7D,UAAU,OAAA,CAAQ,QAAA;AAAA,IAClB,UAAU,OAAA,CAAQ;AAAA,GACnB,CAAA;AACH;;"}
1
+ {"version":3,"file":"server.cjs.js","sources":["../../src/lib/server.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { run, RunChildProcess, RunOnOutput } from '@backstage/cli-common';\nimport { TechdocsGenerator } from '@backstage/plugin-techdocs-node';\n\nexport const runEngineServer = (options: {\n port?: string;\n useDocker?: boolean;\n dockerImage?: string;\n dockerEntrypoint?: string;\n dockerOptions?: string[];\n onStdout?: RunOnOutput;\n onStderr?: RunOnOutput;\n engineBinary: string;\n engineServeArgs: string[];\n}): RunChildProcess => {\n const port = options.port ?? '8000';\n const useDocker = options.useDocker ?? true;\n const dockerImage =\n options.dockerImage ?? TechdocsGenerator.defaultDockerImage;\n\n if (useDocker) {\n return run(\n [\n 'docker',\n 'run',\n '--rm',\n '-w',\n '/content',\n '-v',\n `${process.cwd()}:/content`,\n '-p',\n `${port}:${port}`,\n '-it',\n ...(options.dockerEntrypoint\n ? ['--entrypoint', options.dockerEntrypoint]\n : []),\n ...(options.dockerOptions || []),\n dockerImage,\n ...options.engineServeArgs,\n ],\n {\n onStdout: options.onStdout,\n onStderr: options.onStderr,\n },\n );\n }\n\n return run([options.engineBinary, ...options.engineServeArgs], {\n onStdout: options.onStdout,\n onStderr: options.onStderr,\n });\n};\n"],"names":["TechdocsGenerator","run"],"mappings":";;;;;AAmBO,MAAM,eAAA,GAAkB,CAAC,OAAA,KAUT;AACrB,EAAA,MAAM,IAAA,GAAO,QAAQ,IAAA,IAAQ,MAAA;AAC7B,EAAA,MAAM,SAAA,GAAY,QAAQ,SAAA,IAAa,IAAA;AACvC,EAAA,MAAM,WAAA,GACJ,OAAA,CAAQ,WAAA,IAAeA,oCAAA,CAAkB,kBAAA;AAE3C,EAAA,IAAI,SAAA,EAAW;AACb,IAAA,OAAOC,aAAA;AAAA,MACL;AAAA,QACE,QAAA;AAAA,QACA,KAAA;AAAA,QACA,MAAA;AAAA,QACA,IAAA;AAAA,QACA,UAAA;AAAA,QACA,IAAA;AAAA,QACA,CAAA,EAAG,OAAA,CAAQ,GAAA,EAAK,CAAA,SAAA,CAAA;AAAA,QAChB,IAAA;AAAA,QACA,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,IAAI,CAAA,CAAA;AAAA,QACf,KAAA;AAAA,QACA,GAAI,QAAQ,gBAAA,GACR,CAAC,gBAAgB,OAAA,CAAQ,gBAAgB,IACzC,EAAC;AAAA,QACL,GAAI,OAAA,CAAQ,aAAA,IAAiB,EAAC;AAAA,QAC9B,WAAA;AAAA,QACA,GAAG,OAAA,CAAQ;AAAA,OACb;AAAA,MACA;AAAA,QACE,UAAU,OAAA,CAAQ,QAAA;AAAA,QAClB,UAAU,OAAA,CAAQ;AAAA;AACpB,KACF;AAAA,EACF;AAEA,EAAA,OAAOA,cAAI,CAAC,OAAA,CAAQ,cAAc,GAAG,OAAA,CAAQ,eAAe,CAAA,EAAG;AAAA,IAC7D,UAAU,OAAA,CAAQ,QAAA;AAAA,IAClB,UAAU,OAAA,CAAQ;AAAA,GACnB,CAAA;AACH;;"}
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var version = "1.13.0";
3
+ var version = "1.14.0-next.0";
4
4
 
5
5
  exports.version = version;
6
6
  //# sourceMappingURL=package.json.cjs.js.map
@@ -0,0 +1,178 @@
1
+ 'use strict';
2
+
3
+ var fs = require('fs-extra');
4
+ var path = require('node:path');
5
+ var node_crypto = require('node:crypto');
6
+ var pluginTechdocsNode = require('@backstage/plugin-techdocs-node');
7
+ var alpha = require('@backstage/plugin-techdocs-node/alpha');
8
+ var errors = require('@backstage/errors');
9
+ var zensicalHelpers = require('./zensicalHelpers.cjs.js');
10
+
11
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
12
+
13
+ var fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
14
+ var path__default = /*#__PURE__*/_interopDefaultCompat(path);
15
+
16
+ class TechdocsZensicalGenerator {
17
+ static defaultDockerImage = "ghcr.io/backstage/techdocs-zensical:v0.1.0";
18
+ logger;
19
+ containerRunner;
20
+ options;
21
+ static fromConfig(config, options) {
22
+ const { containerRunner, logger } = options;
23
+ return new TechdocsZensicalGenerator({
24
+ logger,
25
+ containerRunner,
26
+ config
27
+ });
28
+ }
29
+ constructor(options) {
30
+ this.logger = options.logger;
31
+ this.options = readZensicalGeneratorConfig(options.config);
32
+ this.containerRunner = options.containerRunner;
33
+ }
34
+ async run(options) {
35
+ const {
36
+ inputDir,
37
+ outputDir,
38
+ etag,
39
+ logger: childLogger,
40
+ logStream,
41
+ siteOptions,
42
+ runAsDefaultUser
43
+ } = options;
44
+ const { path: mkdocsYmlPath, content } = await pluginTechdocsNode.getMkdocsYml(
45
+ inputDir,
46
+ siteOptions
47
+ );
48
+ const docsDir = await alpha.validateMkdocsYaml(inputDir, content);
49
+ if (this.options.legacyCopyReadmeMdToIndexMd) {
50
+ await alpha.patchIndexPreBuild({ inputDir, logger: childLogger, docsDir });
51
+ }
52
+ await alpha.validateInputDirectory(inputDir);
53
+ const mountDirs = {
54
+ [inputDir]: "/input"
55
+ };
56
+ const relativeSiteDir = `.techdocs-zensical-site-${node_crypto.randomUUID()}`;
57
+ const builtSiteDir = path__default.default.join(
58
+ path__default.default.dirname(mkdocsYmlPath),
59
+ relativeSiteDir
60
+ );
61
+ await alpha.sanitizeMkdocsYml(
62
+ mkdocsYmlPath,
63
+ childLogger,
64
+ this.options.dangerouslyAllowAdditionalKeys,
65
+ this.options.dangerouslyAllowAdditionalPlugins
66
+ );
67
+ const zensicalConfigPath = await zensicalHelpers.patchZensicalConfig(
68
+ mkdocsYmlPath,
69
+ relativeSiteDir,
70
+ childLogger
71
+ );
72
+ try {
73
+ switch (this.options.runIn) {
74
+ case "local":
75
+ await alpha.runCommand({
76
+ command: "zensical",
77
+ args: ["build", "-f", zensicalConfigPath],
78
+ options: {
79
+ cwd: inputDir,
80
+ // Don't expose the backend's environment (and its secrets) to
81
+ // the docs build; the Docker path is scoped the same way.
82
+ env: {
83
+ NODE_ENV: process.env.NODE_ENV,
84
+ PATH: process.env.PATH,
85
+ HOME: process.env.HOME
86
+ }
87
+ },
88
+ logStream
89
+ });
90
+ childLogger.info(
91
+ `Successfully generated docs from ${inputDir} into ${outputDir} using local zensical`
92
+ );
93
+ break;
94
+ case "docker": {
95
+ const containerRunner = this.containerRunner || alpha.createDockerContainerRunner();
96
+ await containerRunner.runContainer({
97
+ imageName: this.options.dockerImage ?? TechdocsZensicalGenerator.defaultDockerImage,
98
+ // Set the entrypoint explicitly rather than relying on the image's.
99
+ command: ["zensical"],
100
+ args: [
101
+ "build",
102
+ "-f",
103
+ path__default.default.posix.join(
104
+ "/input",
105
+ ...path__default.default.relative(inputDir, zensicalConfigPath).split(path__default.default.sep)
106
+ )
107
+ ],
108
+ logStream,
109
+ mountDirs,
110
+ workingDir: "/input",
111
+ envVars: { HOME: "/tmp" },
112
+ pullImage: this.options.pullImage,
113
+ pullOptions: this.options.pullOptions,
114
+ defaultUser: runAsDefaultUser
115
+ });
116
+ childLogger.info(
117
+ `Successfully generated docs from ${inputDir} into ${outputDir} using zensical container`
118
+ );
119
+ break;
120
+ }
121
+ default:
122
+ throw new Error(
123
+ `Invalid config value "${this.options.runIn}" provided in 'techdocs.generator'.`
124
+ );
125
+ }
126
+ } catch (error) {
127
+ await fs__default.default.remove(builtSiteDir);
128
+ this.logger.debug(
129
+ `Failed to generate docs from ${inputDir} into ${outputDir}`
130
+ );
131
+ throw new errors.ForwardedError(
132
+ `Failed to generate docs from ${inputDir} into ${outputDir}`,
133
+ error
134
+ );
135
+ } finally {
136
+ await zensicalHelpers.cleanupZensicalConfig(zensicalConfigPath);
137
+ }
138
+ await fs__default.default.move(builtSiteDir, outputDir, { overwrite: true });
139
+ await zensicalHelpers.normalizeSearchIndex(outputDir, childLogger);
140
+ await alpha.createOrUpdateMetadata(
141
+ path__default.default.join(outputDir, "techdocs_metadata.json"),
142
+ childLogger,
143
+ "zensical"
144
+ );
145
+ if (etag) {
146
+ await alpha.storeEtagMetadata(
147
+ path__default.default.join(outputDir, "techdocs_metadata.json"),
148
+ etag
149
+ );
150
+ }
151
+ }
152
+ }
153
+ function readZensicalGeneratorConfig(config) {
154
+ return {
155
+ runIn: config.getOptionalString(
156
+ "techdocs.generator.runIn"
157
+ ) ?? "docker",
158
+ // Deliberately not falling back to techdocs.generator.dockerImage: that
159
+ // is an MkDocs image and would build the site with MkDocs instead.
160
+ dockerImage: config.getOptionalString(
161
+ "techdocs.generator.zensical.dockerImage"
162
+ ),
163
+ pullImage: config.getOptionalBoolean("techdocs.generator.pullImage"),
164
+ pullOptions: config.getOptional("techdocs.generator.pullOptions"),
165
+ dangerouslyAllowAdditionalKeys: config.getOptionalStringArray(
166
+ "techdocs.generator.mkdocs.dangerouslyAllowAdditionalKeys"
167
+ ),
168
+ dangerouslyAllowAdditionalPlugins: config.getOptionalStringArray(
169
+ "techdocs.generator.mkdocs.dangerouslyAllowAdditionalPlugins"
170
+ ),
171
+ legacyCopyReadmeMdToIndexMd: config.getOptionalBoolean(
172
+ "techdocs.generator.mkdocs.legacyCopyReadmeMdToIndexMd"
173
+ )
174
+ };
175
+ }
176
+
177
+ exports.TechdocsZensicalGenerator = TechdocsZensicalGenerator;
178
+ //# sourceMappingURL=TechdocsZensicalGenerator.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TechdocsZensicalGenerator.cjs.js","sources":["../../../../../../plugins/techdocs-zensical-internal/src/TechdocsZensicalGenerator.ts"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Config } from '@backstage/config';\nimport fs from 'fs-extra';\nimport path from 'node:path';\nimport { randomUUID } from 'node:crypto';\nimport { getMkdocsYml } from '@backstage/plugin-techdocs-node';\nimport type {\n GeneratorBase,\n GeneratorOptions,\n GeneratorRunOptions,\n TechDocsContainerRunner,\n} from '@backstage/plugin-techdocs-node';\nimport {\n createDockerContainerRunner,\n createOrUpdateMetadata,\n patchIndexPreBuild,\n runCommand,\n sanitizeMkdocsYml,\n storeEtagMetadata,\n validateInputDirectory,\n validateMkdocsYaml,\n} from '@backstage/plugin-techdocs-node/alpha';\nimport type { ContainerRunnerPullOptions } from '@backstage/plugin-techdocs-node';\n\ntype ZensicalGeneratorConfig = {\n runIn: 'docker' | 'local';\n dockerImage?: string;\n pullImage?: boolean;\n pullOptions?: ContainerRunnerPullOptions;\n legacyCopyReadmeMdToIndexMd?: boolean;\n dangerouslyAllowAdditionalKeys?: string[];\n dangerouslyAllowAdditionalPlugins?: string[];\n};\nimport { ForwardedError } from '@backstage/errors';\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport {\n patchZensicalConfig,\n cleanupZensicalConfig,\n normalizeSearchIndex,\n} from './zensicalHelpers';\n\n/**\n * Generates documentation files using Zensical.\n * @public\n */\nexport class TechdocsZensicalGenerator implements GeneratorBase {\n public static readonly defaultDockerImage =\n 'ghcr.io/backstage/techdocs-zensical:v0.1.0';\n private readonly logger: LoggerService;\n private readonly containerRunner?: TechDocsContainerRunner;\n private readonly options: ZensicalGeneratorConfig;\n\n static fromConfig(config: Config, options: GeneratorOptions) {\n const { containerRunner, logger } = options;\n return new TechdocsZensicalGenerator({\n logger,\n containerRunner,\n config,\n });\n }\n\n constructor(options: {\n logger: LoggerService;\n containerRunner?: TechDocsContainerRunner;\n config: Config;\n }) {\n this.logger = options.logger;\n this.options = readZensicalGeneratorConfig(options.config);\n this.containerRunner = options.containerRunner;\n }\n\n public async run(options: GeneratorRunOptions): Promise<void> {\n const {\n inputDir,\n outputDir,\n etag,\n logger: childLogger,\n logStream,\n siteOptions,\n runAsDefaultUser,\n } = options;\n\n const { path: mkdocsYmlPath, content } = await getMkdocsYml(\n inputDir,\n siteOptions,\n );\n\n const docsDir = await validateMkdocsYaml(inputDir, content);\n\n if (this.options.legacyCopyReadmeMdToIndexMd) {\n await patchIndexPreBuild({ inputDir, logger: childLogger, docsDir });\n }\n\n await validateInputDirectory(inputDir);\n\n const mountDirs = {\n [inputDir]: '/input',\n };\n\n // Zensical requires site_dir to be within the project root.\n // Unique name so we never clobber a site/ directory committed to the repo.\n // Zensical resolves site_dir against the config file's directory, which\n // can be a subdirectory of inputDir (mkdocsConfigFileName).\n const relativeSiteDir = `.techdocs-zensical-site-${randomUUID()}`;\n const builtSiteDir = path.join(\n path.dirname(mkdocsYmlPath),\n relativeSiteDir,\n );\n // Same allowlist as the MkDocs path: Zensical consumes the same\n // mkdocs.yml format and honors options like plantuml_cmd.\n await sanitizeMkdocsYml(\n mkdocsYmlPath,\n childLogger,\n this.options.dangerouslyAllowAdditionalKeys,\n this.options.dangerouslyAllowAdditionalPlugins,\n );\n\n const zensicalConfigPath = await patchZensicalConfig(\n mkdocsYmlPath,\n relativeSiteDir,\n childLogger,\n );\n\n try {\n switch (this.options.runIn) {\n case 'local':\n await runCommand({\n command: 'zensical',\n args: ['build', '-f', zensicalConfigPath],\n options: {\n cwd: inputDir,\n // Don't expose the backend's environment (and its secrets) to\n // the docs build; the Docker path is scoped the same way.\n env: {\n NODE_ENV: process.env.NODE_ENV,\n PATH: process.env.PATH,\n HOME: process.env.HOME,\n },\n },\n logStream,\n });\n childLogger.info(\n `Successfully generated docs from ${inputDir} into ${outputDir} using local zensical`,\n );\n break;\n case 'docker': {\n const containerRunner =\n this.containerRunner || createDockerContainerRunner();\n await containerRunner.runContainer({\n imageName:\n this.options.dockerImage ??\n TechdocsZensicalGenerator.defaultDockerImage,\n // Set the entrypoint explicitly rather than relying on the image's.\n command: ['zensical'],\n args: [\n 'build',\n '-f',\n path.posix.join(\n '/input',\n ...path.relative(inputDir, zensicalConfigPath).split(path.sep),\n ),\n ],\n logStream,\n mountDirs,\n workingDir: '/input',\n envVars: { HOME: '/tmp' },\n pullImage: this.options.pullImage,\n pullOptions: this.options.pullOptions,\n defaultUser: runAsDefaultUser,\n });\n childLogger.info(\n `Successfully generated docs from ${inputDir} into ${outputDir} using zensical container`,\n );\n break;\n }\n default:\n throw new Error(\n `Invalid config value \"${this.options.runIn}\" provided in 'techdocs.generator'.`,\n );\n }\n } catch (error) {\n await fs.remove(builtSiteDir);\n this.logger.debug(\n `Failed to generate docs from ${inputDir} into ${outputDir}`,\n );\n throw new ForwardedError(\n `Failed to generate docs from ${inputDir} into ${outputDir}`,\n error,\n );\n } finally {\n await cleanupZensicalConfig(zensicalConfigPath);\n }\n\n await fs.move(builtSiteDir, outputDir, { overwrite: true });\n\n await normalizeSearchIndex(outputDir, childLogger);\n\n await createOrUpdateMetadata(\n path.join(outputDir, 'techdocs_metadata.json'),\n childLogger,\n 'zensical',\n );\n\n if (etag) {\n await storeEtagMetadata(\n path.join(outputDir, 'techdocs_metadata.json'),\n etag,\n );\n }\n }\n}\n\nfunction readZensicalGeneratorConfig(config: Config): ZensicalGeneratorConfig {\n return {\n runIn:\n (config.getOptionalString(\n 'techdocs.generator.runIn',\n ) as ZensicalGeneratorConfig['runIn']) ?? 'docker',\n // Deliberately not falling back to techdocs.generator.dockerImage: that\n // is an MkDocs image and would build the site with MkDocs instead.\n dockerImage: config.getOptionalString(\n 'techdocs.generator.zensical.dockerImage',\n ),\n pullImage: config.getOptionalBoolean('techdocs.generator.pullImage'),\n pullOptions: config.getOptional('techdocs.generator.pullOptions'),\n dangerouslyAllowAdditionalKeys: config.getOptionalStringArray(\n 'techdocs.generator.mkdocs.dangerouslyAllowAdditionalKeys',\n ),\n dangerouslyAllowAdditionalPlugins: config.getOptionalStringArray(\n 'techdocs.generator.mkdocs.dangerouslyAllowAdditionalPlugins',\n ),\n legacyCopyReadmeMdToIndexMd: config.getOptionalBoolean(\n 'techdocs.generator.mkdocs.legacyCopyReadmeMdToIndexMd',\n ),\n };\n}\n"],"names":["getMkdocsYml","validateMkdocsYaml","patchIndexPreBuild","validateInputDirectory","randomUUID","path","sanitizeMkdocsYml","patchZensicalConfig","runCommand","createDockerContainerRunner","fs","ForwardedError","cleanupZensicalConfig","normalizeSearchIndex","createOrUpdateMetadata","storeEtagMetadata"],"mappings":"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"}