Skip to content

Commit

Permalink
feat, introduce an API Server to run commands through HTTP server (#7056
Browse files Browse the repository at this point in the history
)
  • Loading branch information
davidfirst authored Feb 17, 2023
1 parent a5a6aa7 commit 7ae80f6
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 3 deletions.
9 changes: 8 additions & 1 deletion .bitmap
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,13 @@
"mainFile": "index.ts",
"rootDir": "scopes/harmony/bit-error"
},
"api-server": {
"scope": "",
"version": "",
"defaultScope": "teambit.harmony",
"mainFile": "index.ts",
"rootDir": "scopes/harmony/api-server"
},
"builder": {
"scope": "teambit.pipelines",
"version": "0.0.988",
Expand Down Expand Up @@ -2440,4 +2447,4 @@
"rootDir": "scopes/dependencies/yarn"
},
"$schema-version": "15.0.0"
}
}
5 changes: 5 additions & 0 deletions scopes/harmony/api-server/api-server.aspect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Aspect } from '@teambit/harmony';

export const ApiServerAspect = Aspect.create({
id: 'teambit.harmony/api-server',
});
50 changes: 50 additions & 0 deletions scopes/harmony/api-server/api-server.main.runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { CLIAspect, CLIMain, MainRuntime } from '@teambit/cli';
import { ExpressAspect, ExpressMain } from '@teambit/express';
import { Logger, LoggerAspect, LoggerMain } from '@teambit/logger';
import WorkspaceAspect, { Workspace } from '@teambit/workspace';
import { ApiServerAspect } from './api-server.aspect';
import { CLIRoute } from './cli.route';
import { ServerCmd } from './server.cmd';

export class ApiServerMain {
constructor(private workspace: Workspace, private logger: Logger, private express: ExpressMain) {}

async runApiServer(options: { port: number }) {
const port = options.port || 3000;
await this.express.listen(port);

this.workspace.watcher
.watchAll({
preCompile: false,
})
.catch((err) => {
// don't throw an error, we don't want to break the "run" process
this.logger.error('watcher found an error', err);
});

// never ending promise to not exit the process (is there a better way?)
return new Promise(() => {
this.logger.consoleSuccess(`Bit Server is listening on port ${port}`);
});
}

static dependencies = [CLIAspect, WorkspaceAspect, LoggerAspect, ExpressAspect];
static runtime = MainRuntime;
static async provider([cli, workspace, loggerMain, express]: [CLIMain, Workspace, LoggerMain, ExpressMain]) {
const logger = loggerMain.createLogger(ApiServerAspect.id);
const apiServer = new ApiServerMain(workspace, logger, express);
cli.register(new ServerCmd(apiServer));

const cliRoute = new CLIRoute(logger, cli);
// register only when the workspace is available. don't register this on a remote-scope, for security reasons.
if (workspace) {
express.register([cliRoute]);
}

return apiServer;
}
}

ApiServerAspect.addRuntime(ApiServerMain);

export default ApiServerMain;
48 changes: 48 additions & 0 deletions scopes/harmony/api-server/cli.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { CLIMain } from '@teambit/cli';
import prettyTime from 'pretty-time';
import { Route, Request, Response } from '@teambit/express';
import { Logger } from '@teambit/logger';

/**
* example usage:
* post to http://localhost:3000/api/cli/list
* with the following json as the body
*
{
"args": ["teambit.workspace"],
"options": {
"ids": true
}
}
*/
export class CLIRoute implements Route {
constructor(private logger: Logger, private cli: CLIMain) {}

method = 'post';
route = '/cli/:cmd';

middlewares = [
async (req: Request, res: Response, next) => {
this.logger.debug(`cli server: got request for ${req.params.cmd}`);
try {
const command = this.cli.getCommand(req.params.cmd);
if (!command) throw new Error(`command "${req.params.cmd}" was not found`);
if (!command.json) throw new Error(`command "${req.params.cmd}" does not have a json method`);
const body = req.body;
const { args, options } = body;
const optsToString = Object.keys(options || {})
.map((key) => `--${key}`)
.join(' ');
this.logger.console(`started a new command: ${req.params.cmd} ${args.join(' ')} ${optsToString}`);
const startTask = process.hrtime();
const result = await command?.json(args || [], options || {});
const duration = prettyTime(process.hrtime(startTask));
this.logger.consoleSuccess(`command "${req.params.cmd}" had been completed in ${duration}`);
res.json(result);
} catch (err) {
this.logger.consoleFailure(`command "${req.params.cmd}" had failed`);
next(err);
}
},
];
}
5 changes: 5 additions & 0 deletions scopes/harmony/api-server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ApiServerAspect } from './api-server.aspect';

export type { ApiServerMain } from './api-server.main.runtime';
export default ApiServerAspect;
export { ApiServerAspect };
19 changes: 19 additions & 0 deletions scopes/harmony/api-server/server.cmd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// eslint-disable-next-line max-classes-per-file
import { Command, CommandOptions } from '@teambit/cli';
import { ApiServerMain } from './api-server.main.runtime';

export class ServerCmd implements Command {
name = 'server';
description = 'EXPERIMENTAL. communicate with bit cli program via http requests';
alias = '';
commands: Command[] = [];
group = 'general';
options = [['p', 'port [port]', 'port to run the server on']] as CommandOptions;

constructor(private apiServer: ApiServerMain) {}

async report(args, options: { port: number }): Promise<string> {
await this.apiServer.runApiServer(options);
return 'server is running successfully'; // should never get here, the previous line is blocking
}
}
2 changes: 2 additions & 0 deletions scopes/harmony/bit/manifests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ import { RemoveAspect } from '@teambit/remove';
import { MergeLanesAspect } from '@teambit/merge-lanes';
import { CheckoutAspect } from '@teambit/checkout';
import { APIReferenceAspect } from '@teambit/api-reference';
import { ApiServerAspect } from '@teambit/api-server';
import { ComponentWriterAspect } from '@teambit/component-writer';
import { TrackerAspect } from '@teambit/tracker';
import { MoverAspect } from '@teambit/mover';
Expand Down Expand Up @@ -196,6 +197,7 @@ export const manifestsMap = {
[CheckoutAspect.id]: CheckoutAspect,
[ComponentWriterAspect.id]: ComponentWriterAspect,
[APIReferenceAspect.id]: APIReferenceAspect,
[ApiServerAspect.id]: ApiServerAspect,
[TrackerAspect.id]: TrackerAspect,
[MoverAspect.id]: MoverAspect,
};
Expand Down
4 changes: 2 additions & 2 deletions scopes/harmony/cli/cli.cmd.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// eslint-disable-next-line max-classes-per-file
import { Command, CommandOptions } from '@teambit/cli';
import logger from '@teambit/legacy/dist/logger/logger';
import legacyLogger from '@teambit/legacy/dist/logger/logger';
import { handleErrorAndExit } from '@teambit/legacy/dist/cli/handle-errors';
import { loadConsumerIfExist } from '@teambit/legacy/dist/consumer';
import readline from 'readline';
Expand Down Expand Up @@ -46,7 +46,7 @@ export class CliCmd implements Command {
constructor(private cliMain: CLIMain, private docsDomain: string) {}

async report(): Promise<string> {
logger.isDaemon = true;
legacyLogger.isDaemon = true;
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
Expand Down
1 change: 1 addition & 0 deletions scopes/workspace/workspace/watch/watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export class Watcher {
}
watcher.on('ready', () => {
msgs?.onReady(this.workspace, this.trackDirs, this.verbose);
loader.stop();
});
// eslint-disable-next-line @typescript-eslint/no-misused-promises
watcher.on('change', async (filePath) => {
Expand Down

0 comments on commit 7ae80f6

Please sign in to comment.