Skip to content

Commit 7c57e37

Browse files
committed
feat: preview command added
1 parent af0c400 commit 7c57e37

File tree

2 files changed

+159
-0
lines changed

2 files changed

+159
-0
lines changed

src/commands/start/preview.ts

+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import Command from '../../core/base';
2+
import { start as startStudio } from '../../core/models/Studio';
3+
import { load } from '../../core/models/SpecificationFile';
4+
import bundle from '@asyncapi/bundler';
5+
import path from 'path';
6+
import fs from 'fs';
7+
import { Args } from '@oclif/core';
8+
import chokidar from 'chokidar';
9+
import { previewFlags } from '../../core/flags/start/preview.flags';
10+
import * as yaml from 'js-yaml';
11+
import { NO_CONTEXTS_SAVED } from '../../core/errors/context-error';
12+
13+
interface LocalRef {
14+
filePath: string;
15+
pointer: string | null;
16+
}
17+
18+
class StartPreview extends Command {
19+
static readonly description =
20+
'Starts Studio in preview mode with local reference files bundled and hot reloading enabled.';
21+
22+
static examples = [
23+
'asyncapi start preview',
24+
'asyncapi start preview ./asyncapi.yaml',
25+
'asyncapi start preview CONTEXT_NAME'
26+
];
27+
28+
static flags = previewFlags();
29+
30+
static args = {
31+
'spec-file': Args.string({
32+
description: 'spec path, url, or context-name',
33+
required: false,
34+
}),
35+
};
36+
37+
parseRef(ref: string): LocalRef {
38+
const [filePath, pointer] = ref.split('#');
39+
return {
40+
filePath: filePath || '',
41+
pointer: pointer || null,
42+
};
43+
}
44+
45+
findLocalRefFiles(obj: any, basePath: string, files: Set<string>): void {
46+
if (typeof obj === 'object' && obj !== null) {
47+
for (const [key, value] of Object.entries(obj)) {
48+
if (
49+
key === '$ref' &&
50+
typeof value === 'string' &&
51+
(value.startsWith('.') || value.startsWith('./') || value.startsWith('../'))
52+
) {
53+
const { filePath } = this.parseRef(value);
54+
const resolvedPath = path.resolve(basePath, filePath);
55+
56+
if (fs.existsSync(resolvedPath)) {
57+
files.add(resolvedPath);
58+
const referencedFile = yaml.load(
59+
fs.readFileSync(resolvedPath, 'utf8')
60+
);
61+
this.findLocalRefFiles(referencedFile, path.dirname(resolvedPath), files);
62+
} else {
63+
this.error(`Missing local reference: ${value}`);
64+
}
65+
} else {
66+
this.findLocalRefFiles(value, basePath, files);
67+
}
68+
}
69+
} else if (Array.isArray(obj)) {
70+
for (const item of obj) {
71+
this.findLocalRefFiles(item, basePath, files);
72+
}
73+
}
74+
}
75+
76+
async updateBundledFile(
77+
AsyncAPIFile: string,
78+
outputFormat: string,
79+
bundledFilePath: string
80+
) {
81+
try {
82+
const document = await bundle(AsyncAPIFile);
83+
const fileContent =
84+
outputFormat === '.yaml' || outputFormat === '.yml'
85+
? document.yml()
86+
: JSON.stringify(document.json());
87+
fs.writeFileSync(bundledFilePath, fileContent, { encoding: 'utf-8' });
88+
} catch (error: any) {
89+
throw new Error(`Error bundling files: ${error.message}`);
90+
}
91+
}
92+
93+
async run() {
94+
const { args, flags } = await this.parse(StartPreview);
95+
const port = flags.port;
96+
const filePath = args['spec-file'];
97+
98+
this.specFile = await load(filePath);
99+
this.metricsMetadata.port = port;
100+
101+
const AsyncAPIFile = this.specFile.getFilePath();
102+
103+
if (!AsyncAPIFile) {
104+
this.error(NO_CONTEXTS_SAVED);
105+
}
106+
107+
const outputFormat = path.extname(AsyncAPIFile);
108+
if (!outputFormat) {
109+
this.error(
110+
'Unable to determine file format from the provided AsyncAPI file.'
111+
);
112+
}
113+
114+
const bundledFilePath = `./asyncapi-bundled${outputFormat}`;
115+
const basePath = path.dirname(path.resolve(AsyncAPIFile));
116+
const filesToWatch = new Set<string>();
117+
118+
filesToWatch.add(this.specFile.getFilePath()!);
119+
const asyncapiDocument = yaml.load(
120+
fs.readFileSync(this.specFile.getFilePath()!, 'utf8')
121+
);
122+
this.findLocalRefFiles(asyncapiDocument, basePath, filesToWatch);
123+
124+
const watcher = chokidar.watch(Array.from(filesToWatch), {
125+
persistent: true,
126+
});
127+
128+
await this.updateBundledFile(
129+
AsyncAPIFile,
130+
outputFormat,
131+
bundledFilePath
132+
);
133+
134+
watcher.on('change', async (changedPath) => {
135+
this.log(`File changed: ${changedPath}`);
136+
try {
137+
await this.updateBundledFile(
138+
AsyncAPIFile,
139+
outputFormat,
140+
bundledFilePath
141+
);
142+
} catch (error: any) {
143+
this.error(`Error updating bundled file: ${error.message}`);
144+
}
145+
});
146+
147+
startStudio(bundledFilePath, port);
148+
}
149+
}
150+
151+
export default StartPreview;

src/core/flags/start/preview.flags.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Flags } from '@oclif/core';
2+
3+
export const previewFlags = () => {
4+
return {
5+
help: Flags.help({ char: 'h' ,description: 'Show CLI help'}),
6+
port: Flags.integer({ char: 'p', description: 'port in which to start Studio' }),
7+
};
8+
};

0 commit comments

Comments
 (0)