diff --git a/src/commands/bundle.ts b/src/commands/bundle.ts index b1867cd80f8..059b57cca27 100644 --- a/src/commands/bundle.ts +++ b/src/commands/bundle.ts @@ -31,13 +31,11 @@ export default class Bundle extends Command { this.metricsMetadata.files = AsyncAPIFiles.length; - const document = await bundle(AsyncAPIFiles, - { - base: flags.base, - baseDir: flags.baseDir, - xOrigin: flags.xOrigin, - } - ); + const document = await this.bundleFiles(AsyncAPIFiles, { + base: flags.base, + baseDir: flags.baseDir, + xOrigin: flags.xOrigin, + }); await this.collectMetricsData(document); @@ -65,6 +63,13 @@ export default class Bundle extends Command { } } + private async bundleFiles( + files: string[], + options: { base?: string; baseDir?: string; xOrigin?: boolean } + ): Promise { + return await bundle(files, options); + } + private async collectMetricsData(document: Document) { try { // We collect the metadata from the final output so it contains all the files @@ -75,4 +80,15 @@ export default class Bundle extends Command { } } } + + /** + * Expose a utility method to bundle and return the bundled document in memory. + * Useful for commands like `start preview`. + */ + public static async bundleInMemory( + files: string[], + options: { base?: string; baseDir?: string; xOrigin?: boolean } + ): Promise { + return await bundle(files, options); + } } diff --git a/src/commands/start/preview.test.ts b/src/commands/start/preview.test.ts new file mode 100644 index 00000000000..fada9209c97 --- /dev/null +++ b/src/commands/start/preview.test.ts @@ -0,0 +1,64 @@ +// test/commands/start/preview.test.ts +import { expect, test } from '@oclif/test'; +import { promises as fs } from 'fs'; +import path from 'path'; +import { bundleInMemory } from '../../../src/commands/bundle'; +import { startPreview } from '../../../src/commands/start/preview'; // + +const asyncAPISpecPath = './test/fixtures/spec/asyncapi.yaml'; +const asyncAPIWithRefsPath = './test/fixtures/spec/asyncapi-with-refs.yaml'; + +async function setupTestSpecFiles() { + await fs.mkdir(path.dirname(asyncAPISpecPath), { recursive: true }); + await fs.writeFile( + asyncAPISpecPath, + 'asyncapi: "2.0.0"\ninfo:\n title: Test API\n version: "1.0.0"\n' + ); + await fs.writeFile( + asyncAPIWithRefsPath, + 'asyncapi: "2.0.0"\ninfo:\n title: Test API with Refs\n version: "1.0.0"\ncomponents:\n schemas:\n example: \n $ref: ./example-schema.yaml\n' + ); +} + +describe('start preview', () => { + before(async () => { + await setupTestSpecFiles(); + }); + + after(async () => { + await fs.rm('./test/fixtures', { recursive: true, force: true }); + }); + + test + .stdout() + .stderr() + .do(async () => { + const bundledDoc = await bundleInMemory([asyncAPISpecPath], {}); + await startPreview(bundledDoc.string(), { readOnly: true }); + }) + .it('should start preview with a bundled AsyncAPI document', (ctx) => { + expect(ctx.stdout).to.contain('Preview started'); + }); + + test + .stdout() + .stderr() + .do(async () => { + const bundledDoc = await bundleInMemory([asyncAPIWithRefsPath], {}); + await startPreview(bundledDoc.string(), { readOnly: true }); + }) + .it('should handle references correctly during preview', (ctx) => { + expect(ctx.stdout).to.contain('Preview started'); + }); + + test + .stderr() + .do(async () => { + try { + await startPreview('', { readOnly: true }); + } catch (error) { + expect(error.message).to.contain('Invalid AsyncAPI document'); + } + }) + .it('should throw an error for invalid or empty AsyncAPI documents'); +}); diff --git a/src/commands/start/preview.ts b/src/commands/start/preview.ts new file mode 100644 index 00000000000..39986b8ce7d --- /dev/null +++ b/src/commands/start/preview.ts @@ -0,0 +1,44 @@ +import { Command } from '@oclif/core'; +import chokidar from 'chokidar'; +import { bundle } from '../bundler'; +import { startStudio } from '../studio'; +import path from 'path'; +import fs from 'fs'; + +export default class StartPreview extends Command { + static description = 'Start a live preview of your AsyncAPI document with references resolved.'; + + static args = [ + { name: 'file', required: true, description: 'Path to the AsyncAPI file' }, + ]; + + async run() { + const { args } = await this.parse(StartPreview); + + const filePath = path.resolve(args.file); + if (!fs.existsSync(filePath)) { + this.error(`File not found: ${filePath}`); + } + + const bundleAndPreview = async () => { + try { + this.log('Bundling AsyncAPI file...'); + const bundledDocument = await bundle(filePath); + this.log('Starting Studio in preview mode...'); + await startStudio(bundledDocument, { readOnly: true }); + } catch (error) { + this.error(`Error bundling AsyncAPI file: ${error.message}`); + } + }; + + await bundleAndPreview(); + + const watcher = chokidar.watch(filePath, { ignoreInitial: true }); + watcher.on('change', async () => { + this.log('File changed. Reloading preview...'); + await bundleAndPreview(); + }); + + this.log('Watching for file changes. Press Ctrl+C to stop.'); + } +} diff --git a/src/commands/studio.ts b/src/commands/studio.ts new file mode 100644 index 00000000000..4cf0e539587 --- /dev/null +++ b/src/commands/studio.ts @@ -0,0 +1,9 @@ +import open from 'open'; + +export async function startStudio(bundledDocument: string, options: { readOnly: boolean }) { + const studioUrl = `https://studio.asyncapi.com/?readOnly=${options.readOnly}&document=${encodeURIComponent( + bundledDocument + )}`; + + await open(studioUrl); +}