Skip to content

Commit 3e24202

Browse files
fmvilasmagicmatatjahuboyney123
authored
feat: add live server support to connect with CLI (#141)
* feat: add live server support to connect with CLI * do not hardcode localhost * add bidirectional communication * fix linter errors * add cross-env dep * add notifications for live server connection and icon in terminal * fix linter errors * change Introduction to Information * change Introduction to Information * Remove console.log * Add ability to send multiple types of messages over WS server * Fix linter * Remove .DS_Store and add it to .gitignore * Apply suggestions from code review Co-authored-by: David Boyne <boyneyy123@gmail.com> Co-authored-by: Matatjahu <urbanczyk.maciej.95@gmail.com> Co-authored-by: David Boyne <boyneyy123@gmail.com>
1 parent 4faf65d commit 3e24202

File tree

11 files changed

+148
-30
lines changed

11 files changed

+148
-30
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1+
.DS_Store
12
/node_modules
23
.vscode/
34
/lib
4-
/build
5+
/build

package-lock.json

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@
4747
"web-vitals": "^1.1.2"
4848
},
4949
"scripts": {
50-
"start": "react-scripts start",
51-
"build": "react-scripts build",
50+
"start": "cross-env DISABLE_ESLINT_PLUGIN=true react-scripts start",
51+
"build": "cross-env DISABLE_ESLINT_PLUGIN=true react-scripts build",
5252
"test": "npm run test:unit",
53-
"test:unit": "react-scripts test --detectOpenHandles",
53+
"test:unit": "cross-env DISABLE_ESLINT_PLUGIN=true react-scripts test --detectOpenHandles",
5454
"eject": "react-scripts eject",
5555
"lint": "eslint --max-warnings 0 --config .eslintrc .",
5656
"lint:fix": "eslint --max-warnings 0 --config .eslintrc . --fix",
@@ -86,12 +86,13 @@
8686
"@types/react": "^17.0.24",
8787
"@types/react-dom": "^17.0.9",
8888
"conventional-changelog-conventionalcommits": "^4.6.1",
89+
"cross-env": "^7.0.3",
8990
"eslint": "^7.32.0",
9091
"eslint-plugin-react": "7.26.1",
9192
"eslint-plugin-security": "^1.4.0",
9293
"eslint-plugin-sonarjs": "^0.10.0",
93-
"react-scripts": "4.0.3",
9494
"markdown-toc": "^1.2.0",
95+
"react-scripts": "4.0.3",
9596
"semantic-release": "^18.0.0",
9697
"typescript": "^4.4.3"
9798
},

src/components/Editor/MonacoWrapper.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export const MonacoWrapper: React.FunctionComponent<MonacoWrapperProps> = ({
5050
}
5151

5252
const onChange = debounce((v: string) => {
53-
EditorService.updateState(v);
53+
EditorService.updateState({ content: v });
5454
SpecificationService.parseSpec(v);
5555
}, 250);
5656

src/components/Navigation.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -305,18 +305,18 @@ export const Navigation: React.FunctionComponent<NavigationProps> = ({
305305
<li className="mb-4">
306306
<div
307307
className={`p-2 pl-3 text-white cursor-pointer hover:bg-gray-900 ${
308-
hash === 'introduction' ? 'bg-gray-900' : ''
308+
hash === 'information' ? 'bg-gray-900' : ''
309309
}`}
310310
onClick={() =>
311311
NavigationService.scrollTo(
312312
'/info',
313313
rawSpec,
314-
'introduction',
314+
'information',
315315
language,
316316
)
317317
}
318318
>
319-
Introduction
319+
Information
320320
</div>
321321
</li>
322322
{spec.hasServers() && (

src/components/Terminal/TerminalInfo.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import React from 'react';
2+
import { VscRadioTower } from 'react-icons/vsc';
23

34
import { SpecificationService } from '../../services';
45
import state from '../../state';
56

67
interface TerminalInfoProps {}
78

89
export const TerminalInfo: React.FunctionComponent<TerminalInfoProps> = () => {
10+
const appState = state.useAppState();
911
const editorState = state.useEditorState();
1012
const parserState = state.useParserState();
1113

14+
const liveServer = appState.liveServer.get();
1215
const actualVersion = parserState.parsedSpec.get()?.version() || '2.0.0';
1316
const latestVersion = SpecificationService.getLastVersion();
1417
const documentValid = parserState.valid.get();
@@ -25,8 +28,16 @@ export const TerminalInfo: React.FunctionComponent<TerminalInfoProps> = () => {
2528

2629
return (
2730
<div className="flex flex-row px-2">
31+
{liveServer && (
32+
<div className="flex flex-row content-center ml-3">
33+
<span className="inline-block mr-2">
34+
<VscRadioTower className="w-4 h-4 text-yellow-500" />
35+
</span>
36+
<span>Live server</span>
37+
</div>
38+
)}
2839
{errors.length ? (
29-
<div>
40+
<div className="ml-3">
3041
<span className="text-red-500">
3142
<svg
3243
xmlns="http://www.w3.org/2000/svg"
@@ -44,7 +55,7 @@ export const TerminalInfo: React.FunctionComponent<TerminalInfoProps> = () => {
4455
<span>Invalid</span>
4556
</div>
4657
) : (
47-
<div>
58+
<div className="ml-3">
4859
<span className="text-green-500">
4960
<svg
5061
xmlns="http://www.w3.org/2000/svg"

src/services/editor.service.ts

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,17 @@ import { FormatService } from './format.service';
55
import { SpecificationService } from './specification.service';
66

77
import state from '../state';
8+
import { SocketClient } from './socket-client.service';
89

910
export type AllowedLanguages = 'json' | 'yaml' | 'yml';
1011

12+
export interface UpdateState {
13+
content: string,
14+
updateModel?: boolean,
15+
sendToServer?: boolean,
16+
language?: AllowedLanguages,
17+
}
18+
1119
export class EditorService {
1220
static getInstance(): monacoAPI.editor.IStandaloneCodeEditor {
1321
return window.Editor;
@@ -18,12 +26,17 @@ export class EditorService {
1826
.getModel()?.getValue() as string;
1927
}
2028

21-
static updateState(
22-
content: string,
29+
static updateState({
30+
content,
2331
updateModel = false,
24-
language?: AllowedLanguages,
25-
) {
26-
if (!content) {
32+
sendToServer = true,
33+
language,
34+
}: UpdateState) {
35+
if (state.editor.editorValue.get() === content) {
36+
return;
37+
}
38+
39+
if (!content && typeof content !== 'string') {
2740
return;
2841
}
2942

@@ -44,6 +57,10 @@ export class EditorService {
4457
}
4558
state.editor.editorValue.set(content);
4659

60+
if (sendToServer) {
61+
SocketClient.send('file:update', { code: content });
62+
}
63+
4764
if (updateModel) {
4865
const instance = this.getInstance();
4966
if (instance) {
@@ -58,7 +75,7 @@ export class EditorService {
5875
this.getValue(),
5976
version || SpecificationService.getLastVersion(),
6077
);
61-
this.updateState(converted, true);
78+
this.updateState({ content: converted, updateModel: true });
6279
}
6380

6481
static async importFromURL(url: string): Promise<void> {
@@ -67,7 +84,7 @@ export class EditorService {
6784
.then(res => res.text())
6885
.then(text => {
6986
state.editor.documentFrom.set(`URL: ${url}` as `URL: ${string}`);
70-
this.updateState(text, true);
87+
this.updateState({ content: text, updateModel: true });
7188
})
7289
.catch(err => {
7390
console.error(err);
@@ -89,7 +106,7 @@ export class EditorService {
89106
fileReader.onload = fileLoadedEvent => {
90107
const content = fileLoadedEvent.target?.result;
91108
console.log(content);
92-
this.updateState(String(content), true);
109+
this.updateState({ content: String(content), updateModel: true });
93110
};
94111
fileReader.readAsText(file, 'UTF-8');
95112
}
@@ -98,7 +115,7 @@ export class EditorService {
98115
try {
99116
const decoded = FormatService.decodeBase64(content);
100117
state.editor.documentFrom.set('Base64');
101-
this.updateState(decoded, true);
118+
this.updateState({ content: String(decoded), updateModel: true });
102119
} catch (err) {
103120
console.error(err);
104121
throw err;
@@ -108,7 +125,7 @@ export class EditorService {
108125
static async convertToYaml() {
109126
try {
110127
const yamlContent = FormatService.convertToYaml(this.getValue());
111-
yamlContent && this.updateState(yamlContent, true, 'yaml');
128+
yamlContent && this.updateState({ content: yamlContent, updateModel: true, language: 'yaml' });
112129
} catch (err) {
113130
console.error(err);
114131
throw err;
@@ -118,7 +135,7 @@ export class EditorService {
118135
static async convertToJSON() {
119136
try {
120137
const jsonContent = FormatService.convertToJSON(this.getValue());
121-
jsonContent && this.updateState(jsonContent, true, 'json');
138+
jsonContent && this.updateState({ content: jsonContent, updateModel: true, language: 'json' });
122139
} catch (err) {
123140
console.error(err);
124141
throw err;
@@ -214,7 +231,6 @@ export class EditorService {
214231
}
215232

216233
private static fileName = 'asyncapi';
217-
218234
private static downloadFile(content: string, fileName: string) {
219235
return fileDownload(content, fileName);
220236
}

src/services/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ export * from './editor.service';
22
export * from './format.service';
33
export * from './monaco.service';
44
export * from './navigation.service';
5+
export * from './socket-client.service';
56
export * from './specification.service';

src/services/navigation.service.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { getLocationOf } from '@asyncapi/parser/lib/utils';
33

44
import { EditorService } from './editor.service';
5+
import { SocketClient } from './socket-client.service';
56
import { SpecificationService } from './specification.service';
67
import state from '../state';
78

@@ -84,19 +85,17 @@ export class NavigationService {
8485

8586
const documentUrl = urlParams.get('url') || urlParams.get('load');
8687
const base64Document = urlParams.get('base64');
88+
const liveServerPort = urlParams.get('liveServer');
8789

88-
if (!documentUrl && !base64Document) {
89-
state.app.initialized.set(true);
90-
return;
91-
}
92-
93-
if (documentUrl) {
90+
if (liveServerPort && typeof Number(liveServerPort) === 'number') {
91+
SocketClient.connect(window.location.hostname, liveServerPort);
92+
} else if (documentUrl) {
9493
await EditorService.importFromURL(documentUrl);
9594
} else if (base64Document) {
9695
await EditorService.importBase64(base64Document);
9796
}
9897

99-
if (this.isReadOnly()) {
98+
if (this.isReadOnly(true)) {
10099
await SpecificationService.parseSpec(state.editor.editorValue.get());
101100
state.sidebar.show.set(false);
102101
state.editor.set({
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import toast from 'react-hot-toast';
2+
3+
import { EditorService } from './editor.service';
4+
5+
import state from '../state';
6+
7+
interface IncomingMessage {
8+
type: 'file:loaded' | 'file:changed' | 'file:deleted';
9+
code?: string;
10+
}
11+
12+
export class SocketClient {
13+
private static ws: WebSocket;
14+
15+
static connect(hostname: string, port: string | number) {
16+
try {
17+
const ws = this.ws = new WebSocket(`ws://${hostname || 'localhost'}:${port}/live-server`);
18+
19+
ws.onopen = this.onOpen;
20+
ws.onmessage = this.onMessage;
21+
ws.onerror = this.onError;
22+
} catch (e) {
23+
console.error(e);
24+
this.onError();
25+
}
26+
}
27+
28+
static send(eventName: string, content: Record<string, unknown>) {
29+
this.ws && this.ws.send(JSON.stringify({ type: eventName, ...content }));
30+
}
31+
32+
private static onMessage(event: MessageEvent<any>) {
33+
try {
34+
const json: IncomingMessage = JSON.parse(event.data);
35+
36+
switch (json.type) {
37+
case 'file:loaded':
38+
case 'file:changed':
39+
EditorService.updateState({
40+
content: json.code as string,
41+
updateModel: true,
42+
sendToServer: false,
43+
});
44+
break;
45+
case 'file:deleted':
46+
console.warn('Live Server: The file has been deleted on the file system.');
47+
break;
48+
default:
49+
console.warn('Live Server: An unknown even has been received. See details:');
50+
console.log(json);
51+
}
52+
} catch (e) {
53+
console.error(`Live Server: An invalid event has been received. See details:\n${event.data}`);
54+
}
55+
}
56+
57+
private static onOpen() {
58+
toast.success(
59+
<div>
60+
<span className="block text-bold">
61+
Correctly connected to the live server!
62+
</span>
63+
</div>
64+
);
65+
state.app.liveServer.set(true);
66+
}
67+
68+
private static onError() {
69+
toast.error(
70+
<div>
71+
<span className="block text-bold">
72+
Failed to connect to live server. Please check developer console for more information.
73+
</span>
74+
</div>
75+
);
76+
state.app.liveServer.set(false);
77+
}
78+
}

0 commit comments

Comments
 (0)