refactor: reunification of techdocs-cli and backstage repos

Signed-off-by: Andrew Thauer <athauer@wealthsimple.com>
This commit is contained in:
Andrew Thauer
2021-10-28 10:08:08 -04:00
committed by Camila Belo
parent 01a0a39521
commit 71e7b0ee92
62 changed files with 3089 additions and 3 deletions
+9
View File
@@ -0,0 +1,9 @@
---
'@techdocs/cli': patch
---
Reunifies the [techdocs-cli](https://github.com/backstage/techdocs-cli) monorepo
code back into the main [backstage](https://github.com/backstage/backstage) repo
(see [7288](https://github.com/backstage/backstage/issues/7288)). The changes
include some internal refactoring that do not affect functionality beyond the
local development setup.
+2
View File
@@ -18,7 +18,9 @@
/plugins/techdocs-backend @backstage/techdocs-core
/plugins/ilert @backstage/reviewers @yacut
/plugins/home @backstage/techdocs-core
/packages/embedded-techdocs-app @backstage/techdocs-core
/packages/search-common @backstage/techdocs-core
/packages/techdocs-cli @backstage/techdocs-core
/packages/techdocs-common @backstage/techdocs-core
/.changeset/cost-insights-* @backstage/reviewers @backstage/silver-lining
/.changeset/search-* @backstage/techdocs-core
+1
View File
@@ -62,6 +62,7 @@ Debounce
declaratively
deduplicated
deps
dependabot
destructured
dev
devops
+42
View File
@@ -0,0 +1,42 @@
name: Techdocs E2E Test
on:
pull_request:
paths-ignore:
- '.changeset/**'
- 'contrib/**'
- 'docs/**'
- 'microsite/**'
jobs:
verify:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x]
env:
CI: true
NODE_OPTIONS: --max-old-space-size=4096
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- name: install dependencies
run: yarn install --frozen-lockfile
- name: generate types
run: yarn tsc
- name: build techdocs-cli
working-directory: packages/techdocs-cli
run: yarn build
- name: Install mkdocs & techdocs-core
run: python -m pip install mkdocs-techdocs-core
- name: techdocs-cli e2e test
working-directory: packages/techdocs-cli
run: yarn test:e2e
Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

+241
View File
@@ -0,0 +1,241 @@
# TechDocs CLI
Utility command line interface for managing TechDocs sites in
[Backstage](https://github.com/backstage/backstage).
https://backstage.io/docs/features/techdocs/techdocs-overview
## Features
- Supports local development/preview of a TechDocs site in a Backstage app.
- Supports generation and publishing of a documentation site in a CI/CD
workflow.
```bash
techdocs-cli --help
Usage: techdocs-cli [options] [command]
Options:
-V, --version output the version number
-h, --help display help for command
Commands:
generate|build [options] Generate TechDocs documentation site using mkdocs.
publish [options] Publish generated TechDocs site to an external storage AWS S3,
Google GCS, etc.
serve:mkdocs [options] Serve a documentation project locally using mkdocs serve.
serve [options] Serve a documentation project locally in a Backstage app-like
environment
help [command] display help for command
```
## Installation
You can always use [`npx`](https://github.com/npm/npx) to run the latest version
of `techdocs-cli` -
```bash
npx @techdocs/cli [command]
```
Or you can install it using [npm](https://www.npmjs.com/package/@techdocs/cli) -
```bash
npm install -g @techdocs/cli
techdocs-cli [command]
```
## Usage
### Preview TechDocs site locally in a Backstage like environment
```bash
techdocs-cli serve
```
![A preview of techdocs-cli serve command](../../assets/features/techdocs/techdocs-cli-serve-preview.png)
By default, Docker and
[techdocs-container](https://github.com/backstage/techdocs-container) is used to
make sure all the dependencies are installed. However, Docker can be disabled
with `--no-docker` flag.
The command starts two local servers - an MkDocs preview server on port 8000 and
a Backstage app server on port 3000. The Backstage app has a custom TechDocs API
implementation, which uses the MkDocs preview server as a proxy to fetch the
generated documentation files and assets.
NOTE: When using a custom `techdocs` docker image, make sure the entry point is
also `ENTRYPOINT ["mkdocs"]`.
Command reference:
```bash
Usage: techdocs-cli serve [options]
Serve a documentation project locally in a Backstage app-like environment
Options:
-i, --docker-image <DOCKER_IMAGE> The mkdocs docker container to use (default: "spotify/techdocs")
--no-docker Do not use Docker, use MkDocs executable in current user environment.
--mkdocs-port <PORT> Port for MkDocs server to use (default: "8000")
-v --verbose Enable verbose output. (default: false)
-h, --help display help for command
```
### Generate TechDocs site from a documentation project
```bash
techdocs-cli generate
```
Alias: `techdocs-cli build`
The generate command uses the
[`@backstage/techdocs-common`](https://github.com/backstage/backstage/tree/master/packages/techdocs-common)
package from Backstage for consistency. A Backstage app can also generate and
publish TechDocs sites if `techdocs.builder` is set to `'local'` in
`app-config.yaml`. See
[configuration reference](https://backstage.io/docs/features/techdocs/configuration).
By default, this command uses Docker and
[techdocs-container](https://github.com/backstage/techdocs-container) to make
sure all the dependencies are installed. But it can be disabled using
`--no-docker` flag.
Command reference:
```bash
techdocs-cli generate --help
Usage: techdocs-cli generate|build [options]
Generate TechDocs documentation site using MkDocs.
Options:
--source-dir <PATH> Source directory containing mkdocs.yml and docs/ directory. (default: ".")
--output-dir <PATH> Output directory containing generated TechDocs site. (default: "./site/")
--docker-image <DOCKER_IMAGE> The mkdocs docker container to use (default: "spotify/techdocs:v0.3.4")
--no-pull Do not pull the latest docker image
--no-docker Do not use Docker, use MkDocs executable and plugins in current user environment.
--techdocs-ref <HOST_TYPE:URL> The repository hosting documentation source files e.g.
github:https://ghe.mycompany.net.com/org/repo.
This value is same as the backstage.io/techdocs-ref annotation of the corresponding
Backstage entity.
It is completely fine to skip this as it is only being used to set repo_url in mkdocs.yml
if not found.
--etag <ETAG> A unique identifier for the prepared tree e.g. commit SHA. If provided it will be stored
in techdocs_metadata.json.
-v --verbose Enable verbose output. (default: false)
-h, --help display help for command
```
### Publish generated TechDocs sites
```bash
techdocs-cli publish --publisher-type <awsS3|googleGcs|azureBlobStorage> --storage-name <bucket/container name> --entity <namespace/kind/name>
```
After generating a TechDocs site using `techdocs-cli generate`, use the publish
command to upload the static generated files on a cloud storage (AWS/GCS) bucket
or (Azure) container which your Backstage app can read from.
The value for `--entity` must be the Backstage entity which the generated
TechDocs site belongs to. You can find the values in your Entity's
`catalog-info.yaml` file. If namespace is missing in the `catalog-info.yaml`,
use `default`. The directory structure used in the storage bucket is
`namespace/kind/name/<files>`.
Note that the values are case-sensitive. An example for `--entity` is
`default/Component/<entityName>`.
Command reference:
```bash
Usage: techdocs-cli publish [options]
Publish generated TechDocs site to an external storage AWS S3, Google GCS, etc.
Options:
--publisher-type <TYPE> (Required always) awsS3 | googleGcs | azureBlobStorage
- same as techdocs.publisher.type in Backstage
app-config.yaml
--storage-name <BUCKET/CONTAINER NAME> (Required always) In case of AWS/GCS, use the bucket
name. In case of Azure, use container name. Same as
techdocs.publisher.[TYPE].bucketName
--entity <NAMESPACE/KIND/NAME> (Required always) Entity uid separated by / in
namespace/kind/name order (case-sensitive). Example:
default/Component/myEntity
--legacyUseCaseSensitiveTripletPaths Publishes objects with cased entity triplet prefix when set (e.g. namespace/Kind/name).
Only use if your TechDocs backend is configured the same way
--azureAccountName <AZURE ACCOUNT NAME> (Required for Azure) specify when --publisher-type
azureBlobStorage
--azureAccountKey <AZURE ACCOUNT KEY> Azure Storage Account key to use for authentication.
If not specified, you must set AZURE_TENANT_ID,
AZURE_CLIENT_ID & AZURE_CLIENT_SECRET as environment
variables.
--awsRoleArn <AWS ROLE ARN> Optional AWS ARN of role to be assumed.
--awsEndpoint <AWS ENDPOINT> Optional AWS endpoint to send requests to.
--awsS3ForcePathStyle Optional AWS S3 option to force path style.
--directory <PATH> Path of the directory containing generated files to
publish (default: "./site/")
-h, --help display help for command
```
### Migrate content for case-insensitive access
Prior to the beta version of TechDocs (`v[0.11.0]`), TechDocs were stored in
object storage using a case-sensitive entity triplet (e.g.
`default/API/name/index.html`). This resulted in a limitation where that exact
case was required in the Backstage URL in order to read/render TechDocs content.
As of `v[0.11.0]` of the TechDocs plugin, any case is allowed in the URL (e.g.
`default/api/name`), matching the behavior of the Catalog plugin.
Backstage instances created with TechDocs `v[0.11.0]` or later do not need this
command. However, when upgrading to this version from an older version of
TechDocs, the `migrate` command can be used prior to deployment to ensure docs
remain accessible without having to rebuild all docs.
Prior to upgrading to `v[0.11.0]` or greater, run this command to copy all
assets to their lower-case triplet equivalents like this:
```bash
techdocs-cli migrate --publisher-type <awsS3|googleGcs|azureBlobStorage> --storage-name <bucket/container name> --verbose
```
Once migrated and the upgraded version of the Backstage plugin has been
deployed, you can clean up the legacy, case-sensitive triplet files by
re-running the command with the `--removeOriginal` flag passed, which _moves_
(rather than copies) the files. Note: this deletes files and is therefore a
destructive operation that should performed with caution.
```bash
techdocs-cli migrate --publisher-type <awsS3|googleGcs|azureBlobStorage> --storage-name <bucket/container name> --removeOriginal --verbose
```
Afterward, update your TechDocs CLI to `v[0.7.0]` to ensure further publishing
happens using a lower-case entity triplet.
Note: arguments for this command largely match those of the `publish` command,
depending on your chosen storage provider. Run `techdocs-cli migrate --help` for
details.
#### Authentication
You need to make sure that your environment is able to authenticate with the
target cloud provider. `techdocs-cli` uses the official Node.js clients provided
by AWS (v2), Google Cloud and Azure. You can authenticate using environment
variables and/or by other means (`~/.aws/credentials`, `~/.config/gcloud` etc.)
Refer to the Authentication section of the following documentation depending
upon your cloud storage provider -
- [Google Cloud Storage](https://backstage.io/docs/features/techdocs/using-cloud-storage#configuring-google-gcs-bucket-with-techdocs)
- [AWS S3](https://backstage.io/docs/features/techdocs/using-cloud-storage#configuring-aws-s3-bucket-with-techdocs)
- [Azure Blob Storage](https://backstage.io/docs/features/techdocs/using-cloud-storage#configuring-azure-blob-storage-container-with-techdocs)
## Development
You are welcome to contribute to TechDocs CLI to improve it and support new
features! See the project
[README](https://github.com/backstage/backstage/blob/main/src/packages/techdocs-cli/README.md)
for more information.
+1
View File
@@ -105,6 +105,7 @@
"features/techdocs/configuration",
"features/techdocs/using-cloud-storage",
"features/techdocs/configuring-ci-cd",
"features/techdocs/cli",
"features/techdocs/how-to-guides",
"features/techdocs/troubleshooting",
"features/techdocs/faqs"
+1 -1
View File
@@ -61,7 +61,6 @@ nav:
- Writing Custom Actions: 'features/software-templates/writing-custom-actions.md'
- Writing Templates (Legacy): 'features/software-templates/legacy.md'
- Migrating from v1alpha1 to v1beta2 templates: 'features/software-templates/migrating-from-v1alpha1-to-v1beta2.md'
- Backstage Search:
- Overview: 'features/search/README.md'
- Getting Started: 'features/search/getting-started.md'
@@ -78,6 +77,7 @@ nav:
- TechDocs Configuration Options: 'features/techdocs/configuration.md'
- Using Cloud Storage: 'features/techdocs/using-cloud-storage.md'
- Configuring CI/CD to generate and publish TechDocs sites: 'features/techdocs/configuring-ci-cd.md'
- CLI: 'features/techdocs/cli.md'
- HOW TO guides: 'features/techdocs/how-to-guides.md'
- Troubleshooting: 'features/techdocs/troubleshooting.md'
- FAQ: 'features/techdocs/FAQ.md'
+1
View File
@@ -30,6 +30,7 @@
"lerna": "lerna",
"storybook": "yarn workspace storybook start",
"build-storybook": "yarn workspace storybook build-storybook",
"techdocs-cli:dev": "TECHDOCS_CLI_DEV_MODE=true packages/techdocs-cli/bin/techdocs-cli",
"prepare": "husky install",
"lock:check": "yarn-lock-check"
},
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
@@ -0,0 +1,7 @@
# NOTE: This file is used for testing techdocs-cli locally
backend:
baseUrl: http://localhost:7000
techdocs:
requestUrl: http://localhost:7000/api
@@ -0,0 +1,10 @@
app:
title: Techdocs Preview App
baseUrl: http://localhost:3000
backend:
baseUrl: http://localhost:3000
techdocs:
builder: 'external'
requestUrl: http://localhost:3000/api
@@ -0,0 +1,5 @@
{
"baseUrl": "http://localhost:3001",
"fixturesFolder": false,
"pluginsFile": false
}
@@ -0,0 +1,21 @@
{
"plugins": ["cypress"],
"extends": ["plugin:cypress/recommended"],
"rules": {
"jest/expect-expect": [
"error",
{
"assertFunctionNames": ["expect", "cy.contains"]
}
],
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": true,
"optionalDependencies": true,
"peerDependencies": true,
"bundledDependencies": true
}
]
}
}
@@ -0,0 +1,22 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
describe('App', () => {
it('should render the catalog', () => {
cy.visit('/');
cy.contains('My Company Service Catalog');
});
});
@@ -0,0 +1,64 @@
{
"name": "embedded-techdocs-app",
"version": "0.0.0",
"private": true,
"bundled": true,
"dependencies": {
"@backstage/catalog-model": "^0.9.5",
"@backstage/cli": "^0.8.0",
"@backstage/config": "^0.1.10",
"@backstage/core-app-api": "^0.1.18",
"@backstage/core-components": "^0.7.1",
"@backstage/core-plugin-api": "^0.1.11",
"@backstage/integration-react": "^0.1.12",
"@backstage/plugin-catalog": "^0.7.2",
"@backstage/plugin-techdocs": "^0.12.3",
"@backstage/test-utils": "^0.1.19",
"@backstage/theme": "^0.2.11",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"history": "^5.0.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^17.2.4"
},
"devDependencies": {
"@backstage/cli": "^089.0",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^13.1.8",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32",
"@types/react-dom": "*",
"cross-env": "^7.0.0",
"cypress": "^7.3.0",
"eslint-plugin-cypress": "^2.10.3",
"start-server-and-test": "^1.10.11"
},
"scripts": {
"start": "backstage-cli app:serve --config ./app-config.yaml --config ./app-config.dev.yaml",
"build": "backstage-cli app:build --config ./app-config.yaml",
"clean": "backstage-cli clean",
"test": "backstage-cli test",
"lint": "backstage-cli lint",
"test:e2e": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:dev",
"test:e2e:ci": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:run",
"cy:dev": "cypress open",
"cy:run": "cypress run"
},
"prettier": "@spotify/prettier-config",
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Backstage is an open platform for building developer portals"
/>
<link rel="apple-touch-icon" href="<%= publicPath %>/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link
rel="manifest"
href="<%= publicPath %>/manifest.json"
crossorigin="use-credentials"
/>
<link rel="icon" href="<%= publicPath %>/favicon.ico" />
<link rel="shortcut icon" href="<%= publicPath %>/favicon.ico" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="<%= publicPath %>/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="<%= publicPath %>/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="<%= publicPath %>/favicon-16x16.png"
/>
<link
rel="mask-icon"
href="<%= publicPath %>/safari-pinned-tab.svg"
color="#5bbad5"
/>
<style>
#root {
min-height: 100%;
}
</style>
<title><%= app.title %></title>
</head>
<body style="margin: 0">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
@@ -0,0 +1,15 @@
{
"short_name": "Backstage",
"name": "Backstage",
"icons": [
{
"src": "favicon.ico",
"sizes": "48x48",
"type": "image/png"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
@@ -0,0 +1,2 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="682.667" height="682.667" preserveAspectRatio="xMidYMid meet" version="1.0" viewBox="0 0 512 512"><metadata>Created by potrace 1.11, written by Peter Selinger 2001-2013</metadata><g fill="#000" stroke="none"><path d="M492 4610 c-4 -3 -8 -882 -7 -1953 l0 -1948 850 2 c898 1 945 3 1118 49 505 134 823 531 829 1037 2 136 -9 212 -44 323 -40 125 -89 218 -163 310 -35 43 -126 128 -169 157 -22 15 -43 30 -46 33 -12 13 -131 70 -188 91 l-64 22 60 28 c171 77 317 224 403 404 64 136 92 266 91 425 -5 424 -245 770 -642 923 -79 30 -105 39 -155 50 -11 3 -38 10 -60 15 -22 6 -60 13 -85 17 -25 3 -58 9 -75 12 -36 8 -1643 11 -1653 3z m1497 -743 c236 -68 352 -254 305 -486 -26 -124 -110 -224 -232 -277 -92 -40 -151 -46 -439 -49 l-283 -3 -1 27 c-1 36 -1 760 0 790 l1 23 298 -5 c226 -4 310 -9 351 -20z m-82 -1538 c98 -3 174 -19 247 -52 169 -78 257 -212 258 -395 0 -116 -36 -221 -100 -293 -64 -72 -192 -135 -314 -155 -23 -3 -181 -7 -350 -8 l-308 -2 -1 26 c-6 210 1 874 9 879 9 5 366 6 559 0z" transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"/><path d="M4160 1789 c-275 -24 -499 -263 -503 -536 -1 -115 21 -212 66 -292 210 -369 697 -402 950 -65 77 103 110 199 111 329 0 50 -6 113 -13 140 -16 58 -62 155 -91 193 -33 43 -122 132 -132 132 -5 0 -26 11 -46 25 -85 56 -219 85 -342 74z" transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"/></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,42 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { renderWithEffects } from '@backstage/test-utils';
import App from './App';
describe('App', () => {
it('should render', async () => {
process.env = {
NODE_ENV: 'test',
APP_CONFIG: [
{
data: {
app: { title: 'Test' },
backend: { baseUrl: 'http://localhost:7000' },
techdocs: {
storageUrl: 'http://localhost:7000/api/techdocs/static/docs',
},
},
context: 'test',
},
] as any,
};
const rendered = await renderWithEffects(<App />);
expect(rendered.baseElement).toBeInTheDocument();
});
});
@@ -0,0 +1,68 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Navigate, Route } from 'react-router';
import { createApp, FlatRoutes } from '@backstage/core-app-api';
import { CatalogEntityPage } from '@backstage/plugin-catalog';
import {
DefaultTechDocsHome,
TechDocsIndexPage,
TechDocsReaderPage,
} from '@backstage/plugin-techdocs';
import { apis } from './apis';
import { Root } from './components/Root';
import { techDocsPage } from './components/TechDocsPage';
import * as plugins from './plugins';
const app = createApp({
apis,
plugins: Object.values(plugins),
});
const AppProvider = app.getProvider();
const AppRouter = app.getRouter();
const routes = (
<FlatRoutes>
<Navigate key="/" to="/docs/default/component/local/" />
{/* we need this route as TechDocs header links relies on it */}
<Route
path="/catalog/:namespace/:kind/:name"
element={<CatalogEntityPage />}
/>
<Route path="/docs" element={<TechDocsIndexPage />}>
<DefaultTechDocsHome />
</Route>
<Route
path="/docs/:namespace/:kind/:name/*"
element={<TechDocsReaderPage />}
>
{techDocsPage}
</Route>
</FlatRoutes>
);
const App = () => (
<AppProvider>
<AppRouter>
<Root>{routes}</Root>
</AppRouter>
</AppProvider>
);
export default App;
+199
View File
@@ -0,0 +1,199 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EntityName } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import {
scmIntegrationsApiRef,
ScmIntegrationsApi,
} from '@backstage/integration-react';
import {
AnyApiFactory,
configApiRef,
createApiFactory,
DiscoveryApi,
discoveryApiRef,
IdentityApi,
identityApiRef,
} from '@backstage/core-plugin-api';
import {
SyncResult,
TechDocsApi,
techdocsApiRef,
TechDocsStorageApi,
techdocsStorageApiRef,
} from '@backstage/plugin-techdocs';
// TODO: Export type from plugin-techdocs and import this here
// import { ParsedEntityId } from '@backstage/plugin-techdocs'
/**
* Note: Override TechDocs API to use local mkdocs server instead of techdocs-backend.
*/
class TechDocsDevStorageApi implements TechDocsStorageApi {
public configApi: Config;
public discoveryApi: DiscoveryApi;
public identityApi: IdentityApi;
constructor({
configApi,
discoveryApi,
identityApi,
}: {
configApi: Config;
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
}) {
this.configApi = configApi;
this.discoveryApi = discoveryApi;
this.identityApi = identityApi;
}
async getApiOrigin() {
return (
this.configApi.getOptionalString('techdocs.requestUrl') ??
(await this.discoveryApi.getBaseUrl('techdocs'))
);
}
async getStorageUrl() {
return (
this.configApi.getOptionalString('techdocs.storageUrl') ??
`${await this.discoveryApi.getBaseUrl('techdocs')}/static/docs`
);
}
async getBuilder() {
return this.configApi.getString('techdocs.builder');
}
async getEntityDocs(_entityId: EntityName, path: string) {
const apiOrigin = await this.getApiOrigin();
// Irrespective of the entity, use mkdocs server to find the file for the path.
const url = `${apiOrigin}/${path}`;
const request = await fetch(
`${url.endsWith('/') ? url : `${url}/`}index.html`,
);
if (request.status === 404) {
throw new Error('Page not found');
}
return request.text();
}
async syncEntityDocs(_: EntityName): Promise<SyncResult> {
// this is just stub of this function as we don't need to check if docs are up to date,
// we always want to retrigger a new build
return 'cached';
}
// Used by transformer to modify the request to assets (CSS, Image) from inside the HTML.
async getBaseUrl(
oldBaseUrl: string,
_entityId: EntityName,
path: string,
): Promise<string> {
const apiOrigin = await this.getApiOrigin();
return new URL(oldBaseUrl, `${apiOrigin}/${path}`).toString();
}
}
class TechDocsDevApi implements TechDocsApi {
public configApi: Config;
public discoveryApi: DiscoveryApi;
public identityApi: IdentityApi;
constructor({
configApi,
discoveryApi,
identityApi,
}: {
configApi: Config;
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
}) {
this.configApi = configApi;
this.discoveryApi = discoveryApi;
this.identityApi = identityApi;
}
async getApiOrigin() {
return (
this.configApi.getOptionalString('techdocs.requestUrl') ??
(await this.discoveryApi.getBaseUrl('techdocs'))
);
}
async getEntityMetadata(_entityId: any) {
return {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'local',
},
spec: {
owner: 'test',
lifecycle: 'experimental',
},
};
}
async getTechDocsMetadata(_entityId: EntityName) {
return {
site_name: 'Live preview environment',
site_description: '',
};
}
}
export const apis: AnyApiFactory[] = [
createApiFactory({
api: techdocsStorageApiRef,
deps: {
configApi: configApiRef,
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
},
factory: ({ configApi, discoveryApi, identityApi }) =>
new TechDocsDevStorageApi({
configApi,
discoveryApi,
identityApi,
}),
}),
createApiFactory({
api: techdocsApiRef,
deps: {
configApi: configApiRef,
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
},
factory: ({ configApi, discoveryApi, identityApi }) =>
new TechDocsDevApi({
configApi,
discoveryApi,
identityApi,
}),
}),
createApiFactory({
api: scmIntegrationsApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
}),
];
File diff suppressed because one or more lines are too long
@@ -0,0 +1,47 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles({
svg: {
width: 'auto',
height: 28,
},
path: {
fill: '#7df3e1',
},
});
const LogoIcon = () => {
const classes = useStyles();
return (
<svg
className={classes.svg}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 337.46 428.5"
>
<path
className={classes.path}
d="M303,166.05a80.69,80.69,0,0,0,13.45-10.37c.79-.77,1.55-1.53,2.3-2.3a83.12,83.12,0,0,0,7.93-9.38A63.69,63.69,0,0,0,333,133.23a48.58,48.58,0,0,0,4.35-16.4c1.49-19.39-10-38.67-35.62-54.22L198.56,0,78.3,115.23,0,190.25l108.6,65.91a111.59,111.59,0,0,0,57.76,16.41c24.92,0,48.8-8.8,66.42-25.69,19.16-18.36,25.52-42.12,13.7-61.87a49.22,49.22,0,0,0-6.8-8.87A89.17,89.17,0,0,0,259,178.29h.15a85.08,85.08,0,0,0,31-5.79A80.88,80.88,0,0,0,303,166.05ZM202.45,225.86c-19.32,18.51-50.4,21.23-75.7,5.9L51.61,186.15l67.45-64.64,76.41,46.38C223,184.58,221.49,207.61,202.45,225.86Zm8.93-82.22-70.65-42.89L205.14,39,274.51,81.1c25.94,15.72,29.31,37,10.55,55A60.69,60.69,0,0,1,211.38,143.64Zm29.86,190c-19.57,18.75-46.17,29.09-74.88,29.09a123.73,123.73,0,0,1-64.1-18.2L0,282.52v24.67L108.6,373.1a111.6,111.6,0,0,0,57.76,16.42c24.92,0,48.8-8.81,66.42-25.69,12.88-12.34,20-27.13,19.68-41.49v-1.79A87.27,87.27,0,0,1,241.24,333.68Zm0-39c-19.57,18.75-46.17,29.08-74.88,29.08a123.81,123.81,0,0,1-64.1-18.19L0,243.53v24.68l108.6,65.91a111.6,111.6,0,0,0,57.76,16.42c24.92,0,48.8-8.81,66.42-25.69,12.88-12.34,20-27.13,19.68-41.5v-1.78A87.27,87.27,0,0,1,241.24,294.7Zm0-39c-19.57,18.76-46.17,29.09-74.88,29.09a123.81,123.81,0,0,1-64.1-18.19L0,204.55v24.68l108.6,65.91a111.59,111.59,0,0,0,57.76,16.41c24.92,0,48.8-8.8,66.42-25.68,12.88-12.35,20-27.13,19.68-41.5v-1.82A86.09,86.09,0,0,1,241.24,255.71Zm83.7,25.74a94.15,94.15,0,0,1-60.2,25.86h0V334a81.6,81.6,0,0,0,51.74-22.37c14-13.38,21.14-28.11,21-42.64v-2.19A94.92,94.92,0,0,1,324.94,281.45Zm-83.7,91.21c-19.57,18.76-46.17,29.09-74.88,29.09a123.73,123.73,0,0,1-64.1-18.2L0,321.5v24.68l108.6,65.9a111.6,111.6,0,0,0,57.76,16.42c24.92,0,48.8-8.8,66.42-25.69,12.88-12.34,20-27.13,19.68-41.49v-1.79A86.29,86.29,0,0,1,241.24,372.66ZM327,162.45c-.68.69-1.35,1.38-2.05,2.06a94.37,94.37,0,0,1-10.64,8.65,91.35,91.35,0,0,1-11.6,7,94.53,94.53,0,0,1-26.24,8.71,97.69,97.69,0,0,1-14.16,1.57c.5,1.61.9,3.25,1.25,4.9a53.27,53.27,0,0,1,1.14,12V217h.05a84.41,84.41,0,0,0,25.35-5.55,81,81,0,0,0,26.39-16.82c.8-.77,1.5-1.56,2.26-2.34a82.08,82.08,0,0,0,7.93-9.38A63.76,63.76,0,0,0,333,172.17a48.55,48.55,0,0,0,4.32-16.45c.09-1.23.2-2.47.19-3.7V150q-1.08,1.54-2.25,3.09A96.73,96.73,0,0,1,327,162.45Zm0,77.92c-.69.7-1.31,1.41-2,2.1a94.2,94.2,0,0,1-60.2,25.86h0l0,26.67h0a81.6,81.6,0,0,0,51.74-22.37A73.51,73.51,0,0,0,333,250.13a48.56,48.56,0,0,0,4.32-16.44c.09-1.24.2-2.47.19-3.71v-2.19c-.74,1.07-1.46,2.15-2.27,3.21A95.68,95.68,0,0,1,327,240.37Zm0-39c-.69.7-1.31,1.41-2,2.1a93.18,93.18,0,0,1-10.63,8.65,91.63,91.63,0,0,1-11.63,7,95.47,95.47,0,0,1-37.94,10.18h0V256h0a81.65,81.65,0,0,0,51.74-22.37c.8-.77,1.5-1.56,2.26-2.34a82.08,82.08,0,0,0,7.93-9.38A63.76,63.76,0,0,0,333,211.15a48.56,48.56,0,0,0,4.32-16.44c.09-1.24.2-2.48.19-3.71v-2.2c-.74,1.08-1.46,2.16-2.27,3.22A95.68,95.68,0,0,1,327,201.39Z"
/>
</svg>
);
};
export default LogoIcon;
@@ -0,0 +1,80 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useContext, PropsWithChildren } from 'react';
import { Link, makeStyles } from '@material-ui/core';
import LibraryBooks from '@material-ui/icons/LibraryBooks';
import LogoFull from './LogoFull';
import LogoIcon from './LogoIcon';
import {
Sidebar,
SidebarPage,
sidebarConfig,
SidebarContext,
SidebarItem,
SidebarDivider,
} from '@backstage/core-components';
import { NavLink } from 'react-router-dom';
const useSidebarLogoStyles = makeStyles({
root: {
width: sidebarConfig.drawerWidthClosed,
height: 3 * sidebarConfig.logoHeight,
display: 'flex',
flexFlow: 'row nowrap',
alignItems: 'center',
marginBottom: -14,
},
link: {
width: sidebarConfig.drawerWidthClosed,
marginLeft: 24,
},
});
const SidebarLogo = () => {
const classes = useSidebarLogoStyles();
const { isOpen } = useContext(SidebarContext);
return (
<div className={classes.root}>
<Link
component={NavLink}
to="/docs/default/component/local/"
underline="none"
className={classes.link}
>
{isOpen ? <LogoFull /> : <LogoIcon />}
</Link>
</div>
);
};
export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarPage>
<Sidebar>
<SidebarLogo />
<SidebarDivider />
{/* Global nav, not org-specific */}
<SidebarItem
icon={LibraryBooks}
to="docs/default/component/local/"
text="Docs Preview"
/>
{/* End global nav */}
</Sidebar>
{children}
</SidebarPage>
);
@@ -0,0 +1,17 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { Root } from './Root';
@@ -0,0 +1,54 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Content } from '@backstage/core-components';
import {
Reader,
TechDocsPage,
TechDocsPageHeader,
} from '@backstage/plugin-techdocs';
const DefaultTechDocsPage = () => {
const techDocsMetadata = {
site_name: 'Live preview environment',
site_description: '',
};
return (
<TechDocsPage>
{({ entityRef, onReady }) => (
<>
<TechDocsPageHeader
techDocsMetadata={techDocsMetadata}
entityRef={entityRef}
/>
<Content data-testid="techdocs-content">
<Reader
onReady={onReady}
entityRef={entityRef}
withSearch={false}
/>
</Content>
</>
)}
</TechDocsPage>
);
};
export const techDocsPage = <DefaultTechDocsPage />;
@@ -0,0 +1,16 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './TechDocsPage';
@@ -0,0 +1,22 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '@backstage/cli/asset-types';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
@@ -0,0 +1,17 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { plugin as TechDocsPlugin } from '@backstage/plugin-techdocs';
@@ -0,0 +1,17 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '@testing-library/jest-dom';
+11
View File
@@ -0,0 +1,11 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
overrides: [
{
files: ['**/*.ts?(x)'],
rules: {
'no-restricted-imports': 0,
},
},
],
};
+70
View File
@@ -0,0 +1,70 @@
# @techdocs/cli
## 0.8.2
### Patch Changes
- 8fc7384: Allow to execute techdocs-cli serve using docker techdocs-container on Windows
## 0.8.1
### Patch Changes
- 0187424: Separate build and publish release steps
## 0.8.0
### Minor Changes
- c6f437a: OpenStack Swift configuration changed due to OSS SDK Client change in @backstage/techdocs-common, it was a breaking change.
PR Reference: https://github.com/backstage/backstage/pull/6839
### Patch Changes
- 05f0409: Merge Jobs for Release Pull Requests and Package Publishes
## 0.7.0
### Minor Changes
- 9d1f8d8: The `techdocs-cli publish` command will now publish TechDocs content to remote
storage using the lowercase'd entity triplet as the storage path. This is in
line with the beta release of the TechDocs plugin (`v0.11.0`).
If you have been running `techdocs-cli` prior to this version, you will need to
follow this [migration guide](https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-alpha-to-beta).
## 0.6.2
### Patch Changes
- f1bcf1a: Changelog (from v0.6.1 to v0.6.2)
#### :bug: Bug Fix
- `techdocs-cli`
- [#105](https://github.com/backstage/techdocs-cli/pull/105) Add azureAccountKey parameter back to the publish command ([@emmaindal](https://github.com/emmaindal))
#### :house: Internal
- `embedded-techdocs-app`
- [#122](https://github.com/backstage/techdocs-cli/pull/122) chore(deps-dev): bump @types/node from 12.20.20 to 16.7.1 in /packages/embedded-techdocs-app ([@dependabot[bot]](https://github.com/apps/dependabot))
- [#120](https://github.com/backstage/techdocs-cli/pull/120) chore(deps-dev): bump @types/react-dom from 16.9.14 to 17.0.9 in /packages/embedded-techdocs-app ([@dependabot[bot]](https://github.com/apps/dependabot))
- [#119](https://github.com/backstage/techdocs-cli/pull/119) chore(deps-dev): bump @testing-library/user-event from 12.8.3 to 13.2.1 in /packages/embedded-techdocs-app ([@dependabot[bot]](https://github.com/apps/dependabot))
- [#118](https://github.com/backstage/techdocs-cli/pull/118) chore(deps-dev): bump @testing-library/react from 10.4.9 to 12.0.0 ([@dependabot[bot]](https://github.com/apps/dependabot))
- Other
- [#117](https://github.com/backstage/techdocs-cli/pull/117) chore(deps): bump @backstage/plugin-catalog from 0.6.11 to 0.6.12 ([@dependabot[bot]](https://github.com/apps/dependabot))
- [#124](https://github.com/backstage/techdocs-cli/pull/124) Update release process docs ([@emmaindal](https://github.com/emmaindal))
- [#116](https://github.com/backstage/techdocs-cli/pull/116) ignore dependabot branches for project board workflow ([@emmaindal](https://github.com/emmaindal))
- [#106](https://github.com/backstage/techdocs-cli/pull/106) Configure dependabot for all packages ([@emmaindal](https://github.com/emmaindal))
- [#102](https://github.com/backstage/techdocs-cli/pull/102) readme: add information about running techdocs-common locally ([@vcapretz](https://github.com/vcapretz))
- [#103](https://github.com/backstage/techdocs-cli/pull/103) Introduce changesets and improve the publish workflow ([@minkimcello](https://github.com/minkimcello))
- [#101](https://github.com/backstage/techdocs-cli/pull/101) update yarn lockfile to get rid of old version of node-forge ([@emmaindal](https://github.com/emmaindal))
#### Committers: 3
Thank you for contributing ❤️
- `Emma Indal` ([@emmaindal](https://github.com/emmaindal))
- `Min Kim` ([@minkimcello](https://github.com/minkimcello))
- `Vitor Capretz` ([@vcapretz](https://github.com/vcapretz))
+68
View File
@@ -0,0 +1,68 @@
# techdocs-cli
[![NPM Version badge](https://img.shields.io/npm/v/@techdocs/cli)](https://www.npmjs.com/package/@techdocs/cli)
## Usage
See [techdocs-cli usage docs](https://backstage.io/docs/features/techdocs/cli).
## Development
NOTE: When we build `techdocs-cli` it copies the output `embedded-techdocs-app`
bundle into the `packages/techdocs-cli/dist` which is then published with the
`@techdocs/cli` npm package.
### Running
```sh
# From the root of this repository run
# NOTE: This will build the embedded-techdocs-app and copy the output into the cli dist directory
yarn build --scope @techdocs/cli
# Now execute the binary
packages/techdocs-cli/bin/techdocs-cli
# ... or as a shell alias in ~/.zshrc or ~/.zprofile or ~/.bashrc or similar
export PATH=/path/to/backstage/packages/techdocs-cli/bin:$PATH
```
If you want to test live test changes to the `packages/embedded-techdocs-app`
you can serve the app and run the CLI using the following commands:
```sh
# Open a shell to the embedded-techdocs-app directory
cd packages/embedded-techdocs-app
# Run the embedded-techdocs-app using dev mode
yarn start
# In another shell use the techdocs-cli from the root of this repo
yarn techdocs-cli:dev [...options]
```
### Testing
Running unit tests requires mkdocs to be installed locally:
```sh
pip install mkdocs
pip install mkdocs-techdocs-core
```
Then run `yarn test`.
### Use an example docs project
We have created an [example documentation project](https://github.com/backstage/techdocs-container/tree/main/mock-docs) and it's shipped with [techdocs-container](https://github.com/backstage/techdocs-container) repository, for the purpose of local development. But you are free to create your own local test site. All it takes is a `docs/index.md` and `mkdocs.yml` in a directory.
```sh
git clone https://github.com/backstage/techdocs-container.git
cd techdocs-container/mock-docs
# To get a view of your docs in Backstage, use:
techdocs-cli serve
# To view the raw mkdocs site (without Backstage), use:
techdocs-cli serve:mkdocs
```
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env node
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const path = require('path');
// Figure out whether we're running inside the backstage repo or as an installed dependency
/* eslint-disable-next-line no-restricted-syntax */
const isLocal = require('fs').existsSync(path.resolve(__dirname, '../src'));
if (!isLocal) {
require('..');
} else {
require('ts-node').register({
transpileOnly: true,
project: path.resolve(__dirname, '../../../tsconfig.json'),
compilerOptions: {
module: 'CommonJS',
},
});
require('../src');
}
+70
View File
@@ -0,0 +1,70 @@
{
"name": "@techdocs/cli",
"description": "Utility CLI for managing TechDocs sites in Backstage.",
"version": "0.8.4",
"private": false,
"publishConfig": {
"access": "public"
},
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "packages/techdocs-cli"
},
"keywords": [
"backstage",
"techdocs"
],
"license": "Apache-2.0",
"main": "dist/index.cjs.js",
"types": "",
"scripts": {
"start": "nodemon --",
"build": "./scripts/build.sh",
"clean": "backstage-cli clean",
"lint": "backstage-cli lint",
"test": "backstage-cli test --testPathIgnorePatterns src/e2e.test.ts",
"test:e2e": "backstage-cli test --testPathPattern src/e2e.test.ts --runInBand"
},
"bin": {
"techdocs-cli": "bin/techdocs-cli"
},
"devDependencies": {
"@backstage/cli": "^0.8.0",
"@types/commander": "^2.12.2",
"@types/fs-extra": "^9.0.6",
"@types/http-proxy": "^1.17.4",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32",
"@types/react-dev-utils": "^9.0.4",
"@types/serve-handler": "^6.1.0",
"@types/webpack-env": "^1.15.3",
"embedded-techdocs-app": "0.0.0",
"nodemon": "^2.0.2",
"ts-node": "^10.0.0"
},
"files": [
"bin",
"dist"
],
"nodemonConfig": {
"watch": "./src",
"exec": "bin/techdocs-cli",
"ext": "ts"
},
"dependencies": {
"@backstage/backend-common": "^0.9.7",
"@backstage/catalog-model": "^0.9.5",
"@backstage/config": "^0.1.10",
"@backstage/techdocs-common": "^0.10.4",
"@types/dockerode": "^3.2.1",
"commander": "^6.1.0",
"dockerode": "^3.2.1",
"fs-extra": "^9.0.1",
"http-proxy": "^1.18.1",
"react-dev-utils": "^11.0.4",
"serve-handler": "^6.1.3",
"winston": "^3.2.1"
}
}
+37
View File
@@ -0,0 +1,37 @@
#!/bin/bash
# Copyright 2020 The Backstage Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -e
# Build the TechDocs CLI
npx backstage-cli -- build --outputs cjs
# Make sure to do `yarn run build` in packages/embedded-techdocs before building here.
EMBEDDED_TECHDOCS_APP_PATH=../embedded-techdocs-app
TECHDOCS_PREVIEW_SOURCE=$EMBEDDED_TECHDOCS_APP_PATH/dist
TECHDOCS_PREVIEW_DEST=dist/techdocs-preview-bundle
# Build the embedded-techdocs-app
pushd $EMBEDDED_TECHDOCS_APP_PATH >/dev/null
yarn build
popd >/dev/null
cp -r $TECHDOCS_PREVIEW_SOURCE $TECHDOCS_PREVIEW_DEST
# Write to console
echo "[techdocs-cli]: Built the dist/ folder"
echo "[techdocs-cli]: Imported @backstage/plugin-techdocs dist/ folder into techdocs-preview-bundle/"
@@ -0,0 +1,99 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { resolve } from 'path';
import { Command } from 'commander';
import fs from 'fs-extra';
import Docker from 'dockerode';
import {
TechdocsGenerator,
ParsedLocationAnnotation,
} from '@backstage/techdocs-common';
import { DockerContainerRunner } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import {
convertTechDocsRefToLocationAnnotation,
createLogger,
} from '../../lib/utility';
import { stdout } from 'process';
export default async function generate(cmd: Command) {
// Use techdocs-common package to generate docs. Keep consistency between Backstage and CI generating docs.
// Docs can be prepared using actions/checkout or git clone, or similar paradigms on CI. The TechDocs CI workflow
// will run on the CI pipeline containing the documentation files.
const logger = createLogger({ verbose: cmd.verbose });
const sourceDir = resolve(cmd.sourceDir);
const outputDir = resolve(cmd.outputDir);
const dockerImage = cmd.dockerImage;
const pullImage = cmd.pull;
logger.info(`Using source dir ${sourceDir}`);
logger.info(`Will output generated files in ${outputDir}`);
logger.verbose('Creating output directory if it does not exist.');
await fs.ensureDir(outputDir);
const config = new ConfigReader({
techdocs: {
generator: {
runIn: cmd.docker ? 'docker' : 'local',
dockerImage,
pullImage,
},
},
});
// Docker client (conditionally) used by the generators, based on techdocs.generators config.
const dockerClient = new Docker();
const containerRunner = new DockerContainerRunner({ dockerClient });
let parsedLocationAnnotation = {} as ParsedLocationAnnotation;
if (cmd.techdocsRef) {
try {
parsedLocationAnnotation = convertTechDocsRefToLocationAnnotation(
cmd.techdocsRef,
);
} catch (err) {
logger.error(err.message);
}
}
// Generate docs using @backstage/techdocs-common
const techdocsGenerator = await TechdocsGenerator.fromConfig(config, {
logger,
containerRunner,
});
logger.info('Generating documentation...');
await techdocsGenerator.run({
inputDir: sourceDir,
outputDir,
...(cmd.techdocsRef
? {
parsedLocationAnnotation,
}
: {}),
logger,
etag: cmd.etag,
...(process.env.LOG_LEVEL === 'debug' ? { logStream: stdout } : {}),
});
logger.info('Done!');
}
+238
View File
@@ -0,0 +1,238 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CommanderStatic } from 'commander';
import { TechdocsGenerator } from '@backstage/techdocs-common';
const defaultDockerImage = TechdocsGenerator.defaultDockerImage;
export function registerCommands(program: CommanderStatic) {
program
.command('generate')
.description('Generate TechDocs documentation site using MkDocs.')
.option(
'--source-dir <PATH>',
'Source directory containing mkdocs.yml and docs/ directory.',
'.',
)
.option(
'--output-dir <PATH>',
'Output directory containing generated TechDocs site.',
'./site/',
)
.option(
'--docker-image <DOCKER_IMAGE>',
'The mkdocs docker container to use',
defaultDockerImage,
)
.option('--no-pull', 'Do not pull the latest docker image', false)
.option(
'--no-docker',
'Do not use Docker, use MkDocs executable and plugins in current user environment.',
)
.option(
'--techdocs-ref <HOST_TYPE:URL>',
'The repository hosting documentation source files e.g. github:https://ghe.mycompany.net.com/org/repo.' +
'\nThis value is same as the backstage.io/techdocs-ref annotation of the corresponding Backstage entity.' +
'\nIt is completely fine to skip this as it is only being used to set repo_url in mkdocs.yml if not found.\n',
)
.option(
'--etag <ETAG>',
'A unique identifier for the prepared tree e.g. commit SHA. If provided it will be stored in techdocs_metadata.json.',
)
.option('-v --verbose', 'Enable verbose output.', false)
.alias('build')
.action(lazy(() => import('./generate/generate').then(m => m.default)));
program
.command('migrate')
.description(
'Migrate objects with case-sensitive entity triplets to lower-case versions.',
)
.requiredOption(
'--publisher-type <TYPE>',
'(Required always) awsS3 | googleGcs | azureBlobStorage | openStackSwift - same as techdocs.publisher.type in Backstage app-config.yaml',
)
.requiredOption(
'--storage-name <BUCKET/CONTAINER NAME>',
'(Required always) In case of AWS/GCS, use the bucket name. In case of Azure, use container name. Same as techdocs.publisher.[TYPE].bucketName',
)
.option(
'--azureAccountName <AZURE ACCOUNT NAME>',
'(Required for Azure) specify when --publisher-type azureBlobStorage',
)
.option(
'--azureAccountKey <AZURE ACCOUNT KEY>',
'Azure Storage Account key to use for authentication. If not specified, you must set AZURE_TENANT_ID, AZURE_CLIENT_ID & AZURE_CLIENT_SECRET as environment variables.',
)
.option(
'--awsRoleArn <AWS ROLE ARN>',
'Optional AWS ARN of role to be assumed.',
)
.option(
'--awsEndpoint <AWS ENDPOINT>',
'Optional AWS endpoint to send requests to.',
)
.option(
'--awsS3ForcePathStyle',
'Optional AWS S3 option to force path style.',
)
.option(
'--osCredentialId <OPENSTACK SWIFT APPLICATION CREDENTIAL ID>',
'(Required for OpenStack) specify when --publisher-type openStackSwift',
)
.option(
'--osSecret <OPENSTACK SWIFT APPLICATION CREDENTIAL SECRET>',
'(Required for OpenStack) specify when --publisher-type openStackSwift',
)
.option(
'--osAuthUrl <OPENSTACK SWIFT AUTHURL>',
'(Required for OpenStack) specify when --publisher-type openStackSwift',
)
.option(
'--osSwiftUrl <OPENSTACK SWIFT SWIFTURL>',
'(Required for OpenStack) specify when --publisher-type openStackSwift',
)
.option(
'--removeOriginal',
'Optional Files are copied by default. If flag is set, files are renamed/moved instead.',
false,
)
.option(
'--concurrency <MAX CONCURRENT REQS>',
'Optional Controls the number of API requests allowed to be performed simultaneously.',
'25',
)
.option('-v --verbose', 'Enable verbose output.', false)
.action(lazy(() => import('./migrate/migrate').then(m => m.default)));
program
.command('publish')
.description(
'Publish generated TechDocs site to an external storage AWS S3, Google GCS, etc.',
)
.requiredOption(
'--publisher-type <TYPE>',
'(Required always) awsS3 | googleGcs | azureBlobStorage | openStackSwift - same as techdocs.publisher.type in Backstage app-config.yaml',
)
.requiredOption(
'--storage-name <BUCKET/CONTAINER NAME>',
'(Required always) In case of AWS/GCS, use the bucket name. In case of Azure, use container name. Same as techdocs.publisher.[TYPE].bucketName',
)
.requiredOption(
'--entity <NAMESPACE/KIND/NAME>',
'(Required always) Entity uid separated by / in namespace/kind/name order (case-sensitive). Example: default/Component/myEntity ',
)
.option(
'--legacyUseCaseSensitiveTripletPaths',
'Publishes objects with cased entity triplet prefix when set (e.g. namespace/Kind/name). Only use if your TechDocs backend is configured the same way.',
false,
)
.option(
'--azureAccountName <AZURE ACCOUNT NAME>',
'(Required for Azure) specify when --publisher-type azureBlobStorage',
)
.option(
'--azureAccountKey <AZURE ACCOUNT KEY>',
'Azure Storage Account key to use for authentication. If not specified, you must set AZURE_TENANT_ID, AZURE_CLIENT_ID & AZURE_CLIENT_SECRET as environment variables.',
)
.option(
'--awsRoleArn <AWS ROLE ARN>',
'Optional AWS ARN of role to be assumed.',
)
.option(
'--awsEndpoint <AWS ENDPOINT>',
'Optional AWS endpoint to send requests to.',
)
.option(
'--awsS3ForcePathStyle',
'Optional AWS S3 option to force path style.',
)
.option(
'--osCredentialId <OPENSTACK SWIFT APPLICATION CREDENTIAL ID>',
'(Required for OpenStack) specify when --publisher-type openStackSwift',
)
.option(
'--osSecret <OPENSTACK SWIFT APPLICATION CREDENTIAL SECRET>',
'(Required for OpenStack) specify when --publisher-type openStackSwift',
)
.option(
'--osAuthUrl <OPENSTACK SWIFT AUTHURL>',
'(Required for OpenStack) specify when --publisher-type openStackSwift',
)
.option(
'--osSwiftUrl <OPENSTACK SWIFT SWIFTURL>',
'(Required for OpenStack) specify when --publisher-type openStackSwift',
)
.option(
'--directory <PATH>',
'Path of the directory containing generated files to publish',
'./site/',
)
.action(lazy(() => import('./publish/publish').then(m => m.default)));
program
.command('serve:mkdocs')
.description('Serve a documentation project locally using MkDocs serve.')
.option(
'-i, --docker-image <DOCKER_IMAGE>',
'The mkdocs docker container to use',
defaultDockerImage,
)
.option(
'--no-docker',
'Do not use Docker, run `mkdocs serve` in current user environment.',
)
.option('-p, --port <PORT>', 'Port to serve documentation locally', '8000')
.option('-v --verbose', 'Enable verbose output.', false)
.action(lazy(() => import('./serve/mkdocs').then(m => m.default)));
program
.command('serve')
.description(
'Serve a documentation project locally in a Backstage app-like environment',
)
.option(
'-i, --docker-image <DOCKER_IMAGE>',
'The mkdocs docker container to use',
defaultDockerImage,
)
.option(
'--no-docker',
'Do not use Docker, use MkDocs executable in current user environment.',
)
.option('--mkdocs-port <PORT>', 'Port for MkDocs server to use', '8000')
.option('-v --verbose', 'Enable verbose output.', false)
.action(lazy(() => import('./serve/serve').then(m => m.default)));
}
// Wraps an action function so that it always exits and handles errors
// Humbly taken from backstage-cli's registerCommands
function lazy(
getActionFunc: () => Promise<(...args: any[]) => Promise<void>>,
): (...args: any[]) => Promise<never> {
return async (...args: any[]) => {
try {
const actionFunc = await getActionFunc();
await actionFunc(...args);
process.exit(0);
} catch (error) {
// eslint-disable-next-line no-console
console.error(error.message);
process.exit(1);
}
};
}
@@ -0,0 +1,55 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { SingleHostDiscovery } from '@backstage/backend-common';
import { Publisher } from '@backstage/techdocs-common';
import { Command } from 'commander';
import { createLogger } from '../../lib/utility';
import { PublisherConfig } from '../../lib/PublisherConfig';
export default async function migrate(cmd: Command) {
const logger = createLogger({ verbose: cmd.verbose });
const config = PublisherConfig.getValidConfig(cmd);
const discovery = SingleHostDiscovery.fromConfig(config);
const publisher = await Publisher.fromConfig(config, { logger, discovery });
if (!publisher.migrateDocsCase) {
throw new Error(`Migration not implemented for ${cmd.publisherType}`);
}
// Check that the publisher's underlying storage is ready and available.
const { isAvailable } = await publisher.getReadiness();
if (!isAvailable) {
// Error messages printed in getReadiness() call. This ensures exit code 1.
throw new Error('');
}
// Validate and parse migration arguments.
const removeOriginal = cmd.removeOriginal;
const numericConcurrency = parseInt(cmd.concurrency, 10);
if (!Number.isInteger(numericConcurrency) || numericConcurrency <= 0) {
throw new Error(
`Concurrency must be a number greater than 1. ${cmd.concurrency} provided.`,
);
}
await publisher.migrateDocsCase({
concurrency: numericConcurrency,
removeOriginal,
});
}
@@ -0,0 +1,52 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { resolve } from 'path';
import { Command } from 'commander';
import { createLogger } from '../../lib/utility';
import { SingleHostDiscovery } from '@backstage/backend-common';
import { Publisher } from '@backstage/techdocs-common';
import { Entity } from '@backstage/catalog-model';
import { PublisherConfig } from '../../lib/PublisherConfig';
export default async function publish(cmd: Command): Promise<any> {
const logger = createLogger({ verbose: cmd.verbose });
const config = PublisherConfig.getValidConfig(cmd);
const discovery = SingleHostDiscovery.fromConfig(config);
const publisher = await Publisher.fromConfig(config, { logger, discovery });
// Check that the publisher's underlying storage is ready and available.
const { isAvailable } = await publisher.getReadiness();
if (!isAvailable) {
// Error messages printed in getReadiness() call. This ensures exit code 1.
return Promise.reject(new Error(''));
}
const [namespace, kind, name] = cmd.entity.split('/');
const entity = {
kind,
metadata: {
namespace,
name,
},
} as Entity;
const directory = resolve(cmd.directory);
await publisher.publish({ entity, directory });
return true;
}
@@ -0,0 +1,71 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Command } from 'commander';
import openBrowser from 'react-dev-utils/openBrowser';
import { createLogger } from '../../lib/utility';
import { runMkdocsServer } from '../../lib/mkdocsServer';
import { LogFunc, waitForSignal } from '../../lib/run';
export default async function serveMkdocs(cmd: Command) {
const logger = createLogger({ verbose: cmd.verbose });
const dockerAddr = `http://0.0.0.0:${cmd.port}`;
const localAddr = `http://127.0.0.1:${cmd.port}`;
const expectedDevAddr = cmd.docker ? dockerAddr : localAddr;
// We want to open browser only once based on a log.
let boolOpenBrowserTriggered = false;
const logFunc: LogFunc = data => {
// Sometimes the lines contain an unnecessary extra new line in between
const logLines = data.toString().split('\n');
const logPrefix = cmd.docker ? '[docker/mkdocs]' : '[mkdocs]';
logLines.forEach(line => {
if (line === '') {
return;
}
// Logs from container is verbose.
logger.verbose(`${logPrefix} ${line}`);
// When the server has started, open a new browser tab for the user.
if (
!boolOpenBrowserTriggered &&
line.includes(`Serving on ${expectedDevAddr}`)
) {
// Always open the local address, since 0.0.0.0 belongs to docker
logger.info(`\nStarting mkdocs server on ${localAddr}\n`);
openBrowser(localAddr);
boolOpenBrowserTriggered = true;
}
});
};
// mkdocs writes all of its logs to stderr by default, and not stdout.
// https://github.com/mkdocs/mkdocs/issues/879#issuecomment-203536006
// Had me questioning this whole implementation for half an hour.
// Commander stores --no-docker in cmd.docker variable
const childProcess = await runMkdocsServer({
port: cmd.port,
dockerImage: cmd.dockerImage,
useDocker: cmd.docker,
stdoutLogFunc: logFunc,
stderrLogFunc: logFunc,
});
// Keep waiting for user to cancel the process
await waitForSignal([childProcess]);
}
@@ -0,0 +1,126 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Command } from 'commander';
import path from 'path';
import openBrowser from 'react-dev-utils/openBrowser';
import HTTPServer from '../../lib/httpServer';
import { runMkdocsServer } from '../../lib/mkdocsServer';
import { LogFunc, waitForSignal } from '../../lib/run';
import { createLogger } from '../../lib/utility';
export default async function serve(cmd: Command) {
const logger = createLogger({ verbose: cmd.verbose });
// Determine if we want to run in local dev mode or not
// This will run the backstage http server on a different port and only used
// for proxying mkdocs to the backstage app running locally (e.g. with webpack-dev-server)
const isDevMode = Object.keys(process.env).includes('TECHDOCS_CLI_DEV_MODE')
? true
: false;
// TODO: Backstage app port should also be configurable as a CLI option. However, since we bundle
// a backstage app, we define app.baseUrl in the app-config.yaml.
// Hence, it is complicated to make this configurable.
const backstagePort = 3000;
const backstageBackendPort = 7000;
const mkdocsDockerAddr = `http://0.0.0.0:${cmd.mkdocsPort}`;
const mkdocsLocalAddr = `http://127.0.0.1:${cmd.mkdocsPort}`;
const mkdocsExpectedDevAddr = cmd.docker ? mkdocsDockerAddr : mkdocsLocalAddr;
let mkdocsServerHasStarted = false;
const mkdocsLogFunc: LogFunc = data => {
// Sometimes the lines contain an unnecessary extra new line
const logLines = data.toString().split('\n');
const logPrefix = cmd.docker ? '[docker/mkdocs]' : '[mkdocs]';
logLines.forEach(line => {
if (line === '') {
return;
}
logger.verbose(`${logPrefix} ${line}`);
// When the server has started, open a new browser tab for the user.
if (
!mkdocsServerHasStarted &&
line.includes(`Serving on ${mkdocsExpectedDevAddr}`)
) {
mkdocsServerHasStarted = true;
}
});
};
// mkdocs writes all of its logs to stderr by default, and not stdout.
// https://github.com/mkdocs/mkdocs/issues/879#issuecomment-203536006
// Had me questioning this whole implementation for half an hour.
logger.info('Starting mkdocs server.');
const mkdocsChildProcess = await runMkdocsServer({
port: cmd.mkdocsPort,
dockerImage: cmd.dockerImage,
useDocker: cmd.docker,
stdoutLogFunc: mkdocsLogFunc,
stderrLogFunc: mkdocsLogFunc,
});
// Wait until mkdocs server has started so that Backstage starts with docs loaded
// Takes 1-5 seconds
for (let attempt = 0; attempt < 10; attempt++) {
await new Promise(r => setTimeout(r, 1000));
if (mkdocsServerHasStarted) {
break;
}
logger.info('Waiting for mkdocs server to start...');
}
if (!mkdocsServerHasStarted) {
logger.error(
'mkdocs server did not start. Exiting. Try re-running command with -v option for more details.',
);
}
// Run the embedded-techdocs Backstage app
const techdocsPreviewBundlePath = path.join(
path.dirname(require.resolve('@techdocs/cli/package.json')),
'dist',
'techdocs-preview-bundle',
);
const httpServer = new HTTPServer(
techdocsPreviewBundlePath,
isDevMode ? backstageBackendPort : backstagePort,
cmd.mkdocsPort,
cmd.verbose,
);
httpServer
.serve()
.catch(err => {
logger.error(err);
mkdocsChildProcess.kill();
process.exit(1);
})
.then(() => {
// The last three things default/component/local/ don't matter. They can be anything.
openBrowser(
`http://localhost:${backstagePort}/docs/default/component/local/`,
);
logger.info(
`Serving docs in Backstage at http://localhost:${backstagePort}/docs/default/component/local/\nOpening browser.`,
);
});
await waitForSignal([mkdocsChildProcess]);
}
+131
View File
@@ -0,0 +1,131 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { spawn } from 'child_process';
import path from 'path';
const PROJECT_ROOT_DIR = path.resolve(__dirname, '..');
const FIXTURE_DIR = path.resolve(PROJECT_ROOT_DIR, 'src/fixture');
describe('end-to-end', () => {
it('shows help text', async () => {
jest.setTimeout(10000);
const proc = await executeTechDocsCliCommand(['--help']);
expect(proc.combinedStdOutErr).toContain('Usage: techdocs-cli [options]');
expect(proc.exit).toEqual(0);
});
it('can generate', async () => {
jest.setTimeout(10000);
const proc = await executeTechDocsCliCommand(['generate', '--no-docker'], {
cwd: FIXTURE_DIR,
killAfter: 8000,
});
expect(proc.combinedStdOutErr).toContain('Successfully generated docs');
expect(proc.exit).toEqual(0);
});
it('can serve in mkdocs', async () => {
jest.setTimeout(10000);
const proc = await executeTechDocsCliCommand(
['serve:mkdocs', '--no-docker'],
{
cwd: FIXTURE_DIR,
killAfter: 8000,
},
);
expect(proc.combinedStdOutErr).toContain('Starting mkdocs server');
expect(proc.exit).toEqual(0);
});
it('can serve in backstage', async () => {
jest.setTimeout(10000);
const proc = await executeTechDocsCliCommand(
['serve', '--no-docker', '--mkdocs-port=8888'],
{
cwd: FIXTURE_DIR,
killAfter: 8000,
},
);
expect(proc.combinedStdOutErr).toContain('Starting mkdocs server');
expect(proc.combinedStdOutErr).toContain('Serving docs in Backstage at');
expect(proc.exit).toEqual(0);
});
});
type CommandResponse = {
stdout: string;
stderr: string;
combinedStdOutErr: string;
exit: number;
};
type ExecuteCommandOptions = {
killAfter?: number;
cwd?: string;
};
function executeTechDocsCliCommand(
args: string[],
opts: ExecuteCommandOptions = {},
): Promise<CommandResponse> {
return new Promise(resolve => {
const pathToCli = path.resolve(PROJECT_ROOT_DIR, 'bin/techdocs-cli');
const commandResponse = {
stdout: '',
stderr: '',
combinedStdOutErr: '',
exit: 0,
};
const listen = spawn(pathToCli, args, {
cwd: opts.cwd,
});
const stdOutChunks: any[] = [];
const stdErrChunks: any[] = [];
const combinedChunks: any[] = [];
listen.stdout.on('data', data => {
stdOutChunks.push(data);
combinedChunks.push(data);
});
listen.stderr.on('data', data => {
stdErrChunks.push(data);
combinedChunks.push(data);
});
listen.on('exit', code => {
commandResponse.exit = code as number;
commandResponse.stdout = Buffer.concat(stdOutChunks).toString('utf8');
commandResponse.stderr = Buffer.concat(stdErrChunks).toString('utf8');
commandResponse.combinedStdOutErr =
Buffer.concat(combinedChunks).toString('utf8');
resolve(commandResponse);
});
if (opts.killAfter) {
setTimeout(() => {
listen.kill('SIGTERM');
}, opts.killAfter);
}
});
}
@@ -0,0 +1 @@
# Test Fixture
@@ -0,0 +1,8 @@
site_name: docs-test-fixture
site_description: Documentation site test fixture
nav:
- HOME: README.md
plugins:
- techdocs-core
+29
View File
@@ -0,0 +1,29 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import program from 'commander';
import { registerCommands } from './commands';
import { version } from '../package.json';
const main = (argv: string[]) => {
program.name('techdocs-cli').version(version);
registerCommands(program);
program.parse(argv);
};
main(process.argv);
@@ -0,0 +1,141 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Command } from 'commander';
import { PublisherConfig } from './PublisherConfig';
describe('getValidPublisherConfig', () => {
it('should not allow unknown publisher types', () => {
const invalidConfig = {
publisherType: 'unknown publisher',
} as unknown as Command;
expect(() => PublisherConfig.getValidConfig(invalidConfig)).toThrowError(
`Unknown publisher type ${invalidConfig.publisherType}`,
);
});
describe('for azureBlobStorage', () => {
it('should require --azureAccountName', () => {
const config = {
publisherType: 'azureBlobStorage',
} as unknown as Command;
expect(() => PublisherConfig.getValidConfig(config)).toThrowError(
'azureBlobStorage requires --azureAccountName to be specified',
);
});
it('should return valid ConfigReader', () => {
const config = {
publisherType: 'azureBlobStorage',
azureAccountName: 'someAccountName',
storageName: 'someContainer',
} as unknown as Command;
const actualConfig = PublisherConfig.getValidConfig(config);
expect(actualConfig.getString('techdocs.publisher.type')).toBe(
'azureBlobStorage',
);
expect(
actualConfig.getString(
'techdocs.publisher.azureBlobStorage.containerName',
),
).toBe('someContainer');
expect(
actualConfig.getString(
'techdocs.publisher.azureBlobStorage.credentials.accountName',
),
).toBe('someAccountName');
});
});
describe('for awsS3', () => {
it('should return valid ConfigReader', () => {
const config = {
publisherType: 'awsS3',
storageName: 'someStorageName',
} as unknown as Command;
const actualConfig = PublisherConfig.getValidConfig(config);
expect(actualConfig.getString('techdocs.publisher.type')).toBe('awsS3');
expect(
actualConfig.getString('techdocs.publisher.awsS3.bucketName'),
).toBe('someStorageName');
});
});
describe('for openStackSwift', () => {
it('should throw error on missing parameters', () => {
const config = {
publisherType: 'openStackSwift',
osCredentialId: 'someCredentialId',
osSecret: 'someSecret',
} as unknown as Command;
expect(() => PublisherConfig.getValidConfig(config)).toThrowError(
`openStackSwift requires the following params to be specified: ${[
'osAuthUrl',
'osSwiftUrl',
].join(', ')}`,
);
});
it('should return valid ConfigReader', () => {
const config = {
publisherType: 'openStackSwift',
storageName: 'someStorageName',
osCredentialId: 'someCredentialId',
osSecret: 'someSecret',
osAuthUrl: 'someAuthUrl',
osSwiftUrl: 'someSwiftUrl',
} as unknown as Command;
const actualConfig = PublisherConfig.getValidConfig(config);
expect(actualConfig.getString('techdocs.publisher.type')).toBe(
'openStackSwift',
);
expect(
actualConfig.getConfig('techdocs.publisher.openStackSwift').get(),
).toMatchObject({
containerName: 'someStorageName',
credentials: {
id: 'someCredentialId',
secret: 'someSecret',
},
authUrl: 'someAuthUrl',
swiftUrl: 'someSwiftUrl',
});
});
});
describe('for googleGcs', () => {
it('should return valid ConfigReader', () => {
const config = {
publisherType: 'googleGcs',
storageName: 'someStorageName',
} as unknown as Command;
const actualConfig = PublisherConfig.getValidConfig(config);
expect(actualConfig.getString('techdocs.publisher.type')).toBe(
'googleGcs',
);
expect(
actualConfig.getString('techdocs.publisher.googleGcs.bucketName'),
).toBe('someStorageName');
});
});
});
@@ -0,0 +1,163 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ConfigReader } from '@backstage/config';
import { Command } from 'commander';
type Publisher = keyof typeof PublisherConfig['configFactories'];
type PublisherConfiguration = {
[p in Publisher]?: any;
} & {
type: Publisher;
};
/**
* Helper when working with publisher-related configurations.
*/
export class PublisherConfig {
/**
* Maps publisher-specific config keys to config getters.
*/
private static configFactories = {
awsS3: PublisherConfig.getValidAwsS3Config,
azureBlobStorage: PublisherConfig.getValidAzureConfig,
googleGcs: PublisherConfig.getValidGoogleGcsConfig,
openStackSwift: PublisherConfig.getValidOpenStackSwiftConfig,
};
/**
* Returns Backstage config suitable for use when instantiating a Publisher. If
* there are any missing or invalid options provided, an error is thrown.
*
* Note: This assumes that proper credentials are set in Environment
* variables for the respective GCS/AWS clients to work.
*/
static getValidConfig(cmd: Command): ConfigReader {
const publisherType = cmd.publisherType;
if (!PublisherConfig.isKnownPublisher(publisherType)) {
throw new Error(`Unknown publisher type ${cmd.publisherType}`);
}
return new ConfigReader({
// This backend config is not used at all. Just something needed a create a mock discovery instance.
backend: {
baseUrl: 'http://localhost:7000',
listen: {
port: 7000,
},
},
techdocs: {
publisher: PublisherConfig.configFactories[publisherType](cmd),
legacyUseCaseSensitiveTripletPaths:
cmd.legacyUseCaseSensitiveTripletPaths,
},
});
}
/**
* Typeguard to ensure the publisher has a known config getter.
*/
private static isKnownPublisher(
type: string,
): type is keyof typeof PublisherConfig['configFactories'] {
return PublisherConfig.configFactories.hasOwnProperty(type);
}
/**
* Retrieve valid AWS S3 configuration based on the command.
*/
private static getValidAwsS3Config(cmd: Command): PublisherConfiguration {
return {
type: 'awsS3',
awsS3: {
bucketName: cmd.storageName,
...(cmd.awsRoleArn && { credentials: { roleArn: cmd.awsRoleArn } }),
...(cmd.awsEndpoint && { endpoint: cmd.awsEndpoint }),
...(cmd.awsS3ForcePathStyle && { s3ForcePathStyle: true }),
},
};
}
/**
* Retrieve valid Azure Blob Storage configuration based on the command.
*/
private static getValidAzureConfig(cmd: Command): PublisherConfiguration {
if (!cmd.azureAccountName) {
throw new Error(
`azureBlobStorage requires --azureAccountName to be specified`,
);
}
return {
type: 'azureBlobStorage',
azureBlobStorage: {
containerName: cmd.storageName,
credentials: {
accountName: cmd.azureAccountName,
accountKey: cmd.azureAccountKey,
},
},
};
}
/**
* Retrieve valid GCS configuration based on the command.
*/
private static getValidGoogleGcsConfig(cmd: Command): PublisherConfiguration {
return {
type: 'googleGcs',
googleGcs: {
bucketName: cmd.storageName,
},
};
}
/**
* Retrieves valid OpenStack Swift configuration based on the command.
*/
private static getValidOpenStackSwiftConfig(
cmd: Command,
): PublisherConfiguration {
const missingParams = [
'osCredentialId',
'osSecret',
'osAuthUrl',
'osSwiftUrl',
].filter((param: string) => !cmd[param]);
if (missingParams.length) {
throw new Error(
`openStackSwift requires the following params to be specified: ${missingParams.join(
', ',
)}`,
);
}
return {
type: 'openStackSwift',
openStackSwift: {
containerName: cmd.storageName,
credentials: {
id: cmd.osCredentialId,
secret: cmd.osSecret,
},
authUrl: cmd.osAuthUrl,
swiftUrl: cmd.osSwiftUrl,
},
};
}
}
+100
View File
@@ -0,0 +1,100 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import serveHandler from 'serve-handler';
import http from 'http';
import httpProxy from 'http-proxy';
import { createLogger } from './utility';
export default class HTTPServer {
private readonly proxyEndpoint: string;
private readonly backstageBundleDir: string;
private readonly backstagePort: number;
private readonly mkdocsPort: number;
private readonly verbose: boolean;
constructor(
backstageBundleDir: string,
backstagePort: number,
mkdocsPort: number,
verbose: boolean,
) {
this.proxyEndpoint = '/api/';
this.backstageBundleDir = backstageBundleDir;
this.backstagePort = backstagePort;
this.mkdocsPort = mkdocsPort;
this.verbose = verbose;
}
// Create a Proxy for mkdocs server
private createProxy() {
const proxy = httpProxy.createProxyServer({
target: `http://localhost:${this.mkdocsPort}`,
});
return (request: http.IncomingMessage): [httpProxy, string] => {
// If the request goes to /api/ we want to remove /api/ from the prefix of the request URL.
// e.g. ['/', 'api', pathChunks]
const [, , ...pathChunks] = request.url?.split('/') ?? [];
const forwardPath = pathChunks.join('/');
return [proxy, forwardPath];
};
}
public async serve(): Promise<http.Server> {
return new Promise<http.Server>((resolve, reject) => {
const proxyHandler = this.createProxy();
const server = http.createServer(
(request: http.IncomingMessage, response: http.ServerResponse) => {
if (request.url?.startsWith(this.proxyEndpoint)) {
const [proxy, forwardPath] = proxyHandler(request);
proxy.on('error', (error: Error) => {
reject(error);
});
response.setHeader('Access-Control-Allow-Origin', '*');
response.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
request.url = forwardPath;
return proxy.web(request, response);
}
return serveHandler(request, response, {
public: this.backstageBundleDir,
trailingSlash: true,
rewrites: [{ source: '**', destination: 'index.html' }],
});
},
);
const logger = createLogger({ verbose: false });
server.listen(this.backstagePort, () => {
if (this.verbose) {
logger.info(
`[techdocs-preview-bundle] Running local version of Backstage at http://localhost:${this.backstagePort}`,
);
}
resolve(server);
});
server.on('error', (error: Error) => {
reject(error);
});
});
}
}
@@ -0,0 +1,90 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { runMkdocsServer } from './mkdocsServer';
import { run } from './run';
jest.mock('./run', () => {
return {
run: jest.fn(),
};
});
describe('runMkdocsServer', () => {
beforeEach(() => {
jest.resetAllMocks();
});
describe('docker', () => {
it('should run docker directly by default', async () => {
await runMkdocsServer({});
const quotedCwd = `"${process.cwd()}":/content`;
expect(run).toHaveBeenCalledWith(
'docker',
expect.arrayContaining([
'run',
quotedCwd,
'8000:8000',
'serve',
'--dev-addr',
'0.0.0.0:8000',
'spotify/techdocs',
]),
expect.objectContaining({}),
);
});
it('should accept port option', async () => {
await runMkdocsServer({ port: '5678' });
expect(run).toHaveBeenCalledWith(
'docker',
expect.arrayContaining(['5678:5678', '0.0.0.0:5678']),
expect.objectContaining({}),
);
});
it('should accept custom docker image', async () => {
await runMkdocsServer({ dockerImage: 'my-org/techdocs' });
expect(run).toHaveBeenCalledWith(
'docker',
expect.arrayContaining(['my-org/techdocs']),
expect.objectContaining({}),
);
});
});
describe('mkdocs', () => {
it('should run mkdocs if specified', async () => {
await runMkdocsServer({ useDocker: false });
expect(run).toHaveBeenCalledWith(
'mkdocs',
expect.arrayContaining(['serve', '--dev-addr', '127.0.0.1:8000']),
expect.objectContaining({}),
);
});
it('should accept port option', async () => {
await runMkdocsServer({ useDocker: false, port: '5678' });
expect(run).toHaveBeenCalledWith(
'mkdocs',
expect.arrayContaining(['127.0.0.1:5678']),
expect.objectContaining({}),
);
});
});
});
@@ -0,0 +1,59 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ChildProcess } from 'child_process';
import { run, LogFunc } from './run';
export const runMkdocsServer = async (options: {
port?: string;
useDocker?: boolean;
dockerImage?: string;
stdoutLogFunc?: LogFunc;
stderrLogFunc?: LogFunc;
}): Promise<ChildProcess> => {
const port = options.port ?? '8000';
const useDocker = options.useDocker ?? true;
const dockerImage = options.dockerImage ?? 'spotify/techdocs';
if (useDocker) {
return await run(
'docker',
[
'run',
'--rm',
'-w',
'/content',
'-v',
`"${process.cwd()}":/content`,
'-p',
`${port}:${port}`,
dockerImage,
'serve',
'--dev-addr',
`0.0.0.0:${port}`,
],
{
stdoutLogFunc: options.stdoutLogFunc,
stderrLogFunc: options.stderrLogFunc,
},
);
}
return await run('mkdocs', ['serve', '--dev-addr', `127.0.0.1:${port}`], {
stdoutLogFunc: options.stdoutLogFunc,
stderrLogFunc: options.stderrLogFunc,
});
};
+106
View File
@@ -0,0 +1,106 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { spawn, SpawnOptions, ChildProcess } from 'child_process';
export type LogFunc = (data: Buffer | string) => void;
type SpawnOptionsPartialEnv = Omit<SpawnOptions, 'env'> & {
env?: Partial<NodeJS.ProcessEnv>;
// Pipe stdout to this log function
stdoutLogFunc?: LogFunc;
// Pipe stderr to this log function
stderrLogFunc?: LogFunc;
};
// TODO: Accept log functions to pipe logs with.
// Runs a child command, returning the child process instance.
// Use it along with waitForSignal to run a long running process e.g. mkdocs serve
export const run = async (
name: string,
args: string[] = [],
options: SpawnOptionsPartialEnv = {},
): Promise<ChildProcess> => {
const { stdoutLogFunc, stderrLogFunc } = options;
const env: NodeJS.ProcessEnv = {
...process.env,
FORCE_COLOR: 'true',
...(options.env ?? {}),
};
// Refer: https://nodejs.org/api/child_process.html#child_process_subprocess_stdio
const stdio = [
'inherit',
stdoutLogFunc ? 'pipe' : 'inherit',
stderrLogFunc ? 'pipe' : 'inherit',
] as ('inherit' | 'pipe')[];
const childProcess = spawn(name, args, {
stdio: stdio,
shell: true,
...options,
env,
});
if (stdoutLogFunc && childProcess.stdout) {
childProcess.stdout.on('data', stdoutLogFunc);
}
if (stderrLogFunc && childProcess.stderr) {
childProcess.stderr.on('data', stderrLogFunc);
}
return childProcess;
};
// Block indefinitely and wait for a signal to kill the child process(es)
// Throw error if any child process errors
// Resolves only when all processes exit with status code 0
export async function waitForSignal(
childProcesses: Array<ChildProcess>,
): Promise<void> {
const promises: Array<Promise<void>> = [];
childProcesses.forEach(childProcess => {
if (typeof childProcess.exitCode === 'number') {
if (childProcess.exitCode) {
throw new Error(`Non zero exit code from child process`);
}
return;
}
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
process.on(signal, () => {
childProcess.kill(signal);
// exit instead of resolve. The process is shutting down and resolving a promise here logs an error
process.exit();
});
}
promises.push(
new Promise<void>((resolve, reject) => {
childProcess.once('error', error => reject(error));
childProcess.once('exit', code => {
if (code) {
reject(new Error(`Non zero exit code from child process`));
} else {
resolve();
}
});
}),
);
});
await Promise.all(promises);
}
+54
View File
@@ -0,0 +1,54 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
RemoteProtocol,
ParsedLocationAnnotation,
} from '@backstage/techdocs-common';
import * as winston from 'winston';
export const convertTechDocsRefToLocationAnnotation = (
techdocsRef: string,
): ParsedLocationAnnotation => {
// Split on the first colon for the protocol and the rest after the first split
// is the location.
const [type, target] = techdocsRef.split(/:(.+)/) as [
RemoteProtocol?,
string?,
];
if (!type || !target) {
throw new Error(
`Can not parse --techdocs-ref ${techdocsRef}. Should be of type HOST:URL.`,
);
}
return { type, target };
};
export const createLogger = ({
verbose = false,
}: {
verbose: boolean;
}): winston.Logger => {
const logger = winston.createLogger({
level: verbose ? 'verbose' : 'info',
transports: [
new winston.transports.Console({ format: winston.format.simple() }),
],
});
return logger;
};
+1
View File
@@ -99,6 +99,7 @@ const SKIPPED_PACKAGES = [
join('packages', 'codemods'),
join('packages', 'create-app'),
join('packages', 'e2e-test'),
join('packages', 'embedded-techdocs-app'),
join('packages', 'storybook'),
join('packages', 'techdocs-cli'),
];
+134 -2
View File
@@ -6825,6 +6825,14 @@
dependencies:
classnames "*"
"@types/clean-css@*":
version "4.2.5"
resolved "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.5.tgz#69ce62cc13557c90ca40460133f672dc52ceaf89"
integrity sha512-NEzjkGGpbs9S9fgC4abuBvTpVwE3i+Acu9BBod3PUyjDVZcNsGx61b8r2PphR61QGPnn0JHVs5ey6/I4eTrkxw==
dependencies:
"@types/node" "*"
source-map "^0.6.0"
"@types/codemirror@^0.0.108":
version "0.0.108"
resolved "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.108.tgz#e640422b666bf49251b384c390cdeb2362585bde"
@@ -6856,6 +6864,13 @@
resolved "https://registry.npmjs.org/@types/command-exists/-/command-exists-1.2.0.tgz#d97e0ed10097090e4ab0367ed425b0312fad86f3"
integrity sha512-ugsxEJfsCuqMLSuCD4PIJkp5Uk2z6TCMRCgYVuhRo5cYQY3+1xXTQkSlPtkpGHuvWMjS2KTeVQXxkXRACMbM6A==
"@types/commander@^2.12.2":
version "2.12.2"
resolved "https://registry.npmjs.org/@types/commander/-/commander-2.12.2.tgz#183041a23842d4281478fa5d23c5ca78e6fd08ae"
integrity sha512-0QEFiR8ljcHp9bAbWxecjVRuAMr16ivPiGOw6KFQBVrVd0RQIcM3xKdRisH2EDWgVWujiYtHwhSkSUoAAGzH7Q==
dependencies:
commander "*"
"@types/compression@^1.7.0":
version "1.7.0"
resolved "https://registry.npmjs.org/@types/compression/-/compression-1.7.0.tgz#8dc2a56604873cf0dd4e746d9ae4d31ae77b2390"
@@ -7112,6 +7127,13 @@
dependencies:
"@types/node" "*"
"@types/fs-extra@^9.0.6":
version "9.0.13"
resolved "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz#7594fbae04fe7f1918ce8b3d213f74ff44ac1f45"
integrity sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==
dependencies:
"@types/node" "*"
"@types/git-url-parse@^9.0.0":
version "9.0.0"
resolved "https://registry.npmjs.org/@types/git-url-parse/-/git-url-parse-9.0.0.tgz#aac1315a44fa4ed5a52c3820f6c3c2fb79cbd12d"
@@ -7169,6 +7191,24 @@
resolved "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.0.tgz#551a4589b6ee2cc9c1dff08056128aec29b94880"
integrity sha512-iYCgjm1dGPRuo12+BStjd1HiVQqhlRhWDOQigNxn023HcjnhsiFz9pc6CzJj4HwDCSQca9bxTL4PxJDbkdm3PA==
"@types/html-minifier@*":
version "4.0.1"
resolved "https://registry.npmjs.org/@types/html-minifier/-/html-minifier-4.0.1.tgz#9486ffc144f8d7b8f75b07939c500ac3d73617a0"
integrity sha512-6u58FWQbWP45bsxVeMJo0yk2LEsjjZsCwn0JDe/i5Edk3L+b9TR5eZ2FGaMCrLdtGYpME5AGxUqv8o+3hWKogw==
dependencies:
"@types/clean-css" "*"
"@types/relateurl" "*"
"@types/uglify-js" "*"
"@types/html-webpack-plugin@*":
version "3.2.6"
resolved "https://registry.npmjs.org/@types/html-webpack-plugin/-/html-webpack-plugin-3.2.6.tgz#07951aaf0fa260dbf626f9644f1d13106d537625"
integrity sha512-U8uJSvlf9lwrKG6sKFnMhqY4qJw2QXad+PHlX9sqEXVUMilVt96aVvFde73tzsdXD+QH9JS6kEytuGO2JcYZog==
dependencies:
"@types/html-minifier" "*"
"@types/tapable" "^1"
"@types/webpack" "^4"
"@types/http-assert@*":
version "1.5.1"
resolved "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.1.tgz#d775e93630c2469c2f980fc27e3143240335db3b"
@@ -7692,6 +7732,17 @@
dependencies:
"@types/react" "*"
"@types/react-dev-utils@^9.0.4":
version "9.0.8"
resolved "https://registry.npmjs.org/@types/react-dev-utils/-/react-dev-utils-9.0.8.tgz#7e4d63d1e1c71cd236c9055bc0c0dbaa3772bcf9"
integrity sha512-H/R8BvtCf9BUWPLL9a2agUWWBOKQQPkBIe5osdrgGaDJHZggQRiNN3emH16rQkzm5zi6TVuslOFrYrfMx+QTjw==
dependencies:
"@types/eslint" "*"
"@types/express" "*"
"@types/html-webpack-plugin" "*"
"@types/webpack" "^4"
"@types/webpack-dev-server" "^3"
"@types/react-dom@*", "@types/react-dom@>=16.9.0":
version "17.0.9"
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.9.tgz#441a981da9d7be117042e1a6fd3dac4b30f55add"
@@ -7802,6 +7853,11 @@
resolved "https://registry.npmjs.org/@types/regression/-/regression-2.0.2.tgz#a1ad747fbcc6726643a8eb2c42bb804bbf34ce02"
integrity sha512-i7KOGl6xdkfpq5+p2ooC+/XFIRUMkYymZ29SD8p+Ko9lesKGUsh6860ey3YM7Y+ZG7kEDGcjzyLO3sOhozqEeA==
"@types/relateurl@*":
version "0.2.29"
resolved "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.29.tgz#68ccecec3d4ffdafb9c577fe764f912afc050fe6"
integrity sha512-QSvevZ+IRww2ldtfv1QskYsqVVVwCKQf1XbwtcyyoRvLIQzfyPhj/C+3+PKzSDRdiyejaiLgnq//XTkleorpLg==
"@types/request@^2.47.1":
version "2.48.5"
resolved "https://registry.npmjs.org/@types/request/-/request-2.48.5.tgz#019b8536b402069f6d11bee1b2c03e7f232937a0"
@@ -7863,6 +7919,13 @@
resolved "https://registry.npmjs.org/@types/semver/-/semver-7.3.8.tgz#508a27995498d7586dcecd77c25e289bfaf90c59"
integrity sha512-D/2EJvAlCEtYFEYmmlGwbGXuK886HzyCc3nZX/tkFTQdEU8jZDAgiv08P162yB17y4ZXZoq7yFAnW4GDBb9Now==
"@types/serve-handler@^6.1.0":
version "6.1.1"
resolved "https://registry.npmjs.org/@types/serve-handler/-/serve-handler-6.1.1.tgz#629dc9a62b201ab79a216e1e46e162aa4c8d1455"
integrity sha512-bIwSmD+OV8w0t2e7EWsuQYlGoS1o5aEdVktgkXaa43Zm0qVWi21xaSRb3DQA1UXD+DJ5bRq1Rgu14ZczB+CjIQ==
dependencies:
"@types/node" "*"
"@types/serve-static@*":
version "1.13.9"
resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz#aacf28a85a05ee29a11fb7c3ead935ac56f33e4e"
@@ -8076,6 +8139,17 @@
"@types/expect" "^1.20.4"
"@types/node" "*"
"@types/webpack-dev-server@^3":
version "3.11.6"
resolved "https://registry.npmjs.org/@types/webpack-dev-server/-/webpack-dev-server-3.11.6.tgz#d8888cfd2f0630203e13d3ed7833a4d11b8a34dc"
integrity sha512-XCph0RiiqFGetukCTC3KVnY1jwLcZ84illFRMbyFzCcWl90B/76ew0tSqF46oBhnLC4obNDG7dMO0JfTN0MgMQ==
dependencies:
"@types/connect-history-api-fallback" "*"
"@types/express" "*"
"@types/serve-static" "*"
"@types/webpack" "^4"
http-proxy-middleware "^1.0.0"
"@types/webpack-dev-server@^3.11.5":
version "3.11.5"
resolved "https://registry.npmjs.org/@types/webpack-dev-server/-/webpack-dev-server-3.11.5.tgz#f4a254a3dd0667c8ee4af90d42afdb4ad1d607f3"
@@ -8092,6 +8166,11 @@
resolved "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.16.0.tgz#8c0a9435dfa7b3b1be76562f3070efb3f92637b4"
integrity sha512-Fx+NpfOO0CpeYX2g9bkvX8O5qh9wrU1sOF4g8sft4Mu7z+qfe387YlyY8w8daDyDsKY5vUxM0yxkAYnbkRbZEw==
"@types/webpack-env@^1.15.3":
version "1.16.3"
resolved "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.16.3.tgz#b776327a73e561b71e7881d0cd6d34a1424db86a"
integrity sha512-9gtOPPkfyNoEqCQgx4qJKkuNm/x0R2hKR7fdl7zvTJyHnIisuE/LfvXOsYWL0o3qq6uiBnKZNNNzi3l0y/X+xw==
"@types/webpack-sources@*":
version "0.1.6"
resolved "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.6.tgz#3d21dfc2ec0ad0c77758e79362426a9ba7d7cbcb"
@@ -11289,6 +11368,11 @@ command-exists@^1.2.9:
resolved "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69"
integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==
commander@*:
version "8.3.0"
resolved "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
commander@7.1.0:
version "7.1.0"
resolved "https://registry.npmjs.org/commander/-/commander-7.1.0.tgz#f2eaecf131f10e36e07d894698226e36ae0eb5ff"
@@ -11507,6 +11591,11 @@ contains-path@^0.1.0:
resolved "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=
content-disposition@0.5.2:
version "0.5.2"
resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ=
content-disposition@0.5.3:
version "0.5.3"
resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
@@ -14364,6 +14453,13 @@ fast-text-encoding@^1.0.0, fast-text-encoding@^1.0.3:
resolved "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53"
integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==
fast-url-parser@1.1.3:
version "1.1.3"
resolved "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d"
integrity sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0=
dependencies:
punycode "^1.3.2"
fastest-stable-stringify@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz#3757a6774f6ec8de40c4e86ec28ea02417214c76"
@@ -20342,6 +20438,18 @@ mime-db@1.49.0:
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d"
integrity sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==
mime-db@~1.33.0:
version "1.33.0"
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==
mime-types@2.1.18:
version "2.1.18"
resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8"
integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==
dependencies:
mime-db "~1.33.0"
mime-types@^2.0.8, mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24:
version "2.1.32"
resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz#1d00e89e7de7fe02008db61001d9e02852670fd5"
@@ -22227,7 +22335,7 @@ path-is-absolute@^1.0.0:
resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
path-is-inside@^1.0.2:
path-is-inside@1.0.2, path-is-inside@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
@@ -22264,6 +22372,11 @@ path-to-regexp@0.1.7:
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
path-to-regexp@2.2.1:
version "2.2.1"
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz#90b617025a16381a879bc82a38d4e8bdeb2bcf45"
integrity sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==
path-to-regexp@^1.7.0:
version "1.8.0"
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
@@ -23428,7 +23541,7 @@ punycode@1.3.2:
resolved "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
punycode@^1.2.4:
punycode@^1.2.4, punycode@^1.3.2:
version "1.4.1"
resolved "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
@@ -23577,6 +23690,11 @@ randomfill@^1.0.3:
randombytes "^2.0.5"
safe-buffer "^5.1.0"
range-parser@1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=
range-parser@^1.2.1, range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
@@ -25428,6 +25546,20 @@ serve-favicon@^2.5.0:
parseurl "~1.3.2"
safe-buffer "5.1.1"
serve-handler@^6.1.3:
version "6.1.3"
resolved "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.3.tgz#1bf8c5ae138712af55c758477533b9117f6435e8"
integrity sha512-FosMqFBNrLyeiIDvP1zgO6YoTzFYHxLDEIavhlmQ+knB2Z7l1t+kGLHkZIDN7UVWqQAmKI3D20A6F6jo3nDd4w==
dependencies:
bytes "3.0.0"
content-disposition "0.5.2"
fast-url-parser "1.1.3"
mime-types "2.1.18"
minimatch "3.0.4"
path-is-inside "1.0.2"
path-to-regexp "2.2.1"
range-parser "1.2.0"
serve-index@^1.9.1:
version "1.9.1"
resolved "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239"