diff --git a/packages/techdocs-cli/README.md b/packages/techdocs-cli/README.md index 8d86363c12..f751497e83 100644 --- a/packages/techdocs-cli/README.md +++ b/packages/techdocs-cli/README.md @@ -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: diff --git a/packages/techdocs-cli/bin/build b/packages/techdocs-cli/bin/build index c0a1cacdfc..a86954ace9 100755 --- a/packages/techdocs-cli/bin/build +++ b/packages/techdocs-cli/bin/build @@ -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 diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index c8985367b0..97c5cbe049 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -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" } } diff --git a/packages/techdocs-cli/src/index.ts b/packages/techdocs-cli/src/index.ts index f9ab1c45de..5b4f947a73 100644 --- a/packages/techdocs-cli/src/index.ts +++ b/packages/techdocs-cli/src/index.ts @@ -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 => { + 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); diff --git a/packages/techdocs-cli/src/lib/httpServer.ts b/packages/techdocs-cli/src/lib/httpServer.ts index 6ca02fc7e9..6e6578840e 100644 --- a/packages/techdocs-cli/src/lib/httpServer.ts +++ b/packages/techdocs-cli/src/lib/httpServer.ts @@ -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 { + return new Promise((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); + }); }); } } diff --git a/yarn.lock b/yarn.lock index fbdde05008..0fb5368314 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"