Skip to content

Commit 3651e21

Browse files
Adi-204derberg
andauthored
fix(hooks): bundle emitted AsyncAPI document in generate:after hook (#2189)
Co-authored-by: Adi-204 <adiboghawala@gmail.com> Co-authored-by: Lukasz Gornicki <lpgornicki@gmail.com>
1 parent c616054 commit 3651e21

13 files changed

Lines changed: 362 additions & 49 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@asyncapi/generator-hooks": patch
3+
---
4+
5+
Fix `createAsyncapiFile` (`generate:after`) copying the source AsyncAPI document verbatim, which left external `$ref`s (e.g. `$ref: './commons/servers.yml#/...'`) unresolved in the generated output and broke runtime consumers such as `@asyncapi/keeper`. The hook now bundles the document via `@asyncapi/bundler` when it was loaded from a file on disk, inlining external `$ref`s into a single self-contained file. If no source file is available, or bundling fails, it falls back to writing the original source unchanged.
6+
7+
Note: refs are resolved relative to the source file's own directory only, so refs pointing outside a path reachable from there (e.g. a `commons/` folder reached only through a symlink) will not bundle and fall back to the verbatim source.

apps/generator/docs/hooks.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@ It is a library of reusable hooks that you can use in your templates. You only h
9292
This library consists of the following hooks:
9393
|Hook name|Hook type|Description|
9494
|---|---|---|
95-
| `createAsyncapiFile` | `generate:after` | It creates an AsyncAPI file with the content of the spec file passed to the generator. By default, it creates the file in the root of the generation output directory. This hook also supports custom parameters that the user can pass to template generation. The parameter called `asyncapiFileDir` allows the user to specify the location where the spec file should be created. To make your template users use this parameter, you need to add it to the configuration of your template like other parameters |
95+
| `createAsyncapiFile` | `generate:after` | It creates an AsyncAPI file with the content of the spec file passed to the generator. If the spec was loaded from a file on disk, external `$ref`s (e.g. `$ref: './commons/servers.yml#/...'`) are bundled via [`@asyncapi/bundler`](https://github.com/asyncapi/bundler) into a single self-contained file; otherwise, or if bundling fails, the original spec is written unchanged. By default, it creates the file in the root of the generation output directory. This hook also supports custom parameters that the user can pass to template generation. The parameter called `asyncapiFileDir` allows the user to specify the location where the spec file should be created. To make your template users use this parameter, you need to add it to the configuration of your template like other parameters |
96+
97+
> **Limitation:** refs are only resolved relative to the source file's own directory, so a `$ref` reachable only through another path (e.g. a symlink) won't bundle and falls back to the unresolved source.
9698
9799
1. In your template configuration in `package.json` specify you want to use this library and what hook exactly:
98100
```json

apps/hooks/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
},
3030
"homepage": "https://github.com/asyncapi/generator/tree/master/apps/hooks#readme",
3131
"dependencies": {
32+
"@asyncapi/bundler": "1.0.1",
3233
"fs.extra": "^1.3.2"
3334
},
3435
"devDependencies": {

apps/hooks/src/index.js

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
const fs = require('fs');
22
const path = require('path');
33
const xfs = require('fs.extra');
4+
const bundle = require('@asyncapi/bundler');
45

5-
function createAsyncapiFile(generator) {
6-
const asyncapi = generator.originalAsyncAPI;
6+
async function createAsyncapiFile(generator) {
7+
const sourceFilePath = typeof generator.asyncapi?.meta === 'function'
8+
? generator.asyncapi.meta('asyncapi')?.source
9+
: undefined;
10+
let asyncapi = generator.originalAsyncAPI;
711
const targetDir = generator.targetDir;
812
const customDirInTarget = generator.templateParams.asyncapiFileDir;
913
const getCustomFileLocation = (target, dir, filename) => {
10-
xfs.mkdirpSync(path.resolve(target, dir));
11-
return path.resolve(target, dir, filename);
14+
xfs.mkdirpSync(path.resolve(target, dir));
15+
return path.resolve(target, dir, filename);
1216
};
1317
let extension;
14-
18+
1519
try {
1620
JSON.parse(asyncapi);
1721
extension = 'json';
@@ -20,11 +24,22 @@ function createAsyncapiFile(generator) {
2024
}
2125

2226
const outputFileName = `asyncapi.${extension}`;
23-
24-
const asyncapiOutputLocation = customDirInTarget
27+
28+
const asyncapiOutputLocation = customDirInTarget
2529
? getCustomFileLocation(targetDir, customDirInTarget, outputFileName)
2630
: path.resolve(targetDir, outputFileName);
2731

32+
if (sourceFilePath && fs.existsSync(sourceFilePath)) {
33+
try {
34+
const bundled = await bundle([sourceFilePath], {
35+
baseDir: path.dirname(sourceFilePath)
36+
});
37+
asyncapi = extension === 'json' ? bundled.string() : bundled.yml();
38+
} catch (err) {
39+
console.warn(`[generator-hooks] Failed to bundle AsyncAPI document, writing original source verbatim: ${err.message}`);
40+
}
41+
}
42+
2843
fs.writeFileSync(asyncapiOutputLocation, asyncapi);
2944
}
3045

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
asyncapi: 3.0.0
2+
info:
3+
title: Broken refs example
4+
version: 1.0.0
5+
channels:
6+
echo:
7+
$ref: './commons/does-not-exist.yml#/channels/echo'
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"asyncapi": "3.0.0",
3+
"info": {
4+
"title": "Refs example",
5+
"version": "1.0.0"
6+
},
7+
"servers": {
8+
"echoServer": {
9+
"$ref": "./commons/servers.yml#/servers/echoServer"
10+
}
11+
},
12+
"channels": {
13+
"echo": {
14+
"$ref": "./commons/channels.yml#/channels/echo"
15+
}
16+
},
17+
"operations": {
18+
"sendEcho": {
19+
"action": "send",
20+
"channel": {
21+
"$ref": "#/channels/echo"
22+
},
23+
"messages": [
24+
{ "$ref": "#/channels/echo/messages/echo" }
25+
]
26+
}
27+
}
28+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
asyncapi: 3.0.0
2+
info:
3+
title: Refs example
4+
version: 1.0.0
5+
servers:
6+
echoServer:
7+
$ref: './commons/servers.yml#/servers/echoServer'
8+
channels:
9+
echo:
10+
$ref: './commons/channels.yml#/channels/echo'
11+
operations:
12+
sendEcho:
13+
action: send
14+
channel:
15+
$ref: '#/channels/echo'
16+
messages:
17+
- $ref: '#/channels/echo/messages/echo'
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
channels:
2+
echo:
3+
address: /
4+
messages:
5+
echo:
6+
$ref: './messages.yml#/messages/echo'
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
messages:
2+
echo:
3+
payload:
4+
type: string
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
servers:
2+
echoServer:
3+
host: echo.example.org
4+
protocol: ws

0 commit comments

Comments
 (0)