@backstage/plugin-techdocs-backend 2.3.1-next.1 → 2.4.0-next.2
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.
- package/CHANGELOG.md +14 -0
- package/dist/actions/createGetTechdocsMetadataAction.cjs.js +145 -0
- package/dist/actions/createGetTechdocsMetadataAction.cjs.js.map +1 -0
- package/dist/actions/index.cjs.js +11 -0
- package/dist/actions/index.cjs.js.map +1 -0
- package/dist/cache/cacheMiddleware.cjs.js +193 -25
- package/dist/cache/cacheMiddleware.cjs.js.map +1 -1
- package/dist/plugin.cjs.js +11 -2
- package/dist/plugin.cjs.js.map +1 -1
- package/package.json +7 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @backstage/plugin-techdocs-backend
|
|
2
2
|
|
|
3
|
+
## 2.4.0-next.2
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 45b0b89: Added a `get-techdocs-metadata` action to the TechDocs backend plugin, registered with the Actions Registry Service (alpha).
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 685aee1: Fixed TechDocs asset caching on reused HTTP connections to avoid listener leaks and cross-request response corruption. Existing cached assets are refreshed automatically.
|
|
12
|
+
- Updated dependencies
|
|
13
|
+
- @backstage/plugin-techdocs-node@2.0.3-next.2
|
|
14
|
+
- @backstage/integration@2.1.3-next.1
|
|
15
|
+
- @backstage/plugin-techdocs-common@0.1.2-next.0
|
|
16
|
+
|
|
3
17
|
## 2.3.1-next.1
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var errors = require('@backstage/errors');
|
|
4
|
+
var catalogModel = require('@backstage/catalog-model');
|
|
5
|
+
var alpha = require('@backstage/plugin-techdocs-common/alpha');
|
|
6
|
+
|
|
7
|
+
const MAX_METADATA_SIZE = 5 * 1024 * 1024;
|
|
8
|
+
const FETCH_TIMEOUT = 15e3;
|
|
9
|
+
const TEXT_PAGE_EXTENSIONS = [
|
|
10
|
+
".html",
|
|
11
|
+
".htm",
|
|
12
|
+
".md",
|
|
13
|
+
".txt",
|
|
14
|
+
".json",
|
|
15
|
+
".xml",
|
|
16
|
+
".yaml",
|
|
17
|
+
".yml"
|
|
18
|
+
];
|
|
19
|
+
function filterTextPages(pages) {
|
|
20
|
+
if (!pages) {
|
|
21
|
+
return void 0;
|
|
22
|
+
}
|
|
23
|
+
return Object.fromEntries(
|
|
24
|
+
Object.entries(pages).filter(
|
|
25
|
+
([path]) => TEXT_PAGE_EXTENSIONS.some((ext) => path.endsWith(ext))
|
|
26
|
+
)
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
const createGetTechdocsMetadataAction = ({
|
|
30
|
+
actionsRegistry,
|
|
31
|
+
auth,
|
|
32
|
+
discovery
|
|
33
|
+
}) => {
|
|
34
|
+
actionsRegistry.register({
|
|
35
|
+
name: "get-techdocs-metadata",
|
|
36
|
+
title: "Get TechDocs Metadata",
|
|
37
|
+
visibilityPermission: alpha.techdocsActionsReadPermission,
|
|
38
|
+
attributes: {
|
|
39
|
+
destructive: false,
|
|
40
|
+
readOnly: true,
|
|
41
|
+
idempotent: true
|
|
42
|
+
},
|
|
43
|
+
description: `
|
|
44
|
+
This allows you to get the metadata for a specific TechDocs site.
|
|
45
|
+
Each entity in the software catalog has a unique name, kind, and namespace. The default namespace is "default".
|
|
46
|
+
The metadata includes information about the site structure, navigation, and other relevant details.
|
|
47
|
+
`,
|
|
48
|
+
schema: {
|
|
49
|
+
input: (zodSchema) => zodSchema.object({
|
|
50
|
+
kind: zodSchema.string().describe("The kind of the entity to query").default("Component"),
|
|
51
|
+
namespace: zodSchema.string().describe("The namespace of the entity to query").default("default"),
|
|
52
|
+
name: zodSchema.string().describe("The name of the entity to query")
|
|
53
|
+
}),
|
|
54
|
+
output: (zodSchema) => zodSchema.object({
|
|
55
|
+
site_name: zodSchema.string().optional().describe("The name of the documentation site"),
|
|
56
|
+
site_description: zodSchema.string().optional().describe("The description of the documentation site"),
|
|
57
|
+
nav: zodSchema.array(zodSchema.any()).optional().describe("Navigation structure of the documentation"),
|
|
58
|
+
pages: zodSchema.record(zodSchema.any()).optional().describe("Pages in the documentation"),
|
|
59
|
+
metadata: zodSchema.record(zodSchema.any()).optional().describe("Additional metadata")
|
|
60
|
+
}).passthrough()
|
|
61
|
+
},
|
|
62
|
+
action: async ({ input, logger, credentials }) => {
|
|
63
|
+
const entityName = {
|
|
64
|
+
kind: input.kind,
|
|
65
|
+
namespace: input.namespace,
|
|
66
|
+
name: input.name
|
|
67
|
+
};
|
|
68
|
+
const entityRef = catalogModel.stringifyEntityRef(entityName);
|
|
69
|
+
try {
|
|
70
|
+
const baseUrl = await discovery.getBaseUrl("techdocs");
|
|
71
|
+
const { token } = await auth.getPluginRequestToken({
|
|
72
|
+
onBehalfOf: credentials,
|
|
73
|
+
targetPluginId: "techdocs"
|
|
74
|
+
});
|
|
75
|
+
const controller = new AbortController();
|
|
76
|
+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetch(
|
|
79
|
+
`${baseUrl}/metadata/techdocs/${encodeURIComponent(
|
|
80
|
+
input.namespace
|
|
81
|
+
)}/${encodeURIComponent(input.kind)}/${encodeURIComponent(
|
|
82
|
+
input.name
|
|
83
|
+
)}`,
|
|
84
|
+
{
|
|
85
|
+
headers: {
|
|
86
|
+
Authorization: `Bearer ${token}`
|
|
87
|
+
},
|
|
88
|
+
signal: controller.signal
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
clearTimeout(timeoutId);
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
if (response.status === 404) {
|
|
94
|
+
throw new errors.NotFoundError(
|
|
95
|
+
`No TechDocs metadata found for entity ${entityRef}`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
throw await errors.ResponseError.fromResponse(response);
|
|
99
|
+
}
|
|
100
|
+
const contentLength = response.headers.get("content-length");
|
|
101
|
+
if (contentLength && parseInt(contentLength, 10) > MAX_METADATA_SIZE) {
|
|
102
|
+
throw new errors.InputError(
|
|
103
|
+
`Metadata too large: ${contentLength} bytes exceeds limit of ${MAX_METADATA_SIZE} bytes`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const text = await response.text();
|
|
107
|
+
const textByteLength = Buffer.byteLength(text, "utf8");
|
|
108
|
+
if (textByteLength > MAX_METADATA_SIZE) {
|
|
109
|
+
throw new errors.InputError(
|
|
110
|
+
`Metadata too large: ${textByteLength} bytes exceeds limit of ${MAX_METADATA_SIZE} bytes`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const metadata = JSON.parse(text);
|
|
114
|
+
if (!metadata || Object.keys(metadata).length === 0) {
|
|
115
|
+
throw new errors.NotFoundError(
|
|
116
|
+
`No TechDocs metadata found for entity ${entityRef}`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const metadataWithExtras = metadata;
|
|
120
|
+
const filteredPages = filterTextPages(metadataWithExtras.pages);
|
|
121
|
+
const filteredMetadata = {
|
|
122
|
+
...metadataWithExtras,
|
|
123
|
+
...filteredPages && { pages: filteredPages }
|
|
124
|
+
};
|
|
125
|
+
return {
|
|
126
|
+
output: filteredMetadata
|
|
127
|
+
};
|
|
128
|
+
} catch (fetchError) {
|
|
129
|
+
clearTimeout(timeoutId);
|
|
130
|
+
throw fetchError;
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
134
|
+
logger.error(`Failed to get TechDocs metadata: ${errorMessage}`);
|
|
135
|
+
if (error instanceof errors.NotFoundError || error instanceof errors.ResponseError) {
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
throw new Error(`Failed to get TechDocs metadata: ${errorMessage}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
exports.createGetTechdocsMetadataAction = createGetTechdocsMetadataAction;
|
|
145
|
+
//# sourceMappingURL=createGetTechdocsMetadataAction.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createGetTechdocsMetadataAction.cjs.js","sources":["../../src/actions/createGetTechdocsMetadataAction.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 */\nimport { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';\nimport {\n AuthService,\n BackstageCredentials,\n DiscoveryService,\n} from '@backstage/backend-plugin-api';\nimport { InputError, NotFoundError, ResponseError } from '@backstage/errors';\nimport { stringifyEntityRef } from '@backstage/catalog-model';\nimport { TechDocsMetadata } from '@backstage/plugin-techdocs-node';\nimport { techdocsActionsReadPermission } from '@backstage/plugin-techdocs-common/alpha';\n\n// Memory optimization constants\nconst MAX_METADATA_SIZE = 5 * 1024 * 1024;\nconst FETCH_TIMEOUT = 15000;\n\nconst TEXT_PAGE_EXTENSIONS = [\n '.html',\n '.htm',\n '.md',\n '.txt',\n '.json',\n '.xml',\n '.yaml',\n '.yml',\n];\n\nfunction filterTextPages(\n pages: Record<string, unknown> | undefined,\n): Record<string, unknown> | undefined {\n if (!pages) {\n return undefined;\n }\n return Object.fromEntries(\n Object.entries(pages).filter(([path]) =>\n TEXT_PAGE_EXTENSIONS.some(ext => path.endsWith(ext)),\n ),\n );\n}\n\nexport const createGetTechdocsMetadataAction = ({\n actionsRegistry,\n auth,\n discovery,\n}: {\n actionsRegistry: ActionsRegistryService;\n auth: AuthService;\n discovery: DiscoveryService;\n}) => {\n actionsRegistry.register({\n name: 'get-techdocs-metadata',\n title: 'Get TechDocs Metadata',\n visibilityPermission: techdocsActionsReadPermission,\n attributes: {\n destructive: false,\n readOnly: true,\n idempotent: true,\n },\n description: `\nThis allows you to get the metadata for a specific TechDocs site.\nEach entity in the software catalog has a unique name, kind, and namespace. The default namespace is \"default\".\nThe metadata includes information about the site structure, navigation, and other relevant details.\n `,\n schema: {\n input: zodSchema =>\n zodSchema.object({\n kind: zodSchema\n .string()\n .describe('The kind of the entity to query')\n .default('Component'),\n namespace: zodSchema\n .string()\n .describe('The namespace of the entity to query')\n .default('default'),\n name: zodSchema.string().describe('The name of the entity to query'),\n }),\n output: zodSchema =>\n zodSchema\n .object({\n site_name: zodSchema\n .string()\n .optional()\n .describe('The name of the documentation site'),\n site_description: zodSchema\n .string()\n .optional()\n .describe('The description of the documentation site'),\n nav: zodSchema\n .array(zodSchema.any())\n .optional()\n .describe('Navigation structure of the documentation'),\n pages: zodSchema\n .record(zodSchema.any())\n .optional()\n .describe('Pages in the documentation'),\n metadata: zodSchema\n .record(zodSchema.any())\n .optional()\n .describe('Additional metadata'),\n })\n .passthrough(),\n },\n action: async ({ input, logger, credentials }) => {\n const entityName = {\n kind: input.kind,\n namespace: input.namespace,\n name: input.name,\n };\n const entityRef = stringifyEntityRef(entityName);\n\n try {\n const baseUrl = await discovery.getBaseUrl('techdocs');\n const { token } = await auth.getPluginRequestToken({\n onBehalfOf: credentials as BackstageCredentials,\n targetPluginId: 'techdocs',\n });\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);\n\n try {\n const response = await fetch(\n `${baseUrl}/metadata/techdocs/${encodeURIComponent(\n input.namespace,\n )}/${encodeURIComponent(input.kind)}/${encodeURIComponent(\n input.name,\n )}`,\n {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n signal: controller.signal,\n },\n );\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n if (response.status === 404) {\n throw new NotFoundError(\n `No TechDocs metadata found for entity ${entityRef}`,\n );\n }\n throw await ResponseError.fromResponse(response);\n }\n\n const contentLength = response.headers.get('content-length');\n if (\n contentLength &&\n parseInt(contentLength, 10) > MAX_METADATA_SIZE\n ) {\n throw new InputError(\n `Metadata too large: ${contentLength} bytes exceeds limit of ${MAX_METADATA_SIZE} bytes`,\n );\n }\n\n const text = await response.text();\n const textByteLength = Buffer.byteLength(text, 'utf8');\n if (textByteLength > MAX_METADATA_SIZE) {\n throw new InputError(\n `Metadata too large: ${textByteLength} bytes exceeds limit of ${MAX_METADATA_SIZE} bytes`,\n );\n }\n\n const metadata = JSON.parse(text) as TechDocsMetadata;\n\n // Add metadata validation\n if (!metadata || Object.keys(metadata).length === 0) {\n throw new NotFoundError(\n `No TechDocs metadata found for entity ${entityRef}`,\n );\n }\n\n const metadataWithExtras = metadata as TechDocsMetadata & {\n pages?: Record<string, unknown>;\n };\n\n const filteredPages = filterTextPages(metadataWithExtras.pages);\n\n const filteredMetadata: Record<string, unknown> = {\n ...metadataWithExtras,\n ...(filteredPages && { pages: filteredPages }),\n };\n\n return {\n output: filteredMetadata,\n };\n } catch (fetchError) {\n clearTimeout(timeoutId);\n throw fetchError;\n }\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n\n logger.error(`Failed to get TechDocs metadata: ${errorMessage}`);\n\n if (error instanceof NotFoundError || error instanceof ResponseError) {\n throw error;\n }\n\n throw new Error(`Failed to get TechDocs metadata: ${errorMessage}`);\n }\n },\n });\n};\n"],"names":["techdocsActionsReadPermission","stringifyEntityRef","NotFoundError","ResponseError","InputError"],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var createGetTechdocsMetadataAction = require('./createGetTechdocsMetadataAction.cjs.js');
|
|
4
|
+
|
|
5
|
+
const createTechdocsActions = (options) => {
|
|
6
|
+
createGetTechdocsMetadataAction.createGetTechdocsMetadataAction(options);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
exports.createGetTechdocsMetadataAction = createGetTechdocsMetadataAction.createGetTechdocsMetadataAction;
|
|
10
|
+
exports.createTechdocsActions = createTechdocsActions;
|
|
11
|
+
//# sourceMappingURL=index.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs.js","sources":["../../src/actions/index.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 */\nimport { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';\nimport { AuthService, DiscoveryService } from '@backstage/backend-plugin-api';\nimport { createGetTechdocsMetadataAction } from './createGetTechdocsMetadataAction';\n\nexport const createTechdocsActions = (options: {\n actionsRegistry: ActionsRegistryService;\n auth: AuthService;\n discovery: DiscoveryService;\n}) => {\n createGetTechdocsMetadataAction(options);\n};\n\nexport { createGetTechdocsMetadataAction } from './createGetTechdocsMetadataAction';\n"],"names":["createGetTechdocsMetadataAction"],"mappings":";;;;AAmBO,MAAM,qBAAA,GAAwB,CAAC,OAAA,KAIhC;AACJ,EAAAA,+DAAA,CAAgC,OAAO,CAAA;AACzC;;;"}
|
|
@@ -6,45 +6,213 @@ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'defau
|
|
|
6
6
|
|
|
7
7
|
var router__default = /*#__PURE__*/_interopDefaultCompat(router);
|
|
8
8
|
|
|
9
|
+
const CACHE_VERSION_HEADER = "x-backstage-techdocs-cache-version";
|
|
10
|
+
const CACHE_VERSION = "1";
|
|
11
|
+
const HEADER_SEPARATOR = Buffer.from("\r\n\r\n");
|
|
12
|
+
const omittedCachedHeaders = /* @__PURE__ */ new Set([
|
|
13
|
+
"connection",
|
|
14
|
+
"content-encoding",
|
|
15
|
+
"content-length",
|
|
16
|
+
"date",
|
|
17
|
+
"keep-alive",
|
|
18
|
+
"proxy-authenticate",
|
|
19
|
+
"proxy-authorization",
|
|
20
|
+
"te",
|
|
21
|
+
"trailer",
|
|
22
|
+
"transfer-encoding",
|
|
23
|
+
"upgrade"
|
|
24
|
+
]);
|
|
25
|
+
function serializeCachedResponse(statusCode, headers, body) {
|
|
26
|
+
const headerLines = Object.entries({
|
|
27
|
+
...headers,
|
|
28
|
+
[CACHE_VERSION_HEADER]: CACHE_VERSION,
|
|
29
|
+
"content-length": body.length,
|
|
30
|
+
connection: "close"
|
|
31
|
+
}).flatMap(
|
|
32
|
+
([name, value]) => (Array.isArray(value) ? value : [value]).map((item) => `${name}: ${item}`)
|
|
33
|
+
);
|
|
34
|
+
return Buffer.concat([
|
|
35
|
+
Buffer.from(
|
|
36
|
+
`HTTP/1.1 ${statusCode} OK\r
|
|
37
|
+
${headerLines.join("\r\n")}`,
|
|
38
|
+
"latin1"
|
|
39
|
+
),
|
|
40
|
+
HEADER_SEPARATOR,
|
|
41
|
+
body
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
44
|
+
function deserializeCachedResponse(data) {
|
|
45
|
+
if (!data) {
|
|
46
|
+
return void 0;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const headersEnd = data.indexOf(HEADER_SEPARATOR);
|
|
50
|
+
if (headersEnd === -1) {
|
|
51
|
+
return void 0;
|
|
52
|
+
}
|
|
53
|
+
const [statusLine, ...headerLines] = data.toString("latin1", 0, headersEnd).split("\r\n");
|
|
54
|
+
if (statusLine !== "HTTP/1.1 200 OK") {
|
|
55
|
+
return void 0;
|
|
56
|
+
}
|
|
57
|
+
const headers = {};
|
|
58
|
+
for (const line of headerLines) {
|
|
59
|
+
const separator = line.indexOf(":");
|
|
60
|
+
if (separator <= 0) {
|
|
61
|
+
return void 0;
|
|
62
|
+
}
|
|
63
|
+
const name = line.slice(0, separator).trim().toLowerCase();
|
|
64
|
+
const value = line.slice(separator + 1).trim();
|
|
65
|
+
const existing = headers[name];
|
|
66
|
+
if (existing === void 0) {
|
|
67
|
+
headers[name] = value;
|
|
68
|
+
} else if (Array.isArray(existing)) {
|
|
69
|
+
headers[name] = [...existing, value];
|
|
70
|
+
} else {
|
|
71
|
+
headers[name] = [String(existing), value];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (headers[CACHE_VERSION_HEADER] !== CACHE_VERSION) {
|
|
75
|
+
return void 0;
|
|
76
|
+
}
|
|
77
|
+
delete headers[CACHE_VERSION_HEADER];
|
|
78
|
+
const body = data.subarray(headersEnd + HEADER_SEPARATOR.length);
|
|
79
|
+
if (headers["content-length"] !== String(body.length)) {
|
|
80
|
+
return void 0;
|
|
81
|
+
}
|
|
82
|
+
delete headers["content-length"];
|
|
83
|
+
delete headers.connection;
|
|
84
|
+
return {
|
|
85
|
+
statusCode: 200,
|
|
86
|
+
headers,
|
|
87
|
+
body
|
|
88
|
+
};
|
|
89
|
+
} catch {
|
|
90
|
+
return void 0;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function getCacheableHeaders(headers) {
|
|
94
|
+
return Object.fromEntries(
|
|
95
|
+
Object.entries(headers).filter(
|
|
96
|
+
(entry) => entry[1] !== void 0 && !omittedCachedHeaders.has(entry[0].toLowerCase())
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
function captureWriteHeadHeaders(target, headers) {
|
|
101
|
+
if (!headers) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!Array.isArray(headers)) {
|
|
105
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
106
|
+
target[name.toLowerCase()] = value;
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
for (let i = 0; i < headers.length; i += 2) {
|
|
111
|
+
const name = headers[i];
|
|
112
|
+
const value = headers[i + 1];
|
|
113
|
+
if (typeof name !== "string" || value === void 0) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const normalizedName = name.toLowerCase();
|
|
117
|
+
const existing = target[normalizedName];
|
|
118
|
+
if (existing === void 0) {
|
|
119
|
+
target[normalizedName] = value;
|
|
120
|
+
} else {
|
|
121
|
+
target[normalizedName] = [
|
|
122
|
+
...Array.isArray(existing) ? existing : [String(existing)],
|
|
123
|
+
...Array.isArray(value) ? value : [String(value)]
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function captureChunk(chunks, chunk, encoding) {
|
|
129
|
+
if (chunk === void 0) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
chunks.push(
|
|
133
|
+
typeof chunk === "string" ? Buffer.from(chunk, typeof encoding === "string" ? encoding : void 0) : Buffer.from(chunk)
|
|
134
|
+
);
|
|
135
|
+
}
|
|
9
136
|
const createCacheMiddleware = ({
|
|
10
137
|
cache
|
|
11
138
|
}) => {
|
|
12
139
|
const cacheMiddleware = router__default.default();
|
|
13
140
|
cacheMiddleware.use(async (req, res, next) => {
|
|
14
|
-
const socket = res.socket;
|
|
15
141
|
const isCacheable = req.path.startsWith("/static/docs/");
|
|
16
142
|
const isGetRequest = req.method === "GET";
|
|
17
|
-
if (!isCacheable
|
|
143
|
+
if (!isCacheable) {
|
|
18
144
|
next();
|
|
19
145
|
return;
|
|
20
146
|
}
|
|
21
147
|
const reqPath = decodeURI(req.path.match(/\/static\/docs\/(.*)$/)[1]);
|
|
22
|
-
const
|
|
23
|
-
const realWrite = socket.write.bind(socket);
|
|
24
|
-
let writeToCache = true;
|
|
25
|
-
const chunks = [];
|
|
26
|
-
socket.write = (data, encoding, callback) => {
|
|
27
|
-
chunks.push(
|
|
28
|
-
typeof data === "string" ? Buffer.from(data) : Buffer.from(data)
|
|
29
|
-
);
|
|
30
|
-
if (typeof encoding === "function") {
|
|
31
|
-
return realWrite(data, encoding);
|
|
32
|
-
}
|
|
33
|
-
return realWrite(data, encoding, callback);
|
|
34
|
-
};
|
|
35
|
-
socket.on("close", async (hadError) => {
|
|
36
|
-
const content = Buffer.concat(chunks);
|
|
37
|
-
const head = content.toString("utf8", 0, 12);
|
|
38
|
-
if (isGetRequest && writeToCache && !hadError && head.match(/HTTP\/\d\.\d 200/)) {
|
|
39
|
-
await cache.set(reqPath, content);
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
const cached = await cache.get(reqPath);
|
|
148
|
+
const cached = deserializeCachedResponse(await cache.get(reqPath));
|
|
43
149
|
if (cached) {
|
|
44
|
-
|
|
45
|
-
|
|
150
|
+
res.status(cached.statusCode);
|
|
151
|
+
for (const [name, value] of Object.entries(cached.headers)) {
|
|
152
|
+
res.setHeader(name, value);
|
|
153
|
+
}
|
|
154
|
+
res.setHeader("Content-Length", cached.body.length);
|
|
155
|
+
res.end(req.method === "HEAD" ? void 0 : cached.body);
|
|
46
156
|
return;
|
|
47
157
|
}
|
|
158
|
+
if (isGetRequest) {
|
|
159
|
+
const chunks = [];
|
|
160
|
+
const writeHeadHeaders = {};
|
|
161
|
+
const realWrite = res.write.bind(res);
|
|
162
|
+
const realEnd = res.end.bind(res);
|
|
163
|
+
const realWriteHead = res.writeHead.bind(res);
|
|
164
|
+
res.writeHead = (statusCode, statusMessageOrHeaders, headers) => {
|
|
165
|
+
let result;
|
|
166
|
+
if (typeof statusMessageOrHeaders === "string") {
|
|
167
|
+
result = realWriteHead(statusCode, statusMessageOrHeaders, headers);
|
|
168
|
+
} else {
|
|
169
|
+
result = realWriteHead(statusCode, statusMessageOrHeaders);
|
|
170
|
+
}
|
|
171
|
+
captureWriteHeadHeaders(
|
|
172
|
+
writeHeadHeaders,
|
|
173
|
+
typeof statusMessageOrHeaders === "string" ? headers : statusMessageOrHeaders
|
|
174
|
+
);
|
|
175
|
+
return result;
|
|
176
|
+
};
|
|
177
|
+
res.write = (chunk, encoding, callback) => {
|
|
178
|
+
captureChunk(chunks, chunk, encoding);
|
|
179
|
+
if (typeof encoding === "function") {
|
|
180
|
+
return realWrite(chunk, encoding);
|
|
181
|
+
}
|
|
182
|
+
if (encoding === void 0) {
|
|
183
|
+
return callback ? realWrite(chunk, callback) : realWrite(chunk);
|
|
184
|
+
}
|
|
185
|
+
return realWrite(chunk, encoding, callback);
|
|
186
|
+
};
|
|
187
|
+
res.end = (chunk, encoding, callback) => {
|
|
188
|
+
if (typeof chunk === "function") {
|
|
189
|
+
return realEnd(chunk);
|
|
190
|
+
}
|
|
191
|
+
captureChunk(chunks, chunk, encoding);
|
|
192
|
+
if (typeof encoding === "function") {
|
|
193
|
+
return realEnd(chunk, encoding);
|
|
194
|
+
}
|
|
195
|
+
if (encoding === void 0) {
|
|
196
|
+
return callback ? realEnd(chunk, callback) : realEnd(chunk);
|
|
197
|
+
}
|
|
198
|
+
return realEnd(chunk, encoding, callback);
|
|
199
|
+
};
|
|
200
|
+
res.once("finish", () => {
|
|
201
|
+
if (res.statusCode === 200) {
|
|
202
|
+
void cache.set(
|
|
203
|
+
reqPath,
|
|
204
|
+
serializeCachedResponse(
|
|
205
|
+
res.statusCode,
|
|
206
|
+
getCacheableHeaders({
|
|
207
|
+
...res.getHeaders(),
|
|
208
|
+
...writeHeadHeaders
|
|
209
|
+
}),
|
|
210
|
+
Buffer.concat(chunks)
|
|
211
|
+
)
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
48
216
|
next();
|
|
49
217
|
});
|
|
50
218
|
return cacheMiddleware;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cacheMiddleware.cjs.js","sources":["../../src/cache/cacheMiddleware.ts"],"sourcesContent":["/*\n * Copyright 2021 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 */\nimport { Router } from 'express';\nimport router from 'express-promise-router';\nimport { TechDocsCache } from './TechDocsCache';\nimport { LoggerService } from '@backstage/backend-plugin-api';\n\ntype CacheMiddlewareOptions = {\n cache: TechDocsCache;\n logger: LoggerService;\n};\n\ntype ErrorCallback = (err?: Error | null) => void;\n\nexport const createCacheMiddleware = ({\n cache,\n}: CacheMiddlewareOptions): Router => {\n const cacheMiddleware = router();\n\n // Middleware that, through socket monkey patching, captures responses as\n // they're sent over /static/docs/* and caches them. Subsequent requests are\n // loaded from cache. Cache key is the object's path (after `/static/docs/`).\n cacheMiddleware.use(async (req, res, next) => {\n const socket = res.socket;\n const isCacheable = req.path.startsWith('/static/docs/');\n const isGetRequest = req.method === 'GET';\n\n // Continue early if this is non-cacheable, or there's no socket.\n if (!isCacheable || !socket) {\n next();\n return;\n }\n\n // Make concrete references to these things.\n const reqPath = decodeURI(req.path.match(/\\/static\\/docs\\/(.*)$/)![1]);\n const realEnd = socket.end.bind(socket);\n const realWrite = socket.write.bind(socket);\n let writeToCache = true;\n const chunks: Buffer[] = [];\n\n // Monkey-patch the response's socket to keep track of chunks as they are\n // written over the wire.\n socket.write = (\n data: string | Uint8Array,\n encoding?: BufferEncoding | ErrorCallback,\n callback?: ErrorCallback,\n ) => {\n // This cast is obviously weird, but it covers a type bug in @types/node\n // which does not gracefully handle union types.\n chunks.push(\n typeof data === 'string' ? Buffer.from(data) : Buffer.from(data),\n );\n if (typeof encoding === 'function') {\n return realWrite(data, encoding);\n }\n return realWrite(data, encoding, callback);\n };\n\n // When a socket is closed, if there were no errors and the data written\n // over the socket should be cached, cache it!\n socket.on('close', async hadError => {\n const content = Buffer.concat(chunks);\n const head = content.toString('utf8', 0, 12);\n if (\n isGetRequest &&\n writeToCache &&\n !hadError &&\n head.match(/HTTP\\/\\d\\.\\d 200/)\n ) {\n await cache.set(reqPath, content);\n }\n });\n\n // Attempt to retrieve data from the cache.\n const cached = await cache.get(reqPath);\n\n // If there is a cache hit, write it out on the socket, ensure we don't re-\n // cache the data, and prevent going back to canonical storage by never\n // calling next().\n if (cached) {\n writeToCache = false;\n realEnd(cached);\n return;\n }\n\n // No data retrieved from cache: allow retrieval from canonical storage.\n next();\n });\n\n return cacheMiddleware;\n};\n"],"names":["router"],"mappings":";;;;;;;;AA2BO,MAAM,wBAAwB,CAAC;AAAA,EACpC;AACF,CAAA,KAAsC;AACpC,EAAA,MAAM,kBAAkBA,uBAAA,EAAO;AAK/B,EAAA,eAAA,CAAgB,GAAA,CAAI,OAAO,GAAA,EAAK,GAAA,EAAK,IAAA,KAAS;AAC5C,IAAA,MAAM,SAAS,GAAA,CAAI,MAAA;AACnB,IAAA,MAAM,WAAA,GAAc,GAAA,CAAI,IAAA,CAAK,UAAA,CAAW,eAAe,CAAA;AACvD,IAAA,MAAM,YAAA,GAAe,IAAI,MAAA,KAAW,KAAA;AAGpC,IAAA,IAAI,CAAC,WAAA,IAAe,CAAC,MAAA,EAAQ;AAC3B,MAAA,IAAA,EAAK;AACL,MAAA;AAAA,IACF;AAGA,IAAA,MAAM,OAAA,GAAU,UAAU,GAAA,CAAI,IAAA,CAAK,MAAM,uBAAuB,CAAA,CAAG,CAAC,CAAC,CAAA;AACrE,IAAA,MAAM,OAAA,GAAU,MAAA,CAAO,GAAA,CAAI,IAAA,CAAK,MAAM,CAAA;AACtC,IAAA,MAAM,SAAA,GAAY,MAAA,CAAO,KAAA,CAAM,IAAA,CAAK,MAAM,CAAA;AAC1C,IAAA,IAAI,YAAA,GAAe,IAAA;AACnB,IAAA,MAAM,SAAmB,EAAC;AAI1B,IAAA,MAAA,CAAO,KAAA,GAAQ,CACb,IAAA,EACA,QAAA,EACA,QAAA,KACG;AAGH,MAAA,MAAA,CAAO,IAAA;AAAA,QACL,OAAO,SAAS,QAAA,GAAW,MAAA,CAAO,KAAK,IAAI,CAAA,GAAI,MAAA,CAAO,IAAA,CAAK,IAAI;AAAA,OACjE;AACA,MAAA,IAAI,OAAO,aAAa,UAAA,EAAY;AAClC,QAAA,OAAO,SAAA,CAAU,MAAM,QAAQ,CAAA;AAAA,MACjC;AACA,MAAA,OAAO,SAAA,CAAU,IAAA,EAAM,QAAA,EAAU,QAAQ,CAAA;AAAA,IAC3C,CAAA;AAIA,IAAA,MAAA,CAAO,EAAA,CAAG,OAAA,EAAS,OAAM,QAAA,KAAY;AACnC,MAAA,MAAM,OAAA,GAAU,MAAA,CAAO,MAAA,CAAO,MAAM,CAAA;AACpC,MAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,QAAA,CAAS,MAAA,EAAQ,GAAG,EAAE,CAAA;AAC3C,MAAA,IACE,gBACA,YAAA,IACA,CAAC,YACD,IAAA,CAAK,KAAA,CAAM,kBAAkB,CAAA,EAC7B;AACA,QAAA,MAAM,KAAA,CAAM,GAAA,CAAI,OAAA,EAAS,OAAO,CAAA;AAAA,MAClC;AAAA,IACF,CAAC,CAAA;AAGD,IAAA,MAAM,MAAA,GAAS,MAAM,KAAA,CAAM,GAAA,CAAI,OAAO,CAAA;AAKtC,IAAA,IAAI,MAAA,EAAQ;AACV,MAAA,YAAA,GAAe,KAAA;AACf,MAAA,OAAA,CAAQ,MAAM,CAAA;AACd,MAAA;AAAA,IACF;AAGA,IAAA,IAAA,EAAK;AAAA,EACP,CAAC,CAAA;AAED,EAAA,OAAO,eAAA;AACT;;"}
|
|
1
|
+
{"version":3,"file":"cacheMiddleware.cjs.js","sources":["../../src/cache/cacheMiddleware.ts"],"sourcesContent":["/*\n * Copyright 2021 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 */\nimport { Router } from 'express';\nimport router from 'express-promise-router';\nimport type { OutgoingHttpHeader, OutgoingHttpHeaders } from 'node:http';\nimport { TechDocsCache } from './TechDocsCache';\nimport { LoggerService } from '@backstage/backend-plugin-api';\n\ntype CacheMiddlewareOptions = {\n cache: TechDocsCache;\n logger: LoggerService;\n};\n\ntype WriteCallback = (err?: Error | null) => void;\n\ntype CachedResponse = {\n statusCode: number;\n headers: Record<string, string | string[] | number>;\n body: Buffer;\n};\n\nconst CACHE_VERSION_HEADER = 'x-backstage-techdocs-cache-version';\nconst CACHE_VERSION = '1';\nconst HEADER_SEPARATOR = Buffer.from('\\r\\n\\r\\n');\n\nconst omittedCachedHeaders = new Set([\n 'connection',\n 'content-encoding',\n 'content-length',\n 'date',\n 'keep-alive',\n 'proxy-authenticate',\n 'proxy-authorization',\n 'te',\n 'trailer',\n 'transfer-encoding',\n 'upgrade',\n]);\n\nfunction serializeCachedResponse(\n statusCode: number,\n headers: Record<string, string | string[] | number>,\n body: Buffer,\n): Buffer {\n const headerLines = Object.entries({\n ...headers,\n [CACHE_VERSION_HEADER]: CACHE_VERSION,\n 'content-length': body.length,\n connection: 'close',\n }).flatMap(([name, value]) =>\n (Array.isArray(value) ? value : [value]).map(item => `${name}: ${item}`),\n );\n\n // Keep the entry valid as a raw HTTP response so that old instances can\n // safely serve entries written by new instances during a rolling update.\n return Buffer.concat([\n Buffer.from(\n `HTTP/1.1 ${statusCode} OK\\r\\n${headerLines.join('\\r\\n')}`,\n 'latin1',\n ),\n HEADER_SEPARATOR,\n body,\n ]);\n}\n\nfunction deserializeCachedResponse(\n data: Buffer | undefined,\n): CachedResponse | undefined {\n if (!data) {\n return undefined;\n }\n\n try {\n const headersEnd = data.indexOf(HEADER_SEPARATOR);\n if (headersEnd === -1) {\n return undefined;\n }\n\n const [statusLine, ...headerLines] = data\n .toString('latin1', 0, headersEnd)\n .split('\\r\\n');\n if (statusLine !== 'HTTP/1.1 200 OK') {\n return undefined;\n }\n\n const headers: Record<string, string | string[] | number> = {};\n for (const line of headerLines) {\n const separator = line.indexOf(':');\n if (separator <= 0) {\n return undefined;\n }\n const name = line.slice(0, separator).trim().toLowerCase();\n const value = line.slice(separator + 1).trim();\n const existing = headers[name];\n if (existing === undefined) {\n headers[name] = value;\n } else if (Array.isArray(existing)) {\n headers[name] = [...existing, value];\n } else {\n headers[name] = [String(existing), value];\n }\n }\n\n if (headers[CACHE_VERSION_HEADER] !== CACHE_VERSION) {\n return undefined;\n }\n delete headers[CACHE_VERSION_HEADER];\n\n const body = data.subarray(headersEnd + HEADER_SEPARATOR.length);\n if (headers['content-length'] !== String(body.length)) {\n return undefined;\n }\n delete headers['content-length'];\n delete headers.connection;\n\n return {\n statusCode: 200,\n headers,\n body,\n };\n } catch {\n return undefined;\n }\n}\n\nfunction getCacheableHeaders(\n headers: OutgoingHttpHeaders,\n): Record<string, string | string[] | number> {\n return Object.fromEntries(\n Object.entries(headers).filter(\n (entry): entry is [string, string | string[] | number] =>\n entry[1] !== undefined &&\n !omittedCachedHeaders.has(entry[0].toLowerCase()),\n ),\n );\n}\n\nfunction captureWriteHeadHeaders(\n target: OutgoingHttpHeaders,\n headers: OutgoingHttpHeaders | OutgoingHttpHeader[] | undefined,\n) {\n if (!headers) {\n return;\n }\n\n if (!Array.isArray(headers)) {\n for (const [name, value] of Object.entries(headers)) {\n target[name.toLowerCase()] = value;\n }\n return;\n }\n\n for (let i = 0; i < headers.length; i += 2) {\n const name = headers[i];\n const value = headers[i + 1];\n if (typeof name !== 'string' || value === undefined) {\n continue;\n }\n\n const normalizedName = name.toLowerCase();\n const existing = target[normalizedName];\n if (existing === undefined) {\n target[normalizedName] = value;\n } else {\n target[normalizedName] = [\n ...(Array.isArray(existing) ? existing : [String(existing)]),\n ...(Array.isArray(value) ? value : [String(value)]),\n ];\n }\n }\n}\n\nfunction captureChunk(\n chunks: Buffer[],\n chunk: string | Uint8Array | undefined,\n encoding?: BufferEncoding | WriteCallback,\n) {\n if (chunk === undefined) {\n return;\n }\n chunks.push(\n typeof chunk === 'string'\n ? Buffer.from(chunk, typeof encoding === 'string' ? encoding : undefined)\n : Buffer.from(chunk),\n );\n}\n\nexport const createCacheMiddleware = ({\n cache,\n}: CacheMiddlewareOptions): Router => {\n const cacheMiddleware = router();\n\n // Middleware that captures responses sent over /static/docs/* and caches\n // them. Subsequent requests are loaded from cache. Cache key is the object's\n // path (after `/static/docs/`).\n cacheMiddleware.use(async (req, res, next) => {\n const isCacheable = req.path.startsWith('/static/docs/');\n const isGetRequest = req.method === 'GET';\n\n if (!isCacheable) {\n next();\n return;\n }\n\n const reqPath = decodeURI(req.path.match(/\\/static\\/docs\\/(.*)$/)![1]);\n const cached = deserializeCachedResponse(await cache.get(reqPath));\n if (cached) {\n res.status(cached.statusCode);\n for (const [name, value] of Object.entries(cached.headers)) {\n res.setHeader(name, value);\n }\n res.setHeader('Content-Length', cached.body.length);\n res.end(req.method === 'HEAD' ? undefined : cached.body);\n return;\n }\n\n if (isGetRequest) {\n const chunks: Buffer[] = [];\n const writeHeadHeaders: OutgoingHttpHeaders = {};\n const realWrite = res.write.bind(res);\n const realEnd = res.end.bind(res);\n const realWriteHead = res.writeHead.bind(res);\n\n res.writeHead = (\n statusCode: number,\n statusMessageOrHeaders?:\n | string\n | OutgoingHttpHeaders\n | OutgoingHttpHeader[],\n headers?: OutgoingHttpHeaders | OutgoingHttpHeader[],\n ) => {\n let result;\n if (typeof statusMessageOrHeaders === 'string') {\n result = realWriteHead(statusCode, statusMessageOrHeaders, headers);\n } else {\n result = realWriteHead(statusCode, statusMessageOrHeaders);\n }\n captureWriteHeadHeaders(\n writeHeadHeaders,\n typeof statusMessageOrHeaders === 'string'\n ? headers\n : statusMessageOrHeaders,\n );\n return result;\n };\n\n res.write = (\n chunk: string | Uint8Array,\n encoding?: BufferEncoding | WriteCallback,\n callback?: WriteCallback,\n ) => {\n captureChunk(chunks, chunk, encoding);\n if (typeof encoding === 'function') {\n return realWrite(chunk, encoding);\n }\n if (encoding === undefined) {\n return callback ? realWrite(chunk, callback) : realWrite(chunk);\n }\n return realWrite(chunk, encoding, callback);\n };\n\n res.end = (\n chunk?: string | Uint8Array | WriteCallback,\n encoding?: BufferEncoding | WriteCallback,\n callback?: WriteCallback,\n ) => {\n if (typeof chunk === 'function') {\n return realEnd(chunk);\n }\n captureChunk(chunks, chunk, encoding);\n if (typeof encoding === 'function') {\n return realEnd(chunk, encoding);\n }\n if (encoding === undefined) {\n return callback ? realEnd(chunk, callback) : realEnd(chunk);\n }\n return realEnd(chunk, encoding, callback);\n };\n\n res.once('finish', () => {\n if (res.statusCode === 200) {\n void cache.set(\n reqPath,\n serializeCachedResponse(\n res.statusCode,\n getCacheableHeaders({\n ...res.getHeaders(),\n ...writeHeadHeaders,\n }),\n Buffer.concat(chunks),\n ),\n );\n }\n });\n }\n\n next();\n });\n\n return cacheMiddleware;\n};\n"],"names":["router"],"mappings":"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"}
|
package/dist/plugin.cjs.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var backendPluginApi = require('@backstage/backend-plugin-api');
|
|
4
|
+
var alpha = require('@backstage/backend-plugin-api/alpha');
|
|
4
5
|
var pluginTechdocsNode = require('@backstage/plugin-techdocs-node');
|
|
5
6
|
var pluginCatalogNode = require('@backstage/plugin-catalog-node');
|
|
6
7
|
var router = require('./service/router.cjs.js');
|
|
8
|
+
var index = require('./actions/index.cjs.js');
|
|
7
9
|
|
|
8
10
|
const techdocsPlugin = backendPluginApi.createBackendPlugin({
|
|
9
11
|
pluginId: "techdocs",
|
|
@@ -67,7 +69,8 @@ const techdocsPlugin = backendPluginApi.createBackendPlugin({
|
|
|
67
69
|
cache: backendPluginApi.coreServices.cache,
|
|
68
70
|
httpAuth: backendPluginApi.coreServices.httpAuth,
|
|
69
71
|
auth: backendPluginApi.coreServices.auth,
|
|
70
|
-
catalog: pluginCatalogNode.catalogServiceRef
|
|
72
|
+
catalog: pluginCatalogNode.catalogServiceRef,
|
|
73
|
+
actionsRegistry: alpha.actionsRegistryServiceRef
|
|
71
74
|
},
|
|
72
75
|
async init({
|
|
73
76
|
config,
|
|
@@ -78,7 +81,8 @@ const techdocsPlugin = backendPluginApi.createBackendPlugin({
|
|
|
78
81
|
cache,
|
|
79
82
|
httpAuth,
|
|
80
83
|
auth,
|
|
81
|
-
catalog
|
|
84
|
+
catalog,
|
|
85
|
+
actionsRegistry
|
|
82
86
|
}) {
|
|
83
87
|
const preparers = await pluginTechdocsNode.Preparers.fromConfig(config, {
|
|
84
88
|
reader: urlReader,
|
|
@@ -118,6 +122,11 @@ const techdocsPlugin = backendPluginApi.createBackendPlugin({
|
|
|
118
122
|
path: "/static",
|
|
119
123
|
allow: "user-cookie"
|
|
120
124
|
});
|
|
125
|
+
index.createTechdocsActions({
|
|
126
|
+
actionsRegistry,
|
|
127
|
+
auth,
|
|
128
|
+
discovery
|
|
129
|
+
});
|
|
121
130
|
}
|
|
122
131
|
});
|
|
123
132
|
}
|
package/dist/plugin.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["/*\n * Copyright 2023 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 {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport {\n DocsBuildStrategy,\n Generators,\n PreparerBase,\n Preparers,\n Publisher,\n PublisherBase,\n PublisherSettings,\n PublisherType,\n RemoteProtocol,\n techdocsBuildsExtensionPoint,\n TechdocsGenerator,\n techdocsGeneratorExtensionPoint,\n techdocsPreparerExtensionPoint,\n techdocsPublisherExtensionPoint,\n} from '@backstage/plugin-techdocs-node';\nimport { catalogServiceRef } from '@backstage/plugin-catalog-node';\nimport * as winston from 'winston';\nimport { createRouter } from './service/router';\n\n/**\n * The TechDocs plugin is responsible for serving and building documentation for any entity.\n * @public\n */\nexport const techdocsPlugin = createBackendPlugin({\n pluginId: 'techdocs',\n register(env) {\n let docsBuildStrategy: DocsBuildStrategy | undefined;\n let buildLogTransport: winston.transport | undefined;\n env.registerExtensionPoint(techdocsBuildsExtensionPoint, {\n setBuildStrategy(buildStrategy: DocsBuildStrategy) {\n if (docsBuildStrategy) {\n throw new Error('DocsBuildStrategy may only be set once');\n }\n docsBuildStrategy = buildStrategy;\n },\n setBuildLogTransport(transport: winston.transport) {\n if (buildLogTransport) {\n throw new Error('BuildLogTransport may only be set once');\n }\n buildLogTransport = transport;\n },\n });\n\n let customTechdocsGenerator: TechdocsGenerator | undefined;\n env.registerExtensionPoint(techdocsGeneratorExtensionPoint, {\n setTechdocsGenerator(generator: TechdocsGenerator) {\n if (customTechdocsGenerator) {\n throw new Error('TechdocsGenerator may only be set once');\n }\n\n customTechdocsGenerator = generator;\n },\n });\n\n const customPreparers = new Map<RemoteProtocol, PreparerBase>();\n env.registerExtensionPoint(techdocsPreparerExtensionPoint, {\n registerPreparer(protocol: RemoteProtocol, preparer: PreparerBase) {\n if (customPreparers.has(protocol)) {\n throw new Error(\n `Preparer for protocol ${protocol} is already registered`,\n );\n }\n customPreparers.set(protocol, preparer);\n },\n });\n\n let customTechdocsPublisher: PublisherBase | undefined;\n const publisherSettings: PublisherSettings = {};\n env.registerExtensionPoint(techdocsPublisherExtensionPoint, {\n registerPublisher(type: PublisherType, publisher: PublisherBase) {\n if (customTechdocsPublisher) {\n throw new Error(`Publisher for type ${type} is already registered`);\n }\n customTechdocsPublisher = publisher;\n },\n registerPublisherSettings<T extends keyof PublisherSettings>(\n publisher: T,\n settings: PublisherSettings[T],\n ) {\n publisherSettings[publisher] = settings;\n },\n });\n\n env.registerInit({\n deps: {\n config: coreServices.rootConfig,\n logger: coreServices.logger,\n urlReader: coreServices.urlReader,\n http: coreServices.httpRouter,\n discovery: coreServices.discovery,\n cache: coreServices.cache,\n httpAuth: coreServices.httpAuth,\n auth: coreServices.auth,\n catalog: catalogServiceRef,\n },\n async init({\n config,\n logger,\n urlReader,\n http,\n discovery,\n cache,\n httpAuth,\n auth,\n catalog,\n }) {\n // Preparers are responsible for fetching source files for documentation.\n const preparers = await Preparers.fromConfig(config, {\n reader: urlReader,\n logger: logger,\n });\n for (const [protocol, preparer] of customPreparers.entries()) {\n preparers.register(protocol, preparer);\n }\n\n // Generators are used for generating documentation sites.\n const generators = await Generators.fromConfig(config, {\n logger: logger,\n customGenerator: customTechdocsGenerator,\n });\n\n // Publisher is used for\n // 1. Publishing generated files to storage\n // 2. Fetching files from storage and passing them to TechDocs frontend.\n const publisher = await Publisher.fromConfig(config, {\n logger: logger,\n discovery: discovery,\n customPublisher: customTechdocsPublisher,\n publisherSettings,\n });\n\n // checks if the publisher is working and logs the result\n await publisher.getReadiness();\n\n http.use(\n await createRouter({\n logger: logger,\n cache,\n docsBuildStrategy,\n buildLogTransport,\n preparers,\n generators,\n publisher,\n config,\n discovery,\n httpAuth,\n auth,\n catalog,\n }),\n );\n\n http.addAuthPolicy({\n path: '/static',\n allow: 'user-cookie',\n });\n },\n });\n },\n});\n"],"names":["createBackendPlugin","techdocsBuildsExtensionPoint","techdocsGeneratorExtensionPoint","techdocsPreparerExtensionPoint","techdocsPublisherExtensionPoint","coreServices","catalogServiceRef","Preparers","Generators","Publisher","createRouter"],"mappings":"
|
|
1
|
+
{"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["/*\n * Copyright 2023 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 {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha';\nimport {\n DocsBuildStrategy,\n Generators,\n PreparerBase,\n Preparers,\n Publisher,\n PublisherBase,\n PublisherSettings,\n PublisherType,\n RemoteProtocol,\n techdocsBuildsExtensionPoint,\n TechdocsGenerator,\n techdocsGeneratorExtensionPoint,\n techdocsPreparerExtensionPoint,\n techdocsPublisherExtensionPoint,\n} from '@backstage/plugin-techdocs-node';\nimport { catalogServiceRef } from '@backstage/plugin-catalog-node';\nimport * as winston from 'winston';\nimport { createRouter } from './service/router';\nimport { createTechdocsActions } from './actions';\n\n/**\n * The TechDocs plugin is responsible for serving and building documentation for any entity.\n * @public\n */\nexport const techdocsPlugin = createBackendPlugin({\n pluginId: 'techdocs',\n register(env) {\n let docsBuildStrategy: DocsBuildStrategy | undefined;\n let buildLogTransport: winston.transport | undefined;\n env.registerExtensionPoint(techdocsBuildsExtensionPoint, {\n setBuildStrategy(buildStrategy: DocsBuildStrategy) {\n if (docsBuildStrategy) {\n throw new Error('DocsBuildStrategy may only be set once');\n }\n docsBuildStrategy = buildStrategy;\n },\n setBuildLogTransport(transport: winston.transport) {\n if (buildLogTransport) {\n throw new Error('BuildLogTransport may only be set once');\n }\n buildLogTransport = transport;\n },\n });\n\n let customTechdocsGenerator: TechdocsGenerator | undefined;\n env.registerExtensionPoint(techdocsGeneratorExtensionPoint, {\n setTechdocsGenerator(generator: TechdocsGenerator) {\n if (customTechdocsGenerator) {\n throw new Error('TechdocsGenerator may only be set once');\n }\n\n customTechdocsGenerator = generator;\n },\n });\n\n const customPreparers = new Map<RemoteProtocol, PreparerBase>();\n env.registerExtensionPoint(techdocsPreparerExtensionPoint, {\n registerPreparer(protocol: RemoteProtocol, preparer: PreparerBase) {\n if (customPreparers.has(protocol)) {\n throw new Error(\n `Preparer for protocol ${protocol} is already registered`,\n );\n }\n customPreparers.set(protocol, preparer);\n },\n });\n\n let customTechdocsPublisher: PublisherBase | undefined;\n const publisherSettings: PublisherSettings = {};\n env.registerExtensionPoint(techdocsPublisherExtensionPoint, {\n registerPublisher(type: PublisherType, publisher: PublisherBase) {\n if (customTechdocsPublisher) {\n throw new Error(`Publisher for type ${type} is already registered`);\n }\n customTechdocsPublisher = publisher;\n },\n registerPublisherSettings<T extends keyof PublisherSettings>(\n publisher: T,\n settings: PublisherSettings[T],\n ) {\n publisherSettings[publisher] = settings;\n },\n });\n\n env.registerInit({\n deps: {\n config: coreServices.rootConfig,\n logger: coreServices.logger,\n urlReader: coreServices.urlReader,\n http: coreServices.httpRouter,\n discovery: coreServices.discovery,\n cache: coreServices.cache,\n httpAuth: coreServices.httpAuth,\n auth: coreServices.auth,\n catalog: catalogServiceRef,\n actionsRegistry: actionsRegistryServiceRef,\n },\n async init({\n config,\n logger,\n urlReader,\n http,\n discovery,\n cache,\n httpAuth,\n auth,\n catalog,\n actionsRegistry,\n }) {\n // Preparers are responsible for fetching source files for documentation.\n const preparers = await Preparers.fromConfig(config, {\n reader: urlReader,\n logger: logger,\n });\n for (const [protocol, preparer] of customPreparers.entries()) {\n preparers.register(protocol, preparer);\n }\n\n // Generators are used for generating documentation sites.\n const generators = await Generators.fromConfig(config, {\n logger: logger,\n customGenerator: customTechdocsGenerator,\n });\n\n // Publisher is used for\n // 1. Publishing generated files to storage\n // 2. Fetching files from storage and passing them to TechDocs frontend.\n const publisher = await Publisher.fromConfig(config, {\n logger: logger,\n discovery: discovery,\n customPublisher: customTechdocsPublisher,\n publisherSettings,\n });\n\n // checks if the publisher is working and logs the result\n await publisher.getReadiness();\n\n http.use(\n await createRouter({\n logger: logger,\n cache,\n docsBuildStrategy,\n buildLogTransport,\n preparers,\n generators,\n publisher,\n config,\n discovery,\n httpAuth,\n auth,\n catalog,\n }),\n );\n\n http.addAuthPolicy({\n path: '/static',\n allow: 'user-cookie',\n });\n\n // Register MCP actions for techdocs\n createTechdocsActions({\n actionsRegistry,\n auth,\n discovery,\n });\n },\n });\n },\n});\n"],"names":["createBackendPlugin","techdocsBuildsExtensionPoint","techdocsGeneratorExtensionPoint","techdocsPreparerExtensionPoint","techdocsPublisherExtensionPoint","coreServices","catalogServiceRef","actionsRegistryServiceRef","Preparers","Generators","Publisher","createRouter","createTechdocsActions"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backstage/plugin-techdocs-backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0-next.2",
|
|
4
4
|
"description": "The Backstage backend plugin that renders technical documentation for your components",
|
|
5
5
|
"backstage": {
|
|
6
6
|
"role": "backend-plugin",
|
|
@@ -80,9 +80,10 @@
|
|
|
80
80
|
"@backstage/catalog-model": "1.10.2-next.0",
|
|
81
81
|
"@backstage/config": "1.3.10-next.0",
|
|
82
82
|
"@backstage/errors": "1.3.2-next.0",
|
|
83
|
-
"@backstage/integration": "2.1.3-next.
|
|
83
|
+
"@backstage/integration": "2.1.3-next.1",
|
|
84
84
|
"@backstage/plugin-catalog-node": "2.2.6-next.1",
|
|
85
|
-
"@backstage/plugin-techdocs-
|
|
85
|
+
"@backstage/plugin-techdocs-common": "0.1.2-next.0",
|
|
86
|
+
"@backstage/plugin-techdocs-node": "2.0.3-next.2",
|
|
86
87
|
"@backstage/types": "1.2.2",
|
|
87
88
|
"express": "^4.22.0",
|
|
88
89
|
"express-promise-router": "^4.1.0",
|
|
@@ -92,10 +93,12 @@
|
|
|
92
93
|
"winston": "^3.2.1"
|
|
93
94
|
},
|
|
94
95
|
"devDependencies": {
|
|
95
|
-
"@backstage/backend-defaults": "0.18.1-next.
|
|
96
|
+
"@backstage/backend-defaults": "0.18.1-next.2",
|
|
96
97
|
"@backstage/backend-test-utils": "1.11.8-next.1",
|
|
97
98
|
"@backstage/cli": "0.36.7-next.0",
|
|
99
|
+
"@types/compression": "^1.7.5",
|
|
98
100
|
"@types/express": "^4.17.6",
|
|
101
|
+
"compression": "^1.7.4",
|
|
99
102
|
"msw": "^2.0.0",
|
|
100
103
|
"supertest": "^7.0.0"
|
|
101
104
|
},
|