techdocs: implement serve for the techdocs-cli (#1581)
* feat(techdocs-cli): point techdocs export to localhost:3000/api * feat(techdocs-cli): add http-proxy package * feat(techdocs-cli): add react-dev-utils package * feat(techdocs-cli): implement serve and serve:mkdocs commands * fix: use [module] prefix convention in console.log * docs: added commands * wip * Fix type errors Co-authored-by: Sebastian Qvarfordt <s.qvarfordt@gmail.com>
This commit is contained in:
@@ -4,6 +4,16 @@ Check out the [TechDocs README](https://github.com/spotify/backstage/blob/master
|
||||
|
||||
**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).**
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Serve localhost:3000 (and localhost:8000)
|
||||
yarn serve
|
||||
|
||||
# Serve localhost:8000 containing your Mkdocs documentation.
|
||||
yarn serve:mkdocs
|
||||
```
|
||||
|
||||
## 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:
|
||||
|
||||
@@ -22,7 +22,7 @@ TECHDOCS_PREVIEW_DEST=$ROOT_DIR/packages/techdocs-cli/dist/techdocs-preview-bund
|
||||
backstage-cli build --outputs cjs
|
||||
|
||||
# Create export of the TechDocs plugin
|
||||
yarn workspace @backstage/plugin-techdocs export
|
||||
APP_CONFIG_techdocs_storageUrl='"http://localhost:3000/api"' yarn workspace @backstage/plugin-techdocs export
|
||||
|
||||
# Copy over export to techdocs-cli dist/ folder
|
||||
cp -r $TECHDOCS_PREVIEW_SOURCE $TECHDOCS_PREVIEW_DEST
|
||||
|
||||
@@ -49,6 +49,8 @@
|
||||
"chalk": "^4.1.0",
|
||||
"commander": "^5.1.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
"http-proxy": "^1.18.1",
|
||||
"react-dev-utils": "^10.2.1",
|
||||
"serve-handler": "^6.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,17 +13,28 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
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';
|
||||
import HTTPServer from './lib/httpServer';
|
||||
import openBrowser from 'react-dev-utils/openBrowser';
|
||||
|
||||
const run = (workingDirectory: string, name: string, args: string[] = []) => {
|
||||
const child = spawn(name, args, {
|
||||
const run = (
|
||||
workingDirectory: string,
|
||||
name: string,
|
||||
args: string[] = [],
|
||||
): ChildProcess => {
|
||||
const [stdin, stdout, stderr] = [
|
||||
'inherit' as const,
|
||||
'pipe' as const,
|
||||
'inherit' as const,
|
||||
];
|
||||
|
||||
const childProcess = spawn(name, args, {
|
||||
cwd: workingDirectory,
|
||||
stdio: ['inherit', 'inherit', 'inherit'],
|
||||
stdio: [stdin, stdout, stderr],
|
||||
shell: true,
|
||||
env: {
|
||||
...process.env,
|
||||
@@ -31,56 +42,87 @@ const run = (workingDirectory: string, name: string, args: string[] = []) => {
|
||||
},
|
||||
});
|
||||
|
||||
child.once('error', error => {
|
||||
childProcess.once('error', error => {
|
||||
console.error(error);
|
||||
childProcess.kill();
|
||||
});
|
||||
child.once('exit', code => {
|
||||
console.log('exited!', code);
|
||||
|
||||
childProcess.once('exit', () => {
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
return childProcess;
|
||||
};
|
||||
|
||||
const runMkdocsServer = (options?: {
|
||||
devAddr: string;
|
||||
}): Promise<ChildProcess> => {
|
||||
const devAddr = options?.devAddr ?? '0.0.0.0:8000';
|
||||
|
||||
return new Promise(resolve => {
|
||||
const childProcess = run(process.env.PWD!, 'docker', [
|
||||
'run',
|
||||
'-it',
|
||||
'-w',
|
||||
'/content',
|
||||
'-v',
|
||||
'$(pwd):/content',
|
||||
'-p',
|
||||
'8000:8000',
|
||||
'mkdocs:local-dev',
|
||||
'serve',
|
||||
'-a',
|
||||
devAddr,
|
||||
]);
|
||||
|
||||
childProcess.stdout?.on('data', rawData => {
|
||||
const data = rawData.toString().split('\n')[0];
|
||||
console.log('[mkdocs] ', data);
|
||||
|
||||
if (data.includes(`Serving on http://${devAddr}`)) {
|
||||
resolve(childProcess);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const main = (argv: string[]) => {
|
||||
program.name('techdocs-cli').version(version);
|
||||
|
||||
program
|
||||
.command('serve:mkdocs')
|
||||
.description('Serve a documentation project locally')
|
||||
.action(() => {
|
||||
runMkdocsServer().then(() => {
|
||||
openBrowser('http://localhost:8000');
|
||||
});
|
||||
});
|
||||
|
||||
program
|
||||
.command('serve')
|
||||
.description('Serve a documentation project locally')
|
||||
.action(() => {
|
||||
// const techdocsPreviewBundlePath = path.join(
|
||||
// __dirname,
|
||||
// '..',
|
||||
// 'dist',
|
||||
// 'techdocs-preview-bundle',
|
||||
// );
|
||||
// Mkdocs server
|
||||
const mkdocsServer = runMkdocsServer();
|
||||
|
||||
// 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'),
|
||||
'..',
|
||||
// Local Backstage Preview
|
||||
const techdocsPreviewBundlePath = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'dist',
|
||||
'techdocs-preview-bundle',
|
||||
);
|
||||
|
||||
run(
|
||||
pluginPath,
|
||||
path.join(require.resolve('@backstage/cli'), '../../bin/backstage-cli'),
|
||||
['plugin:serve'],
|
||||
);
|
||||
const httpServer = new HTTPServer(techdocsPreviewBundlePath, 3000)
|
||||
.serve()
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
mkdocsServer.then(childProcess => childProcess.kill());
|
||||
});
|
||||
|
||||
Promise.all([mkdocsServer, httpServer]).then(() => {
|
||||
openBrowser('http://localhost:3000/docs/local-dev/');
|
||||
});
|
||||
});
|
||||
|
||||
program.parse(argv);
|
||||
|
||||
@@ -16,20 +16,64 @@
|
||||
|
||||
import serveHandler from 'serve-handler';
|
||||
import http from 'http';
|
||||
import httpProxy from 'http-proxy';
|
||||
|
||||
export default class HTTPServer {
|
||||
constructor(public dir: string, public port: number) {}
|
||||
proxyEndpoint: string;
|
||||
|
||||
serve() {
|
||||
const server = http.createServer((request, response) => {
|
||||
return serveHandler(request, response, {
|
||||
public: this.dir,
|
||||
trailingSlash: true,
|
||||
});
|
||||
constructor(public dir: string, public port: number) {
|
||||
this.proxyEndpoint = '/api/';
|
||||
}
|
||||
|
||||
private createProxy() {
|
||||
const proxy = httpProxy.createProxyServer({
|
||||
target: 'http://localhost:8000',
|
||||
});
|
||||
|
||||
server.listen(this.port, () => {
|
||||
console.log('Running at http://localhost:3000');
|
||||
return (request: http.IncomingMessage): [httpProxy, string] => {
|
||||
const [, ...pathChunks] =
|
||||
request.url?.substring(this.proxyEndpoint.length).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);
|
||||
});
|
||||
|
||||
request.url = forwardPath;
|
||||
return proxy.web(request, response);
|
||||
}
|
||||
|
||||
return serveHandler(request, response, {
|
||||
public: this.dir,
|
||||
trailingSlash: true,
|
||||
rewrites: [{ source: '**', destination: 'index.html' }],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
server.listen(this.port, () => {
|
||||
console.log(
|
||||
'[techdocs-preview-bundle] Running local version of Backstage at http://localhost:3000',
|
||||
);
|
||||
resolve(server);
|
||||
});
|
||||
|
||||
server.on('error', (error: Error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10182,6 +10182,15 @@ http-proxy@^1.17.0:
|
||||
follow-redirects "^1.0.0"
|
||||
requires-port "^1.0.0"
|
||||
|
||||
http-proxy@^1.18.1:
|
||||
version "1.18.1"
|
||||
resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
|
||||
integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
|
||||
dependencies:
|
||||
eventemitter3 "^4.0.0"
|
||||
follow-redirects "^1.0.0"
|
||||
requires-port "^1.0.0"
|
||||
|
||||
http-signature@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
|
||||
|
||||
Reference in New Issue
Block a user