Initial techdocs-cli (#1558)

* Initial techdocs-cli

* Run backstage serve and docker container from techdocs cli

* Added documentation for techdocs-cli

* Fix linting

* Builds and export the dev folder of a plugin

* make bin/build executable

* Remove 'src' from files

* fix

* fix: corrected bad rebase

* feat(techdocs-cli): create export in techdocs plugin + clone to cli build

* fix: added support for nodemon

* feat(techdocs-cli): add vercel/serve-handler npm package

* fix: bump eslint-config to 8.0.0 + add prettier-config

* fixup

* fix: add HTTPServer module for serving techdocs-preview-bundle/

* fix: addressed tsc issues (unused import, missing type definition)

Co-authored-by: Bilawal Hameed <bil@spotify.com>
This commit is contained in:
Sebastian Qvarfordt
2020-07-09 11:08:19 +02:00
committed by GitHub
parent a10705da63
commit eed37376fc
9 changed files with 479 additions and 11 deletions
+4 -1
View File
@@ -1,3 +1,6 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
rules: {
'no-console': 0,
},
};
+46
View File
@@ -3,3 +3,49 @@
Check out the [TechDocs README](https://github.com/spotify/backstage/blob/master/plugins/techdocs/README.md) to learn more.
**WIP: This cli is a work in progress. It is not ready for use yet. Follow our progress on [the Backstage Discord](https://discord.gg/MUpMjP2) under #docs-like-code or on [our GitHub Milestone](https://github.com/spotify/backstage/milestone/15).**
## Getting Started
You'll need Docker installed and running to use this. You will also need to build the container located at `plugins/techdocs/mkdocs/container` under the tag `mkdocs:local-dev`, as you can see in the commands from below:
```bash
docker build plugins/techdocs/mkdocs/container -t mkdocs:local-dev
```
From that point, you can invoke the CLI from any project with a docs folder. Try out our example!
```bash
cd plugins/techdocs/mkdocs/mock-docs
npx @techdocs/cli serve
```
## Local Development
You'll need Docker installed and running to use this. You will also need to build the container located at `plugins/techdocs/mkdocs/container` under the tag `mkdocs:local-dev`, as you can see in the commands from below:
```bash
docker build plugins/techdocs/mkdocs/container -t mkdocs:local-dev
```
Once that is built, you'll need to manually create an `alias` for running the CLI locally:
```bash
cd packages/techdocs-cli
echo "$(pwd)/bin/techdocs"
# Copy the value from above and add it in [HERE] below
# For more convenience, add it to your ~/.zshrc or ~/.bash_profile
# otherwise you'll lose it when you open a new Terminal
alias techdocs="[HERE]"
```
From that point, you can invoke `techdocs` from any project with a docs folder. Try out our example!
```bash
cd plugins/techdocs/mkdocs/mock-docs
techdocs serve
```
You should have a `localhost:3000` serving TechDocs in Backstage, as well as `localhost:8000` serving Mkdocs (which won't open up and be exposed to the user).
Happy hacking!
+35
View File
@@ -0,0 +1,35 @@
# Copyright 2020 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.
set -e
ROOT_DIR=$(git rev-parse --show-toplevel)
TECHDOCS_PREVIEW_SOURCE=$ROOT_DIR/plugins/techdocs/dist
TECHDOCS_PREVIEW_DEST=$ROOT_DIR/packages/techdocs-cli/dist/techdocs-preview-bundle
# Build the CLI
backstage-cli build --outputs cjs
# Create export of the TechDocs plugin
yarn workspace @backstage/plugin-techdocs export
# Copy over export to techdocs-cli dist/ folder
cp -r $TECHDOCS_PREVIEW_SOURCE $TECHDOCS_PREVIEW_DEST
# Clean output
yarn workspace @backstage/plugin-techdocs clean
# Write to console
echo "[techdocs-cli]: Built the dist/ folder"
echo "[techdocs-cli]: Imported @backstage/plugin-techdocs dist/ folder into techdocs-preview-bundle/"
+17 -5
View File
@@ -1,8 +1,8 @@
{
"name": "@backstage/techdocs-cli",
"name": "@techdocs/cli",
"description": "CLI for running TechDocs locally.",
"version": "0.1.1-alpha.13",
"private": true,
"private": false,
"publishConfig": {
"access": "public"
},
@@ -18,7 +18,7 @@
"license": "Apache-2.0",
"main": "dist/index.cjs.js",
"scripts": {
"build": "backstage-cli build --outputs cjs",
"build": "./bin/build",
"lint": "backstage-cli lint",
"test": "backstage-cli test --passWithNoTests",
"clean": "backstage-cli clean",
@@ -28,7 +28,11 @@
"techdocs": "bin/techdocs-cli"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.13"
"@spotify/eslint-config": "^7.0.0",
"@spotify/prettier-config": "^7.0.0",
"@types/serve-handler": "^6.1.0",
"eslint": "^7.1.0",
"eslint-plugin-import": "^2.22.0"
},
"files": [
"bin",
@@ -36,7 +40,15 @@
],
"nodemonConfig": {
"watch": "./src",
"exec": "bin/techdocs-cli",
"exec": "bin/build && bin/techdocs-cli",
"ext": "ts"
},
"dependencies": {
"@backstage/cli": "^0.1.1-alpha.13",
"@backstage/plugin-techdocs": "^0.1.1-alpha.13",
"chalk": "^4.1.0",
"commander": "^5.1.0",
"fs-extra": "^9.0.1",
"serve-handler": "^6.1.3"
}
}
+73 -1
View File
@@ -13,5 +13,77 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import program from 'commander';
import { version } from './lib/version';
// import chalk from 'chalk';
import { spawn } from 'child_process';
import path from 'path';
// import HTTPServer from './lib/httpServer';
export const techDocsCli = () => {};
const run = (workingDirectory: string, name: string, args: string[] = []) => {
const child = spawn(name, args, {
cwd: workingDirectory,
stdio: ['inherit', 'inherit', 'inherit'],
shell: true,
env: {
...process.env,
FORCE_COLOR: 'true',
},
});
child.once('error', error => {
console.error(error);
});
child.once('exit', code => {
console.log('exited!', code);
});
};
const main = (argv: string[]) => {
program.name('techdocs-cli').version(version);
program
.command('serve')
.description('Serve a documentation project locally')
.action(() => {
// const techdocsPreviewBundlePath = path.join(
// __dirname,
// '..',
// 'dist',
// 'techdocs-preview-bundle',
// );
// new HTTPServer(techdocsPreviewBundlePath, 3000).serve();
run(process.env.PWD!, 'docker', [
'run',
'-it',
'-w',
'/content',
'-v',
'$(pwd):/content',
'-p',
'8000:8000',
'mkdocs:local-dev',
'serve',
'-a',
'0.0.0.0:8000',
]);
const pluginPath = path.join(
require.resolve('@backstage/plugin-techdocs'),
'..',
'..',
);
run(
pluginPath,
path.join(require.resolve('@backstage/cli'), '../../bin/backstage-cli'),
['plugin:serve'],
);
});
program.parse(argv);
};
main(process.argv);
@@ -0,0 +1,35 @@
/*
* Copyright 2020 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 serveHandler from 'serve-handler';
import http from 'http';
export default class HTTPServer {
constructor(public dir: string, public port: number) {}
serve() {
const server = http.createServer((request, response) => {
return serveHandler(request, response, {
public: this.dir,
trailingSlash: true,
});
});
server.listen(this.port, () => {
console.log('Running at http://localhost:3000');
});
}
}
+150
View File
@@ -0,0 +1,150 @@
/*
* Copyright 2020 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 fs from 'fs-extra';
import { dirname, resolve as resolvePath } from 'path';
export type ResolveFunc = (...paths: string[]) => string;
// Common paths and resolve functions used by the cli.
// Currently assumes it is being executed within a monorepo.
export type Paths = {
// Root dir of the cli itself, containing package.json
ownDir: string;
// Monorepo root dir of the cli itself. Only accessible when running inside Backstage repo.
ownRoot: string;
// The location of the app that the cli is being executed in
targetDir: string;
// The monorepo root package of the app that the cli is being executed in.
targetRoot: string;
// Resolve a path relative to own repo
resolveOwn: ResolveFunc;
// Resolve a path relative to own monorepo root. Only accessible when running inside Backstage repo.
resolveOwnRoot: ResolveFunc;
// Resolve a path relative to the app
resolveTarget: ResolveFunc;
// Resolve a path relative to the app repo root
resolveTargetRoot: ResolveFunc;
};
// Looks for a package.json that has name: "root" to identify the root of the monorepo
export function findRootPath(topPath: string): string {
let path = topPath;
// Some sanity check to avoid infinite loop
for (let i = 0; i < 1000; i++) {
const packagePath = resolvePath(path, 'package.json');
const exists = fs.pathExistsSync(packagePath);
if (exists) {
try {
const data = fs.readJsonSync(packagePath);
if (data.name === 'root' || data.name.includes('backstage-e2e')) {
return path;
}
} catch (error) {
throw new Error(
`Failed to parse package.json file while searching for root, ${error}`,
);
}
}
const newPath = dirname(path);
if (newPath === path) {
throw new Error(
`No package.json with name "root" found as a parent of ${topPath}`,
);
}
path = newPath;
}
throw new Error(
`Iteration limit reached when searching for root package.json at ${topPath}`,
);
}
// Finds the root of the cli package itself
export function findOwnDir() {
// Known relative locations of package in dist/dev
const pathDist = '..';
const pathDev = '../..';
// Check the closest dir first
const pkgInDist = resolvePath(__dirname, pathDist, 'package.json');
const isDist = fs.pathExistsSync(pkgInDist);
const path = isDist ? pathDist : pathDev;
return resolvePath(__dirname, path);
}
// Finds the root of the monorepo that the cli exists in. Only accessible when running inside Backstage repo.
export function findOwnRootPath(ownDir: string) {
const isLocal = fs.pathExistsSync(resolvePath(ownDir, 'src'));
if (!isLocal) {
throw new Error(
'Tried to access monorepo package root dir outside of Backstage repository',
);
}
return resolvePath(ownDir, '../..');
}
export function findPaths(): Paths {
const ownDir = findOwnDir();
const targetDir = fs.realpathSync(process.cwd());
// Lazy load this as it will throw an error if we're not inside the Backstage repo.
let ownRoot = '';
const getOwnRoot = () => {
if (!ownRoot) {
ownRoot = findOwnRootPath(ownDir);
}
return ownRoot;
};
// We're not always running in a monorepo, so we lazy init this to only crash commands
// that require a monorepo when we're not in one.
let targetRoot = '';
const getTargetRoot = () => {
if (!targetRoot) {
targetRoot = findRootPath(targetDir);
}
return targetRoot;
};
return {
ownDir,
get ownRoot() {
return getOwnRoot();
},
targetDir,
get targetRoot() {
return getTargetRoot();
},
resolveOwn: (...paths) => resolvePath(ownDir, ...paths),
resolveOwnRoot: (...paths) => resolvePath(getOwnRoot(), ...paths),
resolveTarget: (...paths) => resolvePath(targetDir, ...paths),
resolveTargetRoot: (...paths) => resolvePath(getTargetRoot(), ...paths),
};
}
export const paths = findPaths();
+26
View File
@@ -0,0 +1,26 @@
/*
* Copyright 2020 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 fs from 'fs-extra';
import { paths } from './paths';
export function findVersion() {
const pkgContent = fs.readFileSync(paths.resolveOwn('package.json'), 'utf8');
return JSON.parse(pkgContent).version;
}
export const version = findVersion();
export const isDev = fs.pathExistsSync(paths.resolveOwn('src'));