Skip to content

Commit 4b75655

Browse files
committed
feat(lesy-plugin-pilot): support multiple projects
1 parent 45c5c8a commit 4b75655

File tree

4 files changed

+206
-85
lines changed

4 files changed

+206
-85
lines changed

packages/plugins/lesy-plugin-pilot/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
"json-stringify-safe": "^5.0.1",
3030
"rxjs": "^6.5.3",
3131
"serve-static": "^1.14.1",
32-
"ws": "^7.1.2"
32+
"ws": "^7.1.2",
33+
"search-in-file": "^1.2.2"
3334
},
3435
"publishConfig": {
3536
"access": "public"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export interface LesyWorkSpace {
2+
[key: string]: string;
3+
}
4+
5+
export interface SocketInputData {
6+
requestSwitchProject: (name: string) => any;
7+
requestRunCommand: () => any;
8+
requestProject: () => any;
9+
requestAllProjects: () => any;
10+
requestAllCommands: () => any;
11+
requestConfig: () => any;
12+
}
13+
14+
export interface AnyObject {
15+
[key: string]: any;
16+
}
Lines changed: 181 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
1+
import { LesyWorkSpace, SocketInputData, AnyObject } from "./models";
12
import { resolve } from "path";
3+
import { readdirSync, existsSync } from "fs";
24
import { createServer } from "http";
3-
/* move inside to run method */
4-
import finalhandler from "finalhandler";
5-
import serveStatic from "serve-static";
6-
import { default as intercept } from "intercept-stdout";
7-
const AU = require("ansi_up");
8-
const ansiUp = new AU.default();
9-
ansiUp.use_classes = true;
10-
ansiUp.escape_for_html = false;
11-
import { take, filter, map } from "rxjs/operators";
12-
import { printAddressBanner } from "./helpers/banner";
13-
import { getLocalIPAddress } from "./helpers/public-ip";
14-
15-
export default {
16-
name: "pilot",
17-
description: "Run commands from GUI",
18-
aliases: ["server", "web", "s", "w"],
19-
isVisible: false,
20-
flags: {
5+
6+
declare global {
7+
module NodeJS {
8+
interface Global {
9+
lesyWorkspace: LesyWorkSpace;
10+
lesySelectedProject: string;
11+
}
12+
}
13+
}
14+
15+
export default class PilotCommand {
16+
name = "pilot";
17+
description = "Run commands from GUI";
18+
aliases = ["server", "web", "s", "w"];
19+
isVisible = false;
20+
flags = {
2121
host: {
2222
aliases: ["h"],
2323
},
@@ -33,90 +33,194 @@ export default {
3333
clientSocketUrl: {
3434
aliases: ["csu"],
3535
},
36-
},
37-
run: ({ flags: props, request, feature, config, utils }) => {
38-
const flags = {
39-
host: "localhost",
40-
port: "8888",
41-
socketHost: "localhost",
42-
socketPort: "8889", // todo: available ports
43-
...props,
36+
};
37+
private socket: any;
38+
private localNetworkIP: string;
39+
private isOffline: boolean;
40+
41+
private getBinFiles(dir: string) {
42+
const files = readdirSync(dir);
43+
return files
44+
.map((filePath: string) => resolve(dir, filePath))
45+
.filter((filePath: string) => existsSync(filePath));
46+
}
47+
48+
private async fetchProjectPaths(): Promise<string[]> {
49+
const execa = require("execa");
50+
const fileSearch = require("search-in-file").fileSearch;
51+
const { stdout: binPath } = await execa("npm", ["bin", "-g"]);
52+
try {
53+
const results = await fileSearch(
54+
this.getBinFiles(binPath),
55+
"@lesy/compiler",
56+
);
57+
return results;
58+
} catch ({ message }) {
59+
console.error(message);
60+
}
61+
}
62+
63+
private async loadProjectData(currCtx: any): Promise<LesyWorkSpace> {
64+
global.lesyWorkspace = {
65+
[currCtx.feature.pkg.name]: currCtx,
4466
};
45-
const chalk = utils.color();
46-
const { "@lesy/lesy-plugin-pilot": pilotConfig, ...allConfig } = config;
67+
global.lesySelectedProject = currCtx.feature.pkg.name;
68+
const projectsPaths = await this.fetchProjectPaths();
69+
for (let i = 0; i < projectsPaths.length; i = i + 1) {
70+
const p = await require(projectsPaths[i]).default;
71+
global.lesyWorkspace[p.feature.pkg.name] = p.localState;
72+
}
73+
return global.lesyWorkspace;
74+
}
75+
76+
private getSelectedProject(): any {
77+
return global.lesyWorkspace[global.lesySelectedProject];
78+
}
79+
80+
private getAllProjects(): any[] {
81+
const projectNames = Object.keys(global.lesyWorkspace);
82+
return projectNames.map((name: string) => ({ name }));
83+
}
84+
85+
private switchProject(name: string): void {
86+
global.lesySelectedProject = name;
87+
}
88+
89+
private getConfig(): AnyObject {
90+
const { feature, config } = this.getSelectedProject();
4791
const { name, version, bin } = feature.pkg;
4892
const defaultPilotConfig = {
4993
appName: name || "Dashboard",
5094
docTitle: name || "Dashboard",
5195
appVersion: version || "0.0",
5296
cmdName: Object.keys(bin)[0],
5397
};
54-
const socket = feature.socket
55-
.startServer(flags.socketHost, flags.socketPort)
56-
.init({
57-
requestRunCommand: request.runCommand,
58-
requestAllCommands: () => ({
59-
onRequestAllCommands: request.getCommands(),
60-
}),
61-
requestConfig: () => ({
62-
onRequestConfig: {
63-
pilot: { ...defaultPilotConfig, ...pilotConfig },
64-
...allConfig,
65-
},
66-
}),
98+
const { "@lesy/lesy-plugin-pilot": pilotConfig, ...allConfig } = config;
99+
return {
100+
pilot: { ...defaultPilotConfig, ...pilotConfig },
101+
...allConfig,
102+
};
103+
}
104+
105+
private getSocketData(): SocketInputData {
106+
const selectedProject = () =>
107+
this.getAllProjects().find(
108+
(p: any) => p.name === global.lesySelectedProject,
109+
);
110+
111+
return {
112+
requestSwitchProject: this.switchProject,
113+
requestRunCommand: this.getSelectedProject().request.runCommand,
114+
requestProject: () => ({
115+
onRequestProject: selectedProject(),
116+
}),
117+
requestAllProjects: () => ({
118+
onRequestAllProjects: this.getAllProjects(),
119+
}),
120+
requestAllCommands: () => ({
121+
onRequestAllCommands: this.getSelectedProject().request.getCommands(),
122+
}),
123+
requestConfig: () => ({
124+
onRequestConfig: this.getConfig(),
125+
}),
126+
};
127+
}
128+
129+
private tapLogs() {
130+
const intercept = require("intercept-stdout");
131+
const AU = require("ansi_up");
132+
const ansiUp = new AU.default();
133+
ansiUp.use_classes = true;
134+
ansiUp.escape_for_html = false;
135+
136+
const cb = (text: any) => {
137+
this.socket.sendMessage({
138+
type: "log",
139+
message: ansiUp.ansi_to_html(text),
67140
});
141+
};
68142

69-
intercept(
70-
(txt: unknown) => {
71-
socket.sendMessage({
72-
type: "log",
73-
message: ansiUp.ansi_to_html(txt),
74-
});
75-
},
76-
(err: unknown) => {
77-
socket.sendMessage({
78-
type: "log",
79-
message: ansiUp.ansi_to_html(err),
80-
});
81-
},
82-
);
143+
intercept(cb, cb);
144+
}
83145

84-
if (feature.promptConfig) {
85-
feature.promptConfig.customPrompt = (questions: any) => {
86-
socket.sendMessage({ questions, messageId: 123 });
87-
return new Promise((res: any) => {
88-
socket
89-
.listen()
90-
.pipe(
91-
take(1),
92-
filter((data: any) => data.REQUEST === "answers"),
93-
map((a: any) => a.PAYLOAD),
94-
filter((data: any) => data.qid === 123),
95-
map((a: any) => a.ans),
96-
)
97-
.subscribe((a: any) => res(a)); // todo: refactor
98-
});
99-
};
100-
}
146+
private hijackPrompt(feature: AnyObject) {
147+
if (!feature.promptConfig) return;
148+
const { take, filter, map } = require("rxjs/operators");
149+
feature.promptConfig.customPrompt = (questions: any) => {
150+
this.socket.sendMessage({ questions, messageId: 123 });
151+
return new Promise((res: any) => {
152+
this.socket
153+
.listen()
154+
.pipe(
155+
take(1),
156+
filter((data: any) => data.REQUEST === "answers"),
157+
map((a: any) => a.PAYLOAD),
158+
filter((data: any) => data.qid === 123),
159+
map((a: any) => a.ans),
160+
)
161+
.subscribe((a: any) => res(a)); // todo: refactor
162+
});
163+
};
164+
}
165+
166+
private getMergedFlags(flags: AnyObject): AnyObject {
167+
const f = {
168+
host: "localhost",
169+
port: "8888",
170+
socketHost: "localhost",
171+
socketPort: "8889", // todo: available ports
172+
...flags,
173+
};
174+
return f;
175+
}
176+
177+
private establishSocket(flags: AnyObject, feature: AnyObject): void {
178+
this.socket = feature.socket
179+
.startServer(flags.socketHost, flags.socketPort)
180+
.init(this.getSocketData());
181+
}
101182

102-
const localNetworkIP = getLocalIPAddress() || "localhost";
103-
const isOffline = localNetworkIP === "localhost";
183+
private startServer(flags: AnyObject) {
184+
const { getLocalIPAddress } = require("./helpers/public-ip");
185+
const finalhandler = require("finalhandler");
186+
const serveStatic = require("serve-static");
187+
188+
this.localNetworkIP = getLocalIPAddress() || "localhost";
189+
this.isOffline = this.localNetworkIP === "localhost";
104190
const dir = resolve(__dirname, "../public");
105191
let cookies = ["socketHost", "socketPort", "clientSocketUrl"];
106192
cookies = cookies.map((c: any) => `${c}=${flags[c]}`);
193+
107194
const serve = serveStatic(dir, {
108195
index: ["index.html"],
109196
setHeaders: (res: any) => res.setHeader("Set-Cookie", cookies),
110197
});
111198
const server = createServer((req, res) => {
112199
serve(req, res, finalhandler(req, res));
113200
});
201+
114202
server.listen(flags.port, flags.host);
203+
}
115204

205+
private renderConnectionInfo(utils: AnyObject, flags: AnyObject) {
206+
const { printAddressBanner } = require("./helpers/banner");
207+
const chalk = utils.color();
116208
printAddressBanner(
117209
chalk,
118210
`http://${flags.host}:${flags.port}`,
119-
`http://${localNetworkIP}:${flags.port} ${isOffline ? "(Offline)" : ""}`,
211+
`http://${this.localNetworkIP}:${flags.port} ${
212+
this.isOffline ? "(Offline)" : ""
213+
}`,
120214
);
121-
},
122-
};
215+
}
216+
217+
async run({ flags: f, request, feature, config, utils }) {
218+
const flags = this.getMergedFlags(f);
219+
await this.loadProjectData({ request, feature, config });
220+
this.tapLogs();
221+
this.hijackPrompt(feature);
222+
this.establishSocket(flags, feature);
223+
this.startServer(flags);
224+
this.renderConnectionInfo(utils, flags);
225+
}
226+
}

packages/plugins/lesy-plugin-pilot/src/pilot.socket.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,35 @@
11
import { Server } from "ws";
22
import { Subject } from "rxjs";
3-
const stringify = require("json-stringify-safe");
3+
import jsonStringifySafe from "json-stringify-safe";
44

55
export class WebSocketBus {
66
private ws: Server;
77
private receiver: Subject<any> = new Subject();
88
private sender: Subject<any> = new Subject();
9-
private requests;
9+
private requests: object;
1010
constructor() {}
1111

12-
startServer(host, port) {
12+
startServer(host: string, port: number) {
1313
this.ws = new Server({ host, port });
1414
this.ws.on("open", this.onConnectionOpen);
1515
this.ws.on("close", this.onConnectionClosed);
1616
return this;
1717
}
1818

19-
init(requests) {
19+
init(requests: object) {
2020
this.requests = requests;
2121
this.ws.on("connection", (ws: any) => {
2222
this.sender.subscribe((a: any) => {
2323
ws.send(JSON.stringify(a));
2424
});
25-
ws.on("message", message =>
25+
ws.on("message", (message: string) =>
2626
this.onMessageReceived.call(this, message, ws),
2727
);
2828
});
2929
return this;
3030
}
3131

32-
sendMessage(message) {
32+
sendMessage(message: string) {
3333
this.sender.next(message);
3434
}
3535

@@ -54,6 +54,6 @@ export class WebSocketBus {
5454
this.receiver.next(data);
5555
if (!this.requests[data.REQUEST]) return;
5656
const response = await this.requests[data.REQUEST](data.PAYLOAD);
57-
if (response) ws.send(stringify(response));
57+
if (response) ws.send(jsonStringifySafe(response));
5858
}
5959
}

0 commit comments

Comments
 (0)