@rvct/asyncapi 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +69 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +66 -0
- package/dist/core/AsyncApiDocument.d.ts +13 -0
- package/dist/core/AsyncApiDocument.js +21 -0
- package/dist/core/entityGraph.d.ts +2 -0
- package/dist/core/entityGraph.js +13 -0
- package/dist/core/generators/OperationGenerator.d.ts +7 -0
- package/dist/core/generators/OperationGenerator.js +145 -0
- package/dist/core/generators/SchemaGenerator.d.ts +7 -0
- package/dist/core/generators/SchemaGenerator.js +26 -0
- package/dist/core/loadFromConfig.d.ts +6 -0
- package/dist/core/loadFromConfig.js +14 -0
- package/dist/core/naming.d.ts +15 -0
- package/dist/core/naming.js +81 -0
- package/dist/core/normalizeSchema.d.ts +8 -0
- package/dist/core/normalizeSchema.js +28 -0
- package/dist/emitters/json-schema.d.ts +6 -0
- package/dist/emitters/json-schema.js +9 -0
- package/dist/emitters/typescript.d.ts +6 -0
- package/dist/emitters/typescript.js +42 -0
- package/dist/emitters/zod.d.ts +6 -0
- package/dist/emitters/zod.js +63 -0
- package/dist/generate.d.ts +2 -0
- package/dist/generate.js +33 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/plugins/asyncapi.d.ts +13 -0
- package/dist/plugins/asyncapi.js +56 -0
- package/dist/plugins/definePlugin.d.ts +2 -0
- package/dist/plugins/definePlugin.js +3 -0
- package/dist/plugins/typescript.d.ts +2 -0
- package/dist/plugins/typescript.js +26 -0
- package/dist/plugins/zod.d.ts +2 -0
- package/dist/plugins/zod.js +26 -0
- package/dist/runtime/PluginManager.d.ts +13 -0
- package/dist/runtime/PluginManager.js +90 -0
- package/dist/runtime/writeArtifacts.d.ts +6 -0
- package/dist/runtime/writeArtifacts.js +73 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.js +1 -0
- package/package.json +62 -0
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import { generate } from "./generate.js";
|
|
4
|
+
const HELP = `
|
|
5
|
+
Usage: asyncapi-codegen [--config <path-to-config>] [--input <path-to-asyncapi.{yaml|json}>] [--out <output-dir>]
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
-c, --config <path> Path to config file. If omitted, asyncapi-codegen.config.{mjs,js,cjs} is resolved from cwd.
|
|
9
|
+
-i, --input <path> Override config.input.path.
|
|
10
|
+
-o, --out <dir> Override config.output.path.
|
|
11
|
+
-h, --help Show this help message.
|
|
12
|
+
|
|
13
|
+
Examples:
|
|
14
|
+
asyncapi-codegen
|
|
15
|
+
asyncapi-codegen --config ./asyncapi-codegen.config.mjs
|
|
16
|
+
asyncapi-codegen --config ./asyncapi-codegen.config.mjs --input ./specs/asyncapi.yaml --out ./generated
|
|
17
|
+
`;
|
|
18
|
+
function printDiagnostics(diagnostics) {
|
|
19
|
+
for (const diagnostic of diagnostics) {
|
|
20
|
+
if (!diagnostic || typeof diagnostic !== "object") {
|
|
21
|
+
console.log(`[asyncapi] ${String(diagnostic)}`);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const value = diagnostic;
|
|
25
|
+
const path = Array.isArray(value.path) && value.path.length > 0
|
|
26
|
+
? value.path.join(".")
|
|
27
|
+
: "<root>";
|
|
28
|
+
const severity = typeof value.severity === "string" || typeof value.severity === "number"
|
|
29
|
+
? String(value.severity)
|
|
30
|
+
: "info";
|
|
31
|
+
const message = typeof value.message === "string" ? value.message : String(diagnostic);
|
|
32
|
+
console.log(`[asyncapi:${severity}] ${path}: ${message}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function runCli(args = process.argv.slice(2)) {
|
|
36
|
+
let values;
|
|
37
|
+
try {
|
|
38
|
+
({ values } = parseArgs({
|
|
39
|
+
args,
|
|
40
|
+
options: {
|
|
41
|
+
config: { type: "string", short: "c" },
|
|
42
|
+
input: { type: "string", short: "i" },
|
|
43
|
+
out: { type: "string", short: "o" },
|
|
44
|
+
help: { type: "boolean", short: "h" },
|
|
45
|
+
},
|
|
46
|
+
strict: true,
|
|
47
|
+
allowPositionals: false,
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
52
|
+
throw new Error(`${message}\n\n${HELP}`);
|
|
53
|
+
}
|
|
54
|
+
if (values.help) {
|
|
55
|
+
console.log(HELP);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const result = await generate({
|
|
59
|
+
config: values.config,
|
|
60
|
+
input: values.input,
|
|
61
|
+
out: values.out,
|
|
62
|
+
});
|
|
63
|
+
printDiagnostics(result.diagnostics);
|
|
64
|
+
console.log(`Generated ${result.total} artifacts -> ${result.outDir}`);
|
|
65
|
+
}
|
|
66
|
+
runCli().catch((error) => {
|
|
67
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
});
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { UserConfig } from "./types.js";
|
|
2
|
+
export declare function defineConfig<T extends UserConfig>(config: T): T;
|
|
3
|
+
export declare function loadConfig({ cwd, configPath, input, out, }: {
|
|
4
|
+
cwd: string;
|
|
5
|
+
configPath?: string;
|
|
6
|
+
input?: string;
|
|
7
|
+
out?: string;
|
|
8
|
+
}): Promise<UserConfig>;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
const CONFIG_FILE_NAMES = [
|
|
6
|
+
"asyncapi.config.mjs",
|
|
7
|
+
"asyncapi.config.js",
|
|
8
|
+
"asyncapi.config.cjs",
|
|
9
|
+
];
|
|
10
|
+
export function defineConfig(config) {
|
|
11
|
+
return config;
|
|
12
|
+
}
|
|
13
|
+
function assertConfigShape(value) {
|
|
14
|
+
if (!value || typeof value !== "object") {
|
|
15
|
+
throw new Error("Config must export an object.");
|
|
16
|
+
}
|
|
17
|
+
const config = value;
|
|
18
|
+
if (!config.input ||
|
|
19
|
+
typeof config.input.path !== "string" ||
|
|
20
|
+
!config.input.path) {
|
|
21
|
+
throw new Error("Config must define input.path.");
|
|
22
|
+
}
|
|
23
|
+
if (!config.output ||
|
|
24
|
+
typeof config.output.path !== "string" ||
|
|
25
|
+
!config.output.path) {
|
|
26
|
+
throw new Error("Config must define output.path.");
|
|
27
|
+
}
|
|
28
|
+
if (config.plugins !== undefined && !Array.isArray(config.plugins)) {
|
|
29
|
+
throw new Error("Config plugins must be an array when provided.");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function resolveConfigPath({ cwd, configPath, }) {
|
|
33
|
+
if (configPath) {
|
|
34
|
+
return resolve(cwd, configPath);
|
|
35
|
+
}
|
|
36
|
+
for (const candidate of CONFIG_FILE_NAMES) {
|
|
37
|
+
const resolved = resolve(cwd, candidate);
|
|
38
|
+
try {
|
|
39
|
+
await access(resolved, constants.R_OK);
|
|
40
|
+
return resolved;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
throw new Error("Config not found. Create asyncapi.config.mjs or pass one with --config.");
|
|
47
|
+
}
|
|
48
|
+
export async function loadConfig({ cwd, configPath, input, out, }) {
|
|
49
|
+
const resolvedConfigPath = await resolveConfigPath({ cwd, configPath });
|
|
50
|
+
const moduleUrl = pathToFileURL(resolvedConfigPath).href;
|
|
51
|
+
const imported = (await import(moduleUrl));
|
|
52
|
+
const loaded = imported.default ?? imported;
|
|
53
|
+
assertConfigShape(loaded);
|
|
54
|
+
return {
|
|
55
|
+
...loaded,
|
|
56
|
+
input: {
|
|
57
|
+
...loaded.input,
|
|
58
|
+
path: input ?? loaded.input.path,
|
|
59
|
+
},
|
|
60
|
+
output: {
|
|
61
|
+
...loaded.output,
|
|
62
|
+
path: out ?? loaded.output.path,
|
|
63
|
+
},
|
|
64
|
+
plugins: Array.isArray(loaded.plugins) ? loaded.plugins : [],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AsyncAPIDocumentInterface, ChannelInterface, Diagnostic, OperationInterface, SchemaInterface } from "@asyncapi/parser";
|
|
2
|
+
import type { AsyncApiEntitySeed } from "../types.js";
|
|
3
|
+
export declare class AsyncApiDocument {
|
|
4
|
+
document: AsyncAPIDocumentInterface;
|
|
5
|
+
diagnostics: Diagnostic[];
|
|
6
|
+
constructor(document: AsyncAPIDocumentInterface, { diagnostics }?: {
|
|
7
|
+
diagnostics?: Diagnostic[];
|
|
8
|
+
});
|
|
9
|
+
getComponentSchemas(): SchemaInterface[];
|
|
10
|
+
getOperations(): OperationInterface[];
|
|
11
|
+
getChannels(): ChannelInterface[];
|
|
12
|
+
resolveNames(entities: AsyncApiEntitySeed[]): AsyncApiEntitySeed[];
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { resolveEntityNames } from "../core/naming.js";
|
|
2
|
+
export class AsyncApiDocument {
|
|
3
|
+
document;
|
|
4
|
+
diagnostics;
|
|
5
|
+
constructor(document, { diagnostics = [] } = {}) {
|
|
6
|
+
this.document = document;
|
|
7
|
+
this.diagnostics = diagnostics;
|
|
8
|
+
}
|
|
9
|
+
getComponentSchemas() {
|
|
10
|
+
return [...(this.document.components?.()?.schemas?.() ?? [])];
|
|
11
|
+
}
|
|
12
|
+
getOperations() {
|
|
13
|
+
return [...(this.document.operations?.() ?? [])];
|
|
14
|
+
}
|
|
15
|
+
getChannels() {
|
|
16
|
+
return [...(this.document.channels?.() ?? [])];
|
|
17
|
+
}
|
|
18
|
+
resolveNames(entities) {
|
|
19
|
+
return resolveEntityNames(entities);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function createEntityGraph(entities) {
|
|
2
|
+
const byId = new Map();
|
|
3
|
+
for (const entity of entities) {
|
|
4
|
+
if (byId.has(entity.id)) {
|
|
5
|
+
throw new Error(`Duplicate entity id detected: ${entity.id}`);
|
|
6
|
+
}
|
|
7
|
+
byId.set(entity.id, entity);
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
entities,
|
|
11
|
+
byId,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AsyncApiDocument } from "../../core/AsyncApiDocument.js";
|
|
2
|
+
import type { AsyncApiEntity } from "../../types.js";
|
|
3
|
+
export declare class OperationGenerator {
|
|
4
|
+
asyncapi: AsyncApiDocument;
|
|
5
|
+
constructor(asyncapi: AsyncApiDocument);
|
|
6
|
+
build(): Promise<AsyncApiEntity[]>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { pascalCase } from "../../core/naming.js";
|
|
2
|
+
import { normalizeSchema } from "../../core/normalizeSchema.js";
|
|
3
|
+
function getMessageVariant(operationId, messageId) {
|
|
4
|
+
const operationName = pascalCase(operationId);
|
|
5
|
+
const messageName = pascalCase(messageId);
|
|
6
|
+
if (messageName.startsWith(operationName) &&
|
|
7
|
+
messageName.length > operationName.length) {
|
|
8
|
+
return messageName.slice(operationName.length);
|
|
9
|
+
}
|
|
10
|
+
return messageName;
|
|
11
|
+
}
|
|
12
|
+
function getPayloadBaseName(operationId, messageId, totalMessages) {
|
|
13
|
+
if (totalMessages === 1) {
|
|
14
|
+
return `${operationId}Payload`;
|
|
15
|
+
}
|
|
16
|
+
return `${operationId}${getMessageVariant(operationId, messageId)}Payload`;
|
|
17
|
+
}
|
|
18
|
+
function getReplyBaseName(operationId, messageId, totalMessages) {
|
|
19
|
+
if (totalMessages === 1) {
|
|
20
|
+
return `${operationId}ReplyPayload`;
|
|
21
|
+
}
|
|
22
|
+
let variant = getMessageVariant(operationId, messageId);
|
|
23
|
+
if (variant.startsWith("Reply") && variant.length > "Reply".length) {
|
|
24
|
+
variant = variant.slice("Reply".length);
|
|
25
|
+
}
|
|
26
|
+
return `${operationId}${variant || "Reply"}ReplyPayload`;
|
|
27
|
+
}
|
|
28
|
+
function getHeadersBaseName(operationId, messageId, totalMessages, role) {
|
|
29
|
+
if (role === "reply" && totalMessages === 1) {
|
|
30
|
+
return `${operationId}ReplyHeaders`;
|
|
31
|
+
}
|
|
32
|
+
if (role === "message" && totalMessages === 1) {
|
|
33
|
+
return `${operationId}Headers`;
|
|
34
|
+
}
|
|
35
|
+
const variant = getMessageVariant(operationId, messageId);
|
|
36
|
+
return `${operationId}${variant}Headers`;
|
|
37
|
+
}
|
|
38
|
+
export class OperationGenerator {
|
|
39
|
+
asyncapi;
|
|
40
|
+
constructor(asyncapi) {
|
|
41
|
+
this.asyncapi = asyncapi;
|
|
42
|
+
}
|
|
43
|
+
async build() {
|
|
44
|
+
const entities = [];
|
|
45
|
+
for (const [index, operation] of this.asyncapi.getOperations().entries()) {
|
|
46
|
+
const operationId = operation.id?.() ?? `operation${index + 1}`;
|
|
47
|
+
const messages = [
|
|
48
|
+
...(operation.messages?.() ?? []),
|
|
49
|
+
];
|
|
50
|
+
for (const [messageIndex, message] of messages.entries()) {
|
|
51
|
+
const messageId = message.id?.() ?? `${operationId}Message${messageIndex + 1}`;
|
|
52
|
+
const payload = message.payload?.();
|
|
53
|
+
const headers = message.headers?.();
|
|
54
|
+
if (payload) {
|
|
55
|
+
entities.push({
|
|
56
|
+
id: `operations.${operationId}.messages.${messageId}.payload`,
|
|
57
|
+
kind: "message-payload",
|
|
58
|
+
baseName: getPayloadBaseName(operationId, messageId, messages.length),
|
|
59
|
+
name: getPayloadBaseName(operationId, messageId, messages.length),
|
|
60
|
+
schema: await normalizeSchema({
|
|
61
|
+
schemaModel: payload,
|
|
62
|
+
schemaFormat: message.schemaFormat?.(),
|
|
63
|
+
name: `${operationId}.${messageId}.payload`,
|
|
64
|
+
}),
|
|
65
|
+
sourcePath: `#/operations/${operationId}/messages/${messageId}/payload`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (headers) {
|
|
69
|
+
entities.push({
|
|
70
|
+
id: `operations.${operationId}.messages.${messageId}.headers`,
|
|
71
|
+
kind: "message-header",
|
|
72
|
+
baseName: getHeadersBaseName(operationId, messageId, messages.length, "message"),
|
|
73
|
+
name: getHeadersBaseName(operationId, messageId, messages.length, "message"),
|
|
74
|
+
schema: await normalizeSchema({
|
|
75
|
+
schemaModel: headers,
|
|
76
|
+
name: `${operationId}.${messageId}.headers`,
|
|
77
|
+
}),
|
|
78
|
+
sourcePath: `#/operations/${operationId}/messages/${messageId}/headers`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const reply = operation.reply?.();
|
|
83
|
+
const replyMessages = reply
|
|
84
|
+
? [...(reply.messages?.() ?? [])]
|
|
85
|
+
: [];
|
|
86
|
+
for (const [replyIndex, message] of replyMessages.entries()) {
|
|
87
|
+
const messageId = message.id?.() ?? `${operationId}Reply${replyIndex + 1}`;
|
|
88
|
+
const payload = message.payload?.();
|
|
89
|
+
const headers = message.headers?.();
|
|
90
|
+
if (payload) {
|
|
91
|
+
entities.push({
|
|
92
|
+
id: `operations.${operationId}.reply.messages.${messageId}.payload`,
|
|
93
|
+
kind: "reply-payload",
|
|
94
|
+
baseName: getReplyBaseName(operationId, messageId, replyMessages.length),
|
|
95
|
+
name: getReplyBaseName(operationId, messageId, replyMessages.length),
|
|
96
|
+
schema: await normalizeSchema({
|
|
97
|
+
schemaModel: payload,
|
|
98
|
+
schemaFormat: message.schemaFormat?.(),
|
|
99
|
+
name: `${operationId}.${messageId}.reply.payload`,
|
|
100
|
+
}),
|
|
101
|
+
sourcePath: `#/operations/${operationId}/reply/messages/${messageId}/payload`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (headers) {
|
|
105
|
+
entities.push({
|
|
106
|
+
id: `operations.${operationId}.reply.messages.${messageId}.headers`,
|
|
107
|
+
kind: "message-header",
|
|
108
|
+
baseName: getHeadersBaseName(operationId, messageId, replyMessages.length, "reply"),
|
|
109
|
+
name: getHeadersBaseName(operationId, messageId, replyMessages.length, "reply"),
|
|
110
|
+
schema: await normalizeSchema({
|
|
111
|
+
schemaModel: headers,
|
|
112
|
+
name: `${operationId}.${messageId}.reply.headers`,
|
|
113
|
+
}),
|
|
114
|
+
sourcePath: `#/operations/${operationId}/reply/messages/${messageId}/headers`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const [channelIndex, channel] of this.asyncapi.getChannels().entries()) {
|
|
120
|
+
const channelId = channel.id?.() ?? `channel${channelIndex + 1}`;
|
|
121
|
+
const parameters = [
|
|
122
|
+
...(channel.parameters?.() ?? []),
|
|
123
|
+
];
|
|
124
|
+
for (const [parameterIndex, parameter] of parameters.entries()) {
|
|
125
|
+
const parameterId = parameter.id?.() ?? `parameter${parameterIndex + 1}`;
|
|
126
|
+
const schema = parameter.schema?.();
|
|
127
|
+
if (!schema) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
entities.push({
|
|
131
|
+
id: `channels.${channelId}.parameters.${parameterId}`,
|
|
132
|
+
kind: "channel-parameter",
|
|
133
|
+
baseName: `${channelId} ${parameterId} Parameter`,
|
|
134
|
+
name: `${channelId} ${parameterId} Parameter`,
|
|
135
|
+
schema: await normalizeSchema({
|
|
136
|
+
schemaModel: schema,
|
|
137
|
+
name: `${channelId}.${parameterId}.parameter`,
|
|
138
|
+
}),
|
|
139
|
+
sourcePath: `#/channels/${channelId}/parameters/${parameterId}`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return entities;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AsyncApiDocument } from "../../core/AsyncApiDocument.js";
|
|
2
|
+
import type { AsyncApiEntity } from "../../types.js";
|
|
3
|
+
export declare class SchemaGenerator {
|
|
4
|
+
asyncapi: AsyncApiDocument;
|
|
5
|
+
constructor(asyncapi: AsyncApiDocument);
|
|
6
|
+
build(): Promise<AsyncApiEntity[]>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { normalizeSchema } from "../../core/normalizeSchema.js";
|
|
2
|
+
export class SchemaGenerator {
|
|
3
|
+
asyncapi;
|
|
4
|
+
constructor(asyncapi) {
|
|
5
|
+
this.asyncapi = asyncapi;
|
|
6
|
+
}
|
|
7
|
+
async build() {
|
|
8
|
+
const entities = [];
|
|
9
|
+
for (const schemaModel of this.asyncapi.getComponentSchemas()) {
|
|
10
|
+
const schemaId = schemaModel.id?.() ?? "Schema";
|
|
11
|
+
entities.push({
|
|
12
|
+
id: `components.schemas.${schemaId}`,
|
|
13
|
+
kind: "component-schema",
|
|
14
|
+
baseName: schemaId,
|
|
15
|
+
name: schemaId,
|
|
16
|
+
schema: await normalizeSchema({
|
|
17
|
+
schemaModel,
|
|
18
|
+
schemaFormat: schemaModel.schemaFormat?.(),
|
|
19
|
+
name: schemaId,
|
|
20
|
+
}),
|
|
21
|
+
sourcePath: `#/components/schemas/${schemaId}`,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return entities;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { DiagnosticSeverity, Parser } from "@asyncapi/parser";
|
|
4
|
+
import { AsyncApiDocument } from "../core/AsyncApiDocument.js";
|
|
5
|
+
export async function loadFromConfig({ cwd, input, }) {
|
|
6
|
+
const parser = new Parser();
|
|
7
|
+
const source = readFileSync(resolve(cwd, input.path), "utf8");
|
|
8
|
+
const { document, diagnostics } = await parser.parse(source);
|
|
9
|
+
const fatalDiagnostics = diagnostics.filter((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error);
|
|
10
|
+
if (!document || fatalDiagnostics.length > 0) {
|
|
11
|
+
throw new Error(`AsyncAPI parse failed with ${fatalDiagnostics.length} error(s).`);
|
|
12
|
+
}
|
|
13
|
+
return new AsyncApiDocument(document, { diagnostics });
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string} value
|
|
3
|
+
* @returns {string}
|
|
4
|
+
*/
|
|
5
|
+
export declare function pascalCase(value: any): string;
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} value
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
export declare function camelCase(value: any): string;
|
|
11
|
+
/**
|
|
12
|
+
* @param {Array<{ id: string, kind: keyof typeof KIND_SUFFIX, baseName?: string, name?: string }>} entities
|
|
13
|
+
* @returns {Array<{ id: string, kind: keyof typeof KIND_SUFFIX, baseName?: string, name: string }>}
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveEntityNames(entities: any): any;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
const KIND_SUFFIX = {
|
|
3
|
+
"component-schema": "Schema",
|
|
4
|
+
"message-payload": "Payload",
|
|
5
|
+
"reply-payload": "ReplyPayload",
|
|
6
|
+
"channel-parameter": "Parameter",
|
|
7
|
+
"message-header": "Headers",
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} value
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
export function pascalCase(value) {
|
|
14
|
+
return String(value)
|
|
15
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
16
|
+
.replace(/[^a-zA-Z0-9]+/g, " ")
|
|
17
|
+
.trim()
|
|
18
|
+
.split(/\s+/)
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
21
|
+
.join("");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} value
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
export function camelCase(value) {
|
|
28
|
+
const normalized = pascalCase(value);
|
|
29
|
+
if (!normalized) {
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
|
32
|
+
return normalized[0].toLowerCase() + normalized.slice(1);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* @param {Array<{ id: string, kind: keyof typeof KIND_SUFFIX, baseName?: string, name?: string }>} entities
|
|
36
|
+
* @returns {Array<{ id: string, kind: keyof typeof KIND_SUFFIX, baseName?: string, name: string }>}
|
|
37
|
+
*/
|
|
38
|
+
export function resolveEntityNames(entities) {
|
|
39
|
+
const initial = entities.map((entity) => ({
|
|
40
|
+
...entity,
|
|
41
|
+
name: pascalCase(entity.baseName ?? entity.name ?? entity.id) || "Entity",
|
|
42
|
+
}));
|
|
43
|
+
const initialCounts = countByName(initial);
|
|
44
|
+
const withSemanticSuffix = initial.map((entity) => {
|
|
45
|
+
if ((initialCounts.get(entity.name) ?? 0) < 2) {
|
|
46
|
+
return entity;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
...entity,
|
|
50
|
+
name: `${entity.name}${KIND_SUFFIX[entity.kind]}`,
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
const finalCounts = countByName(withSemanticSuffix);
|
|
54
|
+
/** @type {Map<string, number>} */
|
|
55
|
+
const seen = new Map();
|
|
56
|
+
return withSemanticSuffix.map((entity) => {
|
|
57
|
+
if ((finalCounts.get(entity.name) ?? 0) < 2) {
|
|
58
|
+
return entity;
|
|
59
|
+
}
|
|
60
|
+
const nextIndex = (seen.get(entity.name) ?? 0) + 1;
|
|
61
|
+
seen.set(entity.name, nextIndex);
|
|
62
|
+
return nextIndex === 1
|
|
63
|
+
? entity
|
|
64
|
+
: {
|
|
65
|
+
...entity,
|
|
66
|
+
name: `${entity.name}${nextIndex}`,
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* @param {Array<{ name: string }>} entities
|
|
72
|
+
* @returns {Map<string, number>}
|
|
73
|
+
*/
|
|
74
|
+
function countByName(entities) {
|
|
75
|
+
/** @type {Map<string, number>} */
|
|
76
|
+
const counts = new Map();
|
|
77
|
+
for (const entity of entities) {
|
|
78
|
+
counts.set(entity.name, (counts.get(entity.name) ?? 0) + 1);
|
|
79
|
+
}
|
|
80
|
+
return counts;
|
|
81
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const PARSER_PRIVATE_KEYS = new Set([
|
|
2
|
+
"x-parser-schema-id",
|
|
3
|
+
"x-parser-unique-object-id",
|
|
4
|
+
]);
|
|
5
|
+
function stripParserMetadata(value) {
|
|
6
|
+
if (Array.isArray(value)) {
|
|
7
|
+
return value.map((item) => stripParserMetadata(item));
|
|
8
|
+
}
|
|
9
|
+
if (value && typeof value === "object") {
|
|
10
|
+
return Object.fromEntries(Object.entries(value)
|
|
11
|
+
.filter(([key, nested]) => !PARSER_PRIVATE_KEYS.has(key) && nested !== undefined)
|
|
12
|
+
.map(([key, nested]) => [key, stripParserMetadata(nested)]));
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
export async function normalizeSchema({ schemaModel, schemaFormat, name, }) {
|
|
17
|
+
const resolvedSchemaFormat = schemaFormat ??
|
|
18
|
+
(typeof schemaModel.schemaFormat === "function"
|
|
19
|
+
? schemaModel.schemaFormat()
|
|
20
|
+
: undefined);
|
|
21
|
+
if (resolvedSchemaFormat &&
|
|
22
|
+
!resolvedSchemaFormat.startsWith("application/vnd.aai.asyncapi") &&
|
|
23
|
+
!resolvedSchemaFormat.startsWith("application/schema+json") &&
|
|
24
|
+
!resolvedSchemaFormat.startsWith("application/json")) {
|
|
25
|
+
throw new Error(`Unsupported schemaFormat for ${name}: ${resolvedSchemaFormat}. v1 only supports AsyncAPI and JSON Schema compatible payloads.`);
|
|
26
|
+
}
|
|
27
|
+
return stripParserMetadata(schemaModel.json());
|
|
28
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AsyncApiEntityGraph, EntityKind, GeneratedArtifact } from "../types.js";
|
|
2
|
+
export declare function emitJsonSchemaArtifacts({ graph, outputPath, include, }: {
|
|
3
|
+
graph: AsyncApiEntityGraph;
|
|
4
|
+
outputPath: string;
|
|
5
|
+
include?: EntityKind[];
|
|
6
|
+
}): GeneratedArtifact[];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function emitJsonSchemaArtifacts({ graph, outputPath, include, }) {
|
|
2
|
+
return graph.entities
|
|
3
|
+
.filter((entity) => Array.isArray(include) ? include.includes(entity.kind) : true)
|
|
4
|
+
.map((entity) => ({
|
|
5
|
+
kind: "json-schema",
|
|
6
|
+
filePath: `${outputPath}/${entity.name}.schema.json`,
|
|
7
|
+
code: `${JSON.stringify(entity.schema, null, 2)}\n`,
|
|
8
|
+
}));
|
|
9
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AsyncApiEntityGraph, EntityKind, GeneratedArtifact } from "../types.js";
|
|
2
|
+
export declare function emitTypescriptArtifacts({ graph, outputPath, include, }: {
|
|
3
|
+
graph: AsyncApiEntityGraph;
|
|
4
|
+
outputPath: string;
|
|
5
|
+
include?: EntityKind[];
|
|
6
|
+
}): Promise<GeneratedArtifact[]>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { compile } from "json-schema-to-typescript";
|
|
2
|
+
const BANNER = `/**
|
|
3
|
+
* Generated from AsyncAPI spec.
|
|
4
|
+
* Do not edit manually.
|
|
5
|
+
*/`;
|
|
6
|
+
function applyBinaryTsType(value) {
|
|
7
|
+
if (Array.isArray(value)) {
|
|
8
|
+
return value.map((item) => applyBinaryTsType(item));
|
|
9
|
+
}
|
|
10
|
+
if (value && typeof value === "object") {
|
|
11
|
+
const node = value;
|
|
12
|
+
const transformed = Object.fromEntries(Object.entries(node).map(([key, nested]) => [
|
|
13
|
+
key,
|
|
14
|
+
applyBinaryTsType(nested),
|
|
15
|
+
]));
|
|
16
|
+
if (transformed.type === "string" && transformed.format === "binary") {
|
|
17
|
+
return {
|
|
18
|
+
...transformed,
|
|
19
|
+
tsType: "ArrayBuffer",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return transformed;
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
export async function emitTypescriptArtifacts({ graph, outputPath, include, }) {
|
|
27
|
+
return Promise.all(graph.entities
|
|
28
|
+
.filter((entity) => Array.isArray(include) ? include.includes(entity.kind) : true)
|
|
29
|
+
.map(async (entity) => ({
|
|
30
|
+
kind: "types",
|
|
31
|
+
filePath: `${outputPath}/${entity.name}.ts`,
|
|
32
|
+
code: await compile(applyBinaryTsType(entity.schema), entity.name, {
|
|
33
|
+
additionalProperties: false,
|
|
34
|
+
bannerComment: BANNER,
|
|
35
|
+
format: false,
|
|
36
|
+
}),
|
|
37
|
+
export: {
|
|
38
|
+
name: entity.name,
|
|
39
|
+
kind: "type",
|
|
40
|
+
},
|
|
41
|
+
})));
|
|
42
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { jsonSchemaToZod } from "json-schema-to-zod";
|
|
2
|
+
import { camelCase } from "../core/naming.js";
|
|
3
|
+
const BANNER = `/**
|
|
4
|
+
* Generated from AsyncAPI spec.
|
|
5
|
+
* Do not edit manually.
|
|
6
|
+
*/`;
|
|
7
|
+
function normalizeZodSchema(value) {
|
|
8
|
+
if (Array.isArray(value)) {
|
|
9
|
+
return value.map((item) => normalizeZodSchema(item));
|
|
10
|
+
}
|
|
11
|
+
if (value && typeof value === "object") {
|
|
12
|
+
const node = value;
|
|
13
|
+
const transformed = Object.fromEntries(Object.entries(node).map(([key, nested]) => [
|
|
14
|
+
key,
|
|
15
|
+
normalizeZodSchema(nested),
|
|
16
|
+
]));
|
|
17
|
+
if (Array.isArray(transformed.oneOf) && transformed.anyOf === undefined) {
|
|
18
|
+
return {
|
|
19
|
+
...transformed,
|
|
20
|
+
anyOf: transformed.oneOf,
|
|
21
|
+
oneOf: undefined,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return transformed;
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
function withBinaryArrayBufferZodParser(node) {
|
|
29
|
+
if (node.type === "string" && node.format === "binary") {
|
|
30
|
+
return "z.instanceof(ArrayBuffer)";
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
function emitZodExpression(schema) {
|
|
35
|
+
return jsonSchemaToZod(normalizeZodSchema(schema), {
|
|
36
|
+
parserOverride: withBinaryArrayBufferZodParser,
|
|
37
|
+
})
|
|
38
|
+
.replace(/z\.record\((?!z\.string\(\),\s)/g, "z.record(z.string(), ")
|
|
39
|
+
.replaceAll("z.any()", "z.unknown()")
|
|
40
|
+
.trim()
|
|
41
|
+
.replace(/;$/, "");
|
|
42
|
+
}
|
|
43
|
+
export function emitZodArtifacts({ graph, outputPath, include, }) {
|
|
44
|
+
return graph.entities
|
|
45
|
+
.filter((entity) => Array.isArray(include) ? include.includes(entity.kind) : true)
|
|
46
|
+
.map((entity) => {
|
|
47
|
+
const exportName = `${camelCase(entity.name)}Schema`;
|
|
48
|
+
return {
|
|
49
|
+
kind: "zod",
|
|
50
|
+
filePath: `${outputPath}/${entity.name}Schema.ts`,
|
|
51
|
+
code: `${BANNER}
|
|
52
|
+
|
|
53
|
+
import { z } from "zod/v4";
|
|
54
|
+
|
|
55
|
+
export const ${exportName} = ${emitZodExpression(entity.schema)};
|
|
56
|
+
`,
|
|
57
|
+
export: {
|
|
58
|
+
name: exportName,
|
|
59
|
+
kind: "value",
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
package/dist/generate.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { loadConfig } from "./config.js";
|
|
2
|
+
import { PluginManager } from "./runtime/PluginManager.js";
|
|
3
|
+
import { writeArtifacts } from "./runtime/writeArtifacts.js";
|
|
4
|
+
export async function generate(options = {}) {
|
|
5
|
+
const cwd = options.cwd ?? process.cwd();
|
|
6
|
+
const config = await loadConfig({
|
|
7
|
+
cwd,
|
|
8
|
+
configPath: options.config,
|
|
9
|
+
input: options.input,
|
|
10
|
+
out: options.out,
|
|
11
|
+
});
|
|
12
|
+
const pluginManager = new PluginManager(config);
|
|
13
|
+
const context = {
|
|
14
|
+
cwd,
|
|
15
|
+
config,
|
|
16
|
+
asyncapi: null,
|
|
17
|
+
graph: null,
|
|
18
|
+
diagnostics: [],
|
|
19
|
+
artifacts: [],
|
|
20
|
+
};
|
|
21
|
+
await pluginManager.run(context);
|
|
22
|
+
const outDir = await writeArtifacts({
|
|
23
|
+
cwd,
|
|
24
|
+
outDir: config.output.path,
|
|
25
|
+
artifacts: context.artifacts,
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
total: context.artifacts.length,
|
|
29
|
+
outDir,
|
|
30
|
+
diagnostics: context.diagnostics,
|
|
31
|
+
artifacts: context.artifacts,
|
|
32
|
+
};
|
|
33
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EntityKind, PluginInstance } from "../types.js";
|
|
2
|
+
interface AsyncApiPluginOptions {
|
|
3
|
+
output?: {
|
|
4
|
+
path?: string;
|
|
5
|
+
} | false;
|
|
6
|
+
include?: EntityKind[];
|
|
7
|
+
}
|
|
8
|
+
export declare const asyncapi: (options?: Record<string, unknown>) => PluginInstance<Required<Pick<AsyncApiPluginOptions, "include">> & {
|
|
9
|
+
output: {
|
|
10
|
+
path: string;
|
|
11
|
+
} | null;
|
|
12
|
+
}>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createEntityGraph } from "../core/entityGraph.js";
|
|
2
|
+
import { OperationGenerator } from "../core/generators/OperationGenerator.js";
|
|
3
|
+
import { SchemaGenerator } from "../core/generators/SchemaGenerator.js";
|
|
4
|
+
import { loadFromConfig } from "../core/loadFromConfig.js";
|
|
5
|
+
import { definePlugin } from "../plugins/definePlugin.js";
|
|
6
|
+
import { emitJsonSchemaArtifacts } from "../emitters/json-schema.js";
|
|
7
|
+
function normalizeOutput(output, defaultPath) {
|
|
8
|
+
if (output === false) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
path: output?.path ?? defaultPath,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export const asyncapi = definePlugin((options = {}) => {
|
|
16
|
+
const output = normalizeOutput(options.output, "schemas");
|
|
17
|
+
return {
|
|
18
|
+
name: "asyncapi",
|
|
19
|
+
options: {
|
|
20
|
+
output,
|
|
21
|
+
include: options.include ?? [],
|
|
22
|
+
},
|
|
23
|
+
inject() {
|
|
24
|
+
return {
|
|
25
|
+
getAsyncApi: async () => this.asyncapi,
|
|
26
|
+
getEntityGraph: async () => this.graph,
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
async install() {
|
|
30
|
+
const asyncapi = await loadFromConfig({
|
|
31
|
+
cwd: this.cwd,
|
|
32
|
+
input: this.config.input,
|
|
33
|
+
});
|
|
34
|
+
const schemaGenerator = new SchemaGenerator(asyncapi);
|
|
35
|
+
const operationGenerator = new OperationGenerator(asyncapi);
|
|
36
|
+
const graph = createEntityGraph(asyncapi.resolveNames([
|
|
37
|
+
...(await schemaGenerator.build()),
|
|
38
|
+
...(await operationGenerator.build()),
|
|
39
|
+
]));
|
|
40
|
+
this.asyncapi = asyncapi;
|
|
41
|
+
this.graph = graph;
|
|
42
|
+
this.diagnostics = asyncapi.diagnostics;
|
|
43
|
+
if (!output) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const artifacts = emitJsonSchemaArtifacts({
|
|
47
|
+
graph,
|
|
48
|
+
outputPath: output.path,
|
|
49
|
+
include: options.include,
|
|
50
|
+
});
|
|
51
|
+
for (const artifact of artifacts) {
|
|
52
|
+
this.addArtifact(artifact);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { definePlugin } from "../plugins/definePlugin.js";
|
|
2
|
+
import { emitTypescriptArtifacts } from "../emitters/typescript.js";
|
|
3
|
+
export const typescript = definePlugin((options = {}) => ({
|
|
4
|
+
name: "typescript",
|
|
5
|
+
pre: ["asyncapi"],
|
|
6
|
+
options: {
|
|
7
|
+
output: {
|
|
8
|
+
path: options.output?.path ?? "types",
|
|
9
|
+
},
|
|
10
|
+
include: options.include ?? [],
|
|
11
|
+
},
|
|
12
|
+
async install() {
|
|
13
|
+
const graph = await this.getEntityGraph?.();
|
|
14
|
+
if (!graph) {
|
|
15
|
+
throw new Error("typescript requires asyncapi to initialize the entity graph.");
|
|
16
|
+
}
|
|
17
|
+
const artifacts = await emitTypescriptArtifacts({
|
|
18
|
+
graph,
|
|
19
|
+
outputPath: options.output?.path ?? "types",
|
|
20
|
+
include: options.include,
|
|
21
|
+
});
|
|
22
|
+
for (const artifact of artifacts) {
|
|
23
|
+
this.addArtifact(artifact);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
}));
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { definePlugin } from "../plugins/definePlugin.js";
|
|
2
|
+
import { emitZodArtifacts } from "../emitters/zod.js";
|
|
3
|
+
export const zod = definePlugin((options = {}) => ({
|
|
4
|
+
name: "zod",
|
|
5
|
+
pre: ["asyncapi"],
|
|
6
|
+
options: {
|
|
7
|
+
output: {
|
|
8
|
+
path: options.output?.path ?? "zod",
|
|
9
|
+
},
|
|
10
|
+
include: options.include ?? [],
|
|
11
|
+
},
|
|
12
|
+
async install() {
|
|
13
|
+
const graph = await this.getEntityGraph?.();
|
|
14
|
+
if (!graph) {
|
|
15
|
+
throw new Error("zod requires asyncapi to initialize the entity graph.");
|
|
16
|
+
}
|
|
17
|
+
const artifacts = emitZodArtifacts({
|
|
18
|
+
graph,
|
|
19
|
+
outputPath: options.output?.path ?? "zod",
|
|
20
|
+
include: options.include,
|
|
21
|
+
});
|
|
22
|
+
for (const artifact of artifacts) {
|
|
23
|
+
this.addArtifact(artifact);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
}));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { GenerationContext, PluginContext, PluginInstance, UserConfig } from "../types.js";
|
|
2
|
+
export declare class PluginManager {
|
|
3
|
+
config: UserConfig;
|
|
4
|
+
plugins: PluginInstance[];
|
|
5
|
+
constructor(config: UserConfig);
|
|
6
|
+
run(context: GenerationContext): Promise<GenerationContext>;
|
|
7
|
+
sortByDependencies(plugins: PluginInstance[]): PluginInstance[];
|
|
8
|
+
createPluginContext({ context, plugin, injections, }: {
|
|
9
|
+
context: GenerationContext;
|
|
10
|
+
plugin: PluginInstance;
|
|
11
|
+
injections: Record<string, unknown>;
|
|
12
|
+
}): PluginContext;
|
|
13
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export class PluginManager {
|
|
2
|
+
config;
|
|
3
|
+
plugins;
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.config = config;
|
|
6
|
+
this.plugins = Array.isArray(config.plugins) ? [...config.plugins] : [];
|
|
7
|
+
}
|
|
8
|
+
async run(context) {
|
|
9
|
+
const ordered = this.sortByDependencies(this.plugins);
|
|
10
|
+
const injections = {};
|
|
11
|
+
for (const plugin of ordered) {
|
|
12
|
+
const pluginContext = this.createPluginContext({
|
|
13
|
+
context,
|
|
14
|
+
plugin,
|
|
15
|
+
injections,
|
|
16
|
+
});
|
|
17
|
+
await plugin.install.call(pluginContext, pluginContext);
|
|
18
|
+
if (typeof plugin.inject === "function") {
|
|
19
|
+
Object.assign(injections, plugin.inject.call(pluginContext, pluginContext) ?? {});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return context;
|
|
23
|
+
}
|
|
24
|
+
sortByDependencies(plugins) {
|
|
25
|
+
const availableNames = new Set(plugins.map((plugin) => plugin.name));
|
|
26
|
+
for (const plugin of plugins) {
|
|
27
|
+
const dependencies = [...(plugin.pre ?? []), ...(plugin.post ?? [])];
|
|
28
|
+
const missing = dependencies.filter((name) => !availableNames.has(name));
|
|
29
|
+
if (missing.length > 0) {
|
|
30
|
+
throw new Error(`Plugin "${plugin.name}" requires missing dependencies: ${missing.join(", ")}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const ordered = [];
|
|
34
|
+
const pending = [...plugins];
|
|
35
|
+
const resolvedNames = new Set();
|
|
36
|
+
while (pending.length > 0) {
|
|
37
|
+
const readyIndex = pending.findIndex((plugin) => {
|
|
38
|
+
const dependencies = [...(plugin.pre ?? []), ...(plugin.post ?? [])];
|
|
39
|
+
return dependencies.every((name) => resolvedNames.has(name));
|
|
40
|
+
});
|
|
41
|
+
if (readyIndex === -1) {
|
|
42
|
+
throw new Error(`Cannot resolve plugin order for: ${pending
|
|
43
|
+
.map((plugin) => plugin.name)
|
|
44
|
+
.join(", ")}`);
|
|
45
|
+
}
|
|
46
|
+
const [plugin] = pending.splice(readyIndex, 1);
|
|
47
|
+
ordered.push(plugin);
|
|
48
|
+
resolvedNames.add(plugin.name);
|
|
49
|
+
}
|
|
50
|
+
return ordered;
|
|
51
|
+
}
|
|
52
|
+
createPluginContext({ context, plugin, injections, }) {
|
|
53
|
+
const pluginContext = {
|
|
54
|
+
cwd: context.cwd,
|
|
55
|
+
config: context.config,
|
|
56
|
+
plugin,
|
|
57
|
+
pluginManager: this,
|
|
58
|
+
artifacts: context.artifacts,
|
|
59
|
+
get asyncapi() {
|
|
60
|
+
return context.asyncapi;
|
|
61
|
+
},
|
|
62
|
+
set asyncapi(value) {
|
|
63
|
+
context.asyncapi = value;
|
|
64
|
+
},
|
|
65
|
+
get graph() {
|
|
66
|
+
return context.graph;
|
|
67
|
+
},
|
|
68
|
+
set graph(value) {
|
|
69
|
+
context.graph = value;
|
|
70
|
+
},
|
|
71
|
+
get diagnostics() {
|
|
72
|
+
return context.diagnostics;
|
|
73
|
+
},
|
|
74
|
+
set diagnostics(value) {
|
|
75
|
+
context.diagnostics = Array.isArray(value) ? value : [value];
|
|
76
|
+
},
|
|
77
|
+
addArtifact(artifact) {
|
|
78
|
+
context.artifacts.push(artifact);
|
|
79
|
+
},
|
|
80
|
+
resolveName(name, type = "type") {
|
|
81
|
+
return plugin.resolveName?.call(pluginContext, name, type) ?? name;
|
|
82
|
+
},
|
|
83
|
+
resolvePath(baseName, mode = "split") {
|
|
84
|
+
return (plugin.resolvePath?.call(pluginContext, baseName, mode) ?? baseName);
|
|
85
|
+
},
|
|
86
|
+
...injections,
|
|
87
|
+
};
|
|
88
|
+
return pluginContext;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { dirname, join, resolve } from "node:path";
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import prettier from "prettier";
|
|
4
|
+
function toPosixPath(value) {
|
|
5
|
+
return value.replaceAll("\\", "/");
|
|
6
|
+
}
|
|
7
|
+
function assertSafeOutputRoot(root, cwd) {
|
|
8
|
+
if (root === cwd) {
|
|
9
|
+
throw new Error("Refusing to clean the current working directory. Use a dedicated output.path.");
|
|
10
|
+
}
|
|
11
|
+
const parent = dirname(root);
|
|
12
|
+
if (root === parent) {
|
|
13
|
+
throw new Error(`Refusing to clean filesystem root: ${root}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function createBarrelFiles(artifacts) {
|
|
17
|
+
const groups = new Map();
|
|
18
|
+
for (const artifact of artifacts) {
|
|
19
|
+
if (!artifact.export) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const directory = dirname(artifact.filePath);
|
|
23
|
+
const items = groups.get(directory) ?? [];
|
|
24
|
+
items.push({
|
|
25
|
+
filePath: artifact.filePath,
|
|
26
|
+
export: artifact.export,
|
|
27
|
+
});
|
|
28
|
+
groups.set(directory, items);
|
|
29
|
+
}
|
|
30
|
+
const generated = new Map();
|
|
31
|
+
for (const [directory, items] of [...groups.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
32
|
+
const lines = items
|
|
33
|
+
.sort((left, right) => left.filePath.localeCompare(right.filePath))
|
|
34
|
+
.map((item) => {
|
|
35
|
+
const fileName = item.filePath
|
|
36
|
+
.slice(directory.length + 1)
|
|
37
|
+
.replace(/\.ts$/, ".js");
|
|
38
|
+
const path = `./${toPosixPath(fileName)}`;
|
|
39
|
+
return item.export.kind === "type"
|
|
40
|
+
? `export type { ${item.export.name} } from "${path}";`
|
|
41
|
+
: `export { ${item.export.name} } from "${path}";`;
|
|
42
|
+
});
|
|
43
|
+
generated.set(join(directory, "index.ts"), `${lines.join("\n")}\n`);
|
|
44
|
+
}
|
|
45
|
+
const rootLines = [...groups.keys()]
|
|
46
|
+
.sort((left, right) => left.localeCompare(right))
|
|
47
|
+
.map((directory) => `export * from "./${toPosixPath(join(directory, "index.js"))}";`);
|
|
48
|
+
if (rootLines.length > 0) {
|
|
49
|
+
generated.set("index.ts", `${rootLines.join("\n")}\n`);
|
|
50
|
+
}
|
|
51
|
+
return generated;
|
|
52
|
+
}
|
|
53
|
+
export async function writeArtifacts({ cwd, outDir, artifacts, }) {
|
|
54
|
+
const resolvedOutDir = resolve(cwd, outDir);
|
|
55
|
+
assertSafeOutputRoot(resolvedOutDir, cwd);
|
|
56
|
+
rmSync(resolvedOutDir, { recursive: true, force: true });
|
|
57
|
+
mkdirSync(resolvedOutDir, { recursive: true });
|
|
58
|
+
const prettierConfig = await prettier.resolveConfig(cwd);
|
|
59
|
+
const files = new Map(createBarrelFiles(artifacts));
|
|
60
|
+
for (const artifact of artifacts) {
|
|
61
|
+
files.set(artifact.filePath, artifact.code);
|
|
62
|
+
}
|
|
63
|
+
for (const [relativePath, code] of [...files.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
64
|
+
const absolutePath = resolve(resolvedOutDir, relativePath);
|
|
65
|
+
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
66
|
+
const formatted = await prettier.format(code, {
|
|
67
|
+
...(prettierConfig ?? {}),
|
|
68
|
+
filepath: absolutePath,
|
|
69
|
+
});
|
|
70
|
+
writeFileSync(absolutePath, formatted);
|
|
71
|
+
}
|
|
72
|
+
return resolvedOutDir;
|
|
73
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Diagnostic } from "@asyncapi/parser";
|
|
2
|
+
import type { AsyncApiDocument } from "./core/AsyncApiDocument.js";
|
|
3
|
+
export type EntityKind = "component-schema" | "message-payload" | "reply-payload" | "channel-parameter" | "message-header";
|
|
4
|
+
export interface AsyncApiEntity {
|
|
5
|
+
id: string;
|
|
6
|
+
kind: EntityKind;
|
|
7
|
+
baseName?: string;
|
|
8
|
+
name: string;
|
|
9
|
+
schema: unknown;
|
|
10
|
+
sourcePath: string;
|
|
11
|
+
}
|
|
12
|
+
export interface AsyncApiEntitySeed {
|
|
13
|
+
id: string;
|
|
14
|
+
kind: EntityKind;
|
|
15
|
+
baseName?: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface AsyncApiEntityGraph {
|
|
19
|
+
entities: AsyncApiEntity[];
|
|
20
|
+
byId: Map<string, AsyncApiEntity>;
|
|
21
|
+
}
|
|
22
|
+
export interface GeneratedArtifact {
|
|
23
|
+
kind: "json-schema" | "types" | "zod";
|
|
24
|
+
filePath: string;
|
|
25
|
+
code: string;
|
|
26
|
+
export?: {
|
|
27
|
+
name: string;
|
|
28
|
+
kind: "type" | "value";
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export interface UserConfig {
|
|
32
|
+
input: {
|
|
33
|
+
path: string;
|
|
34
|
+
};
|
|
35
|
+
output: {
|
|
36
|
+
path: string;
|
|
37
|
+
};
|
|
38
|
+
plugins?: PluginInstance[];
|
|
39
|
+
}
|
|
40
|
+
export interface GenerateOptions {
|
|
41
|
+
cwd?: string;
|
|
42
|
+
config?: string;
|
|
43
|
+
input?: string;
|
|
44
|
+
out?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface GenerateResult {
|
|
47
|
+
total: number;
|
|
48
|
+
outDir: string;
|
|
49
|
+
diagnostics: Diagnostic[];
|
|
50
|
+
artifacts: GeneratedArtifact[];
|
|
51
|
+
}
|
|
52
|
+
export interface GenerationContext {
|
|
53
|
+
cwd: string;
|
|
54
|
+
config: UserConfig;
|
|
55
|
+
asyncapi: AsyncApiDocument | null;
|
|
56
|
+
graph: AsyncApiEntityGraph | null;
|
|
57
|
+
diagnostics: Diagnostic[];
|
|
58
|
+
artifacts: GeneratedArtifact[];
|
|
59
|
+
}
|
|
60
|
+
export interface PluginContext extends GenerationContext {
|
|
61
|
+
plugin: PluginInstance;
|
|
62
|
+
pluginManager: unknown;
|
|
63
|
+
addArtifact(artifact: GeneratedArtifact): void;
|
|
64
|
+
resolveName(name: string, type?: "file" | "type"): string;
|
|
65
|
+
resolvePath(baseName: string, mode?: "single" | "split"): string;
|
|
66
|
+
getAsyncApi?(): Promise<AsyncApiDocument | null>;
|
|
67
|
+
getEntityGraph?(): Promise<AsyncApiEntityGraph | null>;
|
|
68
|
+
}
|
|
69
|
+
export interface PluginInstance<TOptions extends Record<string, unknown> = Record<string, unknown>> {
|
|
70
|
+
name: string;
|
|
71
|
+
options: TOptions;
|
|
72
|
+
pre?: string[];
|
|
73
|
+
post?: string[];
|
|
74
|
+
resolveName?(this: PluginContext, name: string, type?: "file" | "type"): string | undefined;
|
|
75
|
+
resolvePath?(this: PluginContext, baseName: string, mode?: "single" | "split"): string | undefined;
|
|
76
|
+
inject?(this: PluginContext, context: PluginContext): Record<string, unknown> | void;
|
|
77
|
+
install(this: PluginContext, context: PluginContext): Promise<void> | void;
|
|
78
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rvct/asyncapi",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "rm -rf dist && tsc -p tsconfig.json && tsc-alias -p tsconfig.json -f",
|
|
7
|
+
"lint": "eslint . && tsc --noEmit -p tsconfig.json",
|
|
8
|
+
"format": "eslint --fix . && prettier --write .",
|
|
9
|
+
"format.check": "eslint . && prettier --check .",
|
|
10
|
+
"test": "npm run test:unit && npm run test:smoke",
|
|
11
|
+
"test:unit": "vitest run --config vitest.config.ts",
|
|
12
|
+
"test:smoke": "npm run build && vitest run --config vitest.smoke.config.ts"
|
|
13
|
+
},
|
|
14
|
+
"description": "Config-driven AsyncAPI to JSON Schema, TypeScript, and Zod generator",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/ravecat/asyncapi.git"
|
|
18
|
+
},
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"module": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": "./dist/index.js",
|
|
24
|
+
"./cli": "./dist/cli.js",
|
|
25
|
+
"./config": "./dist/config.js",
|
|
26
|
+
"./plugins/asyncapi": "./dist/plugins/asyncapi.js",
|
|
27
|
+
"./plugins/typescript": "./dist/plugins/typescript.js",
|
|
28
|
+
"./plugins/zod": "./dist/plugins/zod.js"
|
|
29
|
+
},
|
|
30
|
+
"bin": {
|
|
31
|
+
"asyncapi-codegen": "./dist/cli.js"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"asyncapi",
|
|
41
|
+
"codegen",
|
|
42
|
+
"typescript",
|
|
43
|
+
"zod"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@asyncapi/parser": "^3.6.0",
|
|
47
|
+
"json-schema-to-typescript": "^15.0.4",
|
|
48
|
+
"json-schema-to-zod": "^2.7.0",
|
|
49
|
+
"prettier": "^3.6.2"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@eslint/js": "^9.39.1",
|
|
53
|
+
"@types/node": "^25.6.0",
|
|
54
|
+
"eslint": "^9.39.1",
|
|
55
|
+
"semantic-release": "25.0.3",
|
|
56
|
+
"tsc-alias": "^1.8.16",
|
|
57
|
+
"typescript": "^6.0.2",
|
|
58
|
+
"typescript-eslint": "^8.58.1",
|
|
59
|
+
"vite-tsconfig-paths": "^5.1.4",
|
|
60
|
+
"vitest": "^3.2.4"
|
|
61
|
+
}
|
|
62
|
+
}
|