@eventcatalog/core 4.10.15 → 4.11.1
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/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/astro-output.cjs +60 -1
- package/dist/astro-output.js +1 -1
- package/dist/{chunk-U52FCZQ6.js → chunk-2YJRF4ZX.js} +1 -1
- package/dist/chunk-634NFV5U.js +75 -0
- package/dist/{chunk-I6HUOX4R.js → chunk-AXDNSGAN.js} +1 -1
- package/dist/chunk-DHH5U5PF.js +53 -0
- package/dist/{chunk-A4JRWRCV.js → chunk-DSCIWCX2.js} +1 -1
- package/dist/chunk-FWXEV4LF.js +19 -0
- package/dist/{chunk-Q2ATWE7N.js → chunk-GZVDXCYF.js} +1 -1
- package/dist/{chunk-7LWOCX4R.js → chunk-RYNPEIHQ.js} +1 -1
- package/dist/{chunk-HBVTU6ZP.js → chunk-YYVW4DCO.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/copy-core.cjs +88 -0
- package/dist/copy-core.d.cts +4 -0
- package/dist/copy-core.d.ts +4 -0
- package/dist/copy-core.js +8 -0
- package/dist/eventcatalog.cjs +234 -115
- package/dist/eventcatalog.js +36 -30
- package/dist/federation/federate.js +3 -3
- package/dist/federation/source-provider.js +2 -2
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/update-check.cjs +44 -0
- package/dist/update-check.d.cts +18 -0
- package/dist/update-check.d.ts +18 -0
- package/dist/update-check.js +8 -0
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/astro.config.mjs +8 -1
- package/eventcatalog/public/icons/languages/android.svg +1 -0
- package/eventcatalog/public/icons/languages/angular.svg +1 -0
- package/eventcatalog/public/icons/languages/apple.svg +1 -0
- package/eventcatalog/public/icons/languages/arduino.svg +1 -0
- package/eventcatalog/public/icons/languages/bash.svg +1 -0
- package/eventcatalog/public/icons/languages/bun.svg +1 -0
- package/eventcatalog/public/icons/languages/c.svg +1 -0
- package/eventcatalog/public/icons/languages/clojure.svg +1 -0
- package/eventcatalog/public/icons/languages/cpp.svg +1 -0
- package/eventcatalog/public/icons/languages/crystal.svg +1 -0
- package/eventcatalog/public/icons/languages/csharp.svg +1 -0
- package/eventcatalog/public/icons/languages/css.svg +1 -0
- package/eventcatalog/public/icons/languages/curl.svg +1 -0
- package/eventcatalog/public/icons/languages/dart.svg +1 -0
- package/eventcatalog/public/icons/languages/deno.svg +1 -0
- package/eventcatalog/public/icons/languages/docker.svg +1 -0
- package/eventcatalog/public/icons/languages/elm.svg +1 -0
- package/eventcatalog/public/icons/languages/erlang.svg +1 -0
- package/eventcatalog/public/icons/languages/flutter.svg +1 -0
- package/eventcatalog/public/icons/languages/fortran.svg +1 -0
- package/eventcatalog/public/icons/languages/fsharp.svg +1 -0
- package/eventcatalog/public/icons/languages/git.svg +1 -0
- package/eventcatalog/public/icons/languages/gradle.svg +1 -0
- package/eventcatalog/public/icons/languages/graphql.svg +1 -0
- package/eventcatalog/public/icons/languages/grpc.svg +1 -0
- package/eventcatalog/public/icons/languages/haskell.svg +1 -0
- package/eventcatalog/public/icons/languages/hasura.svg +1 -0
- package/eventcatalog/public/icons/languages/html.svg +1 -0
- package/eventcatalog/public/icons/languages/httpie.svg +1 -0
- package/eventcatalog/public/icons/languages/javascript.svg +1 -0
- package/eventcatalog/public/icons/languages/json-schema.svg +1 -0
- package/eventcatalog/public/icons/languages/json.svg +1 -0
- package/eventcatalog/public/icons/languages/julia.svg +1 -0
- package/eventcatalog/public/icons/languages/jupyter.svg +1 -0
- package/eventcatalog/public/icons/languages/kafka.svg +1 -0
- package/eventcatalog/public/icons/languages/kotlin.svg +1 -0
- package/eventcatalog/public/icons/languages/lua.svg +1 -0
- package/eventcatalog/public/icons/languages/markdown.svg +1 -0
- package/eventcatalog/public/icons/languages/maven.svg +1 -0
- package/eventcatalog/public/icons/languages/mongodb.svg +1 -0
- package/eventcatalog/public/icons/languages/mysql.svg +1 -0
- package/eventcatalog/public/icons/languages/nginx.svg +1 -0
- package/eventcatalog/public/icons/languages/nim.svg +1 -0
- package/eventcatalog/public/icons/languages/ocaml.svg +1 -0
- package/eventcatalog/public/icons/languages/perl.svg +1 -0
- package/eventcatalog/public/icons/languages/php.svg +1 -0
- package/eventcatalog/public/icons/languages/postgresql.svg +1 -0
- package/eventcatalog/public/icons/languages/prisma.svg +1 -0
- package/eventcatalog/public/icons/languages/purescript.svg +1 -0
- package/eventcatalog/public/icons/languages/python.svg +1 -0
- package/eventcatalog/public/icons/languages/r.svg +1 -0
- package/eventcatalog/public/icons/languages/react.svg +1 -0
- package/eventcatalog/public/icons/languages/reasonml.svg +1 -0
- package/eventcatalog/public/icons/languages/redis.svg +1 -0
- package/eventcatalog/public/icons/languages/ruby.svg +1 -0
- package/eventcatalog/public/icons/languages/rust.svg +1 -0
- package/eventcatalog/public/icons/languages/sass.svg +1 -0
- package/eventcatalog/public/icons/languages/scala.svg +1 -0
- package/eventcatalog/public/icons/languages/solidity.svg +1 -0
- package/eventcatalog/public/icons/languages/spring.svg +1 -0
- package/eventcatalog/public/icons/languages/sqlite.svg +1 -0
- package/eventcatalog/public/icons/languages/svelte.svg +1 -0
- package/eventcatalog/public/icons/languages/swagger.svg +1 -0
- package/eventcatalog/public/icons/languages/swift.svg +1 -0
- package/eventcatalog/public/icons/languages/tailwind.svg +1 -0
- package/eventcatalog/public/icons/languages/terraform.svg +1 -0
- package/eventcatalog/public/icons/languages/toml.svg +1 -0
- package/eventcatalog/public/icons/languages/typescript.svg +1 -0
- package/eventcatalog/public/icons/languages/unity.svg +1 -0
- package/eventcatalog/public/icons/languages/vim.svg +1 -0
- package/eventcatalog/public/icons/languages/vue.svg +1 -0
- package/eventcatalog/public/icons/languages/webassembly.svg +1 -0
- package/eventcatalog/public/icons/languages/yaml.svg +1 -0
- package/eventcatalog/public/icons/languages/zig.svg +1 -0
- package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.astro +12 -0
- package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.tsx +176 -0
- package/eventcatalog/src/components/MDX/CodeGroup/MarkdownCodeGroup.tsx +22 -0
- package/eventcatalog/src/components/MDX/CodeGroup/code-group-icons.ts +345 -0
- package/eventcatalog/src/components/MDX/CodeGroup/code-group.css +22 -0
- package/eventcatalog/src/components/MDX/CodeGroup/parse-panels.ts +11 -0
- package/eventcatalog/src/components/MDX/Columns/Column.astro +10 -0
- package/eventcatalog/src/components/MDX/Columns/Columns.astro +14 -0
- package/eventcatalog/src/components/MDX/Columns/MarkdownColumns.tsx +37 -0
- package/eventcatalog/src/components/MDX/Columns/layout.ts +16 -0
- package/eventcatalog/src/components/MDX/components.tsx +6 -0
- package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +1 -1
- package/eventcatalog/src/components/SchemaExplorer/ExamplesViewer.tsx +151 -114
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +510 -248
- package/eventcatalog/src/components/SchemaExplorer/SchemaFlows.tsx +76 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaGraph.tsx +47 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaPageViewer.tsx +11 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaUsage.tsx +17 -0
- package/eventcatalog/src/components/SchemaExplorer/types.ts +48 -3
- package/eventcatalog/src/components/SchemaExplorer/utils.ts +28 -1
- package/eventcatalog/src/content.config.ts +28 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/[...filename].astro +38 -7
- package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro +144 -37
- package/eventcatalog/src/pages/schemas/explorer/index.astro +5 -0
- package/eventcatalog/src/plugins/empty-collection-warning.d.ts +21 -0
- package/eventcatalog/src/plugins/empty-collection-warning.mjs +107 -0
- package/eventcatalog/src/plugins/quiet-empty-collections-logger.mjs +11 -0
- package/eventcatalog/src/remark-plugins/code-group.ts +39 -0
- package/eventcatalog/src/remark-plugins/markdown-code-group.ts +59 -0
- package/eventcatalog/src/remark-plugins/markdown-columns.ts +66 -0
- package/eventcatalog/src/utils/collections/channels.ts +12 -4
- package/eventcatalog/src/utils/collections/examples.ts +133 -41
- package/eventcatalog/src/utils/collections/flows.ts +43 -0
- package/eventcatalog/src/utils/collections/messages.ts +2 -2
- package/eventcatalog/src/utils/collections/schemas.ts +15 -0
- package/eventcatalog/src/utils/node-graphs/message-node-graph.ts +17 -15
- package/eventcatalog/src/utils/schema-explorer.ts +73 -7
- package/eventcatalog/src/utils/schema-usage-graph.ts +92 -0
- package/package.json +5 -4
- package/dist/chunk-RDYYZQYU.js +0 -18
- package/eventcatalog/src/components/EnvironmentDropdown.test.ts +0 -64
- package/eventcatalog/src/components/Grids/message-link.spec.ts +0 -59
- package/eventcatalog/src/components/MDX/CustomProperties/custom-properties.spec.ts +0 -38
- package/eventcatalog/src/components/MDX/SchemaViewer/schema-viewer-utils.spec.ts +0 -116
- package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.test.tsx +0 -114
- package/eventcatalog/src/components/SchemaExplorer/ProtobufSchemaViewer.test.tsx +0 -91
- package/eventcatalog/src/components/SchemaExplorer/SchemaViewerHover.test.tsx +0 -33
- package/eventcatalog/src/components/SchemaExplorer/protobuf-validation.test.ts +0 -84
- package/eventcatalog/src/components/Search/search-utils.spec.ts +0 -173
- package/eventcatalog/src/components/SideNav/NestedSideBar/utils.spec.ts +0 -105
- package/eventcatalog/src/components/navigation-progress.spec.ts +0 -68
- package/eventcatalog/src/enterprise/directory/user-team-directory.spec.ts +0 -527
- package/eventcatalog/src/enterprise/fields/field-extractor.test.ts +0 -302
- package/eventcatalog/src/enterprise/fields/fields-db.test.ts +0 -185
- package/eventcatalog/src/stores/eventcatalog-store.spec.ts +0 -60
- package/eventcatalog/src/utils/collections/glob-loader.spec.ts +0 -87
- package/dist/{chunk-6K7XZAYI.js → chunk-A2RZR3U4.js} +3 -3
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
3
4
|
import yaml from 'js-yaml';
|
|
4
5
|
import { getResourceBasePath } from '@utils/resource-files';
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* A usage example for a message: any text file inside the message's `examples/` folder.
|
|
9
|
+
*/
|
|
6
10
|
export interface MessageExample {
|
|
11
|
+
/** File path relative to the `examples/` folder, for example `basic-order.json`. */
|
|
7
12
|
fileName: string;
|
|
13
|
+
/** Display title: frontmatter `title`, else the first level-one heading, else the file name. */
|
|
8
14
|
title: string;
|
|
15
|
+
/** File extension without the leading dot. */
|
|
9
16
|
extension: string;
|
|
10
|
-
|
|
17
|
+
/** Markdown is rendered as prose; other formats and unsupported MDX are rendered as source. */
|
|
18
|
+
renderMode: 'markdown' | 'code';
|
|
19
|
+
/** Optional short description from frontmatter `summary`. */
|
|
11
20
|
summary?: string;
|
|
21
|
+
/** Optional usage instructions from the legacy examples config. */
|
|
12
22
|
usage?: string;
|
|
23
|
+
/** File contents. Markdown frontmatter is removed. */
|
|
24
|
+
content: string;
|
|
13
25
|
}
|
|
14
26
|
|
|
15
27
|
interface ExampleConfig {
|
|
@@ -18,68 +30,148 @@ interface ExampleConfig {
|
|
|
18
30
|
usage?: string;
|
|
19
31
|
}
|
|
20
32
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const jsonPath = path.join(examplesDir, 'examples.config.json');
|
|
25
|
-
|
|
26
|
-
let configPath: string | null = null;
|
|
27
|
-
if (fs.existsSync(yamlPath)) configPath = yamlPath;
|
|
28
|
-
else if (fs.existsSync(ymlPath)) configPath = ymlPath;
|
|
29
|
-
else if (fs.existsSync(jsonPath)) configPath = jsonPath;
|
|
33
|
+
const CONFIG_FILES = new Set(['examples.config.yaml', 'examples.config.yml', 'examples.config.json']);
|
|
34
|
+
const MARKDOWN_EXTENSIONS = new Set(['.md', '.mdx']);
|
|
35
|
+
const CLIENT_RENDERED_MDX_COMPONENTS = new Set(['Column', 'Columns', 'CodeGroup']);
|
|
30
36
|
|
|
37
|
+
const loadConfig = (examplesDir: string): Record<string, ExampleConfig> => {
|
|
38
|
+
const configPath = ['examples.config.yaml', 'examples.config.yml', 'examples.config.json']
|
|
39
|
+
.map((fileName) => path.join(examplesDir, fileName))
|
|
40
|
+
.find(fs.existsSync);
|
|
31
41
|
if (!configPath) return {};
|
|
32
42
|
|
|
33
43
|
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
return (yaml.load(raw) as Record<string, ExampleConfig>) || {};
|
|
38
|
-
}
|
|
44
|
+
return configPath.endsWith('.json') ? JSON.parse(raw) || {} : (yaml.load(raw) as Record<string, ExampleConfig>) || {};
|
|
45
|
+
};
|
|
39
46
|
|
|
40
|
-
const
|
|
47
|
+
const humanizeFileName = (fileName: string) =>
|
|
48
|
+
path
|
|
49
|
+
.parse(fileName)
|
|
50
|
+
.name.replace(/[-_]+/g, ' ')
|
|
51
|
+
.replace(/\s+/g, ' ')
|
|
52
|
+
.trim()
|
|
53
|
+
.replace(/^\w/, (character) => character.toUpperCase());
|
|
41
54
|
|
|
42
|
-
|
|
43
|
-
const results:
|
|
55
|
+
const collectExampleFiles = (dir: string, baseDir: string): string[] => {
|
|
56
|
+
const results: string[] = [];
|
|
44
57
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
45
58
|
|
|
46
59
|
for (const entry of entries) {
|
|
47
60
|
const fullPath = path.join(dir, entry.name);
|
|
48
61
|
if (entry.isDirectory()) {
|
|
49
|
-
results.push(...
|
|
50
|
-
} else if (entry.isFile() && !CONFIG_FILES.
|
|
51
|
-
results.push(
|
|
52
|
-
fileName: path.relative(baseDir, fullPath),
|
|
53
|
-
title: path.parse(entry.name).name,
|
|
54
|
-
extension: path.parse(entry.name).ext.slice(1),
|
|
55
|
-
content: fs.readFileSync(fullPath, 'utf-8'),
|
|
56
|
-
});
|
|
62
|
+
results.push(...collectExampleFiles(fullPath, baseDir));
|
|
63
|
+
} else if (entry.isFile() && !CONFIG_FILES.has(entry.name)) {
|
|
64
|
+
results.push(path.relative(baseDir, fullPath));
|
|
57
65
|
}
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
return results;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const hasUnsupportedMdxComponent = (content: string) =>
|
|
72
|
+
[...content.matchAll(/<([A-Z][A-Za-z0-9.]*)\b/g)].some((match) => !CLIENT_RENDERED_MDX_COMPONENTS.has(match[1]));
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Splits an example file into its title and Markdown body. The title comes from frontmatter,
|
|
76
|
+
* else the leading level-one heading, else the file name. The body is left as written.
|
|
77
|
+
*/
|
|
78
|
+
export const parseExampleFile = (fileName: string, raw: string): MessageExample => {
|
|
79
|
+
const { data, content } = matter(raw);
|
|
80
|
+
const body = content.replace(/^\s*\n/, '');
|
|
81
|
+
let title = typeof data.title === 'string' && data.title.trim() ? data.title.trim() : undefined;
|
|
82
|
+
const extension = path.extname(fileName).slice(1).toLowerCase();
|
|
83
|
+
|
|
84
|
+
const headingMatch = body.match(/^#[ \t]+(.+)(?:\r?\n|$)/);
|
|
85
|
+
if (headingMatch) title = title ?? headingMatch[1].trim();
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
fileName: fileName.replace(/\\/g, '/'),
|
|
89
|
+
title: title ?? humanizeFileName(fileName),
|
|
90
|
+
extension,
|
|
91
|
+
renderMode: extension === 'mdx' && hasUnsupportedMdxComponent(body) ? 'code' : 'markdown',
|
|
92
|
+
...(typeof data.summary === 'string' && data.summary.trim() ? { summary: data.summary.trim() } : {}),
|
|
93
|
+
content: body.trim(),
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const parseRawExampleFile = (fileName: string, raw: string, config: ExampleConfig = {}): MessageExample => ({
|
|
98
|
+
fileName: fileName.replace(/\\/g, '/'),
|
|
99
|
+
title: config.name?.trim() || humanizeFileName(fileName),
|
|
100
|
+
extension: path.extname(fileName).slice(1).toLowerCase(),
|
|
101
|
+
renderMode: 'code',
|
|
102
|
+
...(config.summary?.trim() ? { summary: config.summary.trim() } : {}),
|
|
103
|
+
...(config.usage?.trim() ? { usage: config.usage.trim() } : {}),
|
|
104
|
+
content: raw,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/** The parts of an `examples` collection entry the schema pages need. */
|
|
108
|
+
export interface ExampleEntryLike {
|
|
109
|
+
id: string;
|
|
110
|
+
filePath?: string;
|
|
111
|
+
body?: string;
|
|
112
|
+
data: { title?: string; summary?: string };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const normalize = (value: string) => path.resolve(value).replace(/\\/g, '/');
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Selects the example entries that belong to a message: the files under the `examples/`
|
|
119
|
+
* folder beside the message file, sorted by path.
|
|
120
|
+
*/
|
|
121
|
+
export function getExampleEntriesForMessage<T extends ExampleEntryLike>(entries: T[], messageFilePath?: string): T[] {
|
|
122
|
+
if (!messageFilePath) return [];
|
|
123
|
+
const examplesDir = `${normalize(path.dirname(messageFilePath))}/examples/`;
|
|
124
|
+
|
|
125
|
+
return entries
|
|
126
|
+
.filter((entry) => entry.filePath && normalize(entry.filePath).startsWith(examplesDir))
|
|
127
|
+
.sort((a, b) => (a.filePath as string).localeCompare(b.filePath as string));
|
|
61
128
|
}
|
|
62
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Display details for a collection entry, mirroring `parseExampleFile`: frontmatter title,
|
|
132
|
+
* else the first level-one heading, else the file name.
|
|
133
|
+
*/
|
|
134
|
+
export function getExampleEntryDetails(entry: ExampleEntryLike, examplesDir?: string) {
|
|
135
|
+
const fileName =
|
|
136
|
+
entry.filePath && examplesDir
|
|
137
|
+
? path.relative(examplesDir, entry.filePath).replace(/\\/g, '/')
|
|
138
|
+
: path.basename(entry.filePath ?? entry.id);
|
|
139
|
+
const parsed = parseExampleFile(fileName, entry.body ?? '');
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
fileName,
|
|
143
|
+
title: entry.data.title?.trim() || parsed.title,
|
|
144
|
+
summary: entry.data.summary?.trim() || parsed.summary,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Reads usage examples stored in the `examples/` folder next to a message.
|
|
150
|
+
*/
|
|
63
151
|
export function getExamplesForResource(resource: { filePath?: string }): MessageExample[] {
|
|
64
152
|
const basePath = getResourceBasePath(resource);
|
|
65
153
|
if (!basePath) return [];
|
|
66
154
|
|
|
67
155
|
const examplesDir = path.join(basePath, 'examples');
|
|
68
156
|
if (!fs.existsSync(examplesDir)) return [];
|
|
69
|
-
|
|
70
157
|
const config = loadConfig(examplesDir);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
.map((
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
158
|
+
|
|
159
|
+
return collectExampleFiles(examplesDir, examplesDir)
|
|
160
|
+
.sort((a, b) => a.localeCompare(b))
|
|
161
|
+
.map((fileName) => {
|
|
162
|
+
const raw = fs.readFileSync(path.join(examplesDir, fileName), 'utf-8');
|
|
163
|
+
const normalizedFileName = fileName.replace(/\\/g, '/');
|
|
164
|
+
const extension = path.extname(fileName).toLowerCase();
|
|
165
|
+
if (MARKDOWN_EXTENSIONS.has(extension)) {
|
|
166
|
+
const example = parseExampleFile(fileName, raw);
|
|
167
|
+
const metadata = config[normalizedFileName];
|
|
168
|
+
return {
|
|
169
|
+
...example,
|
|
170
|
+
...(metadata?.name?.trim() ? { title: metadata.name.trim() } : {}),
|
|
171
|
+
...(metadata?.summary?.trim() ? { summary: metadata.summary.trim() } : {}),
|
|
172
|
+
...(metadata?.usage?.trim() ? { usage: metadata.usage.trim() } : {}),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return parseRawExampleFile(fileName, raw, config[normalizedFileName]);
|
|
176
|
+
});
|
|
85
177
|
}
|
|
@@ -146,3 +146,46 @@ export const getFlowsNotInAnyResource = async (): Promise<Flow[]> => {
|
|
|
146
146
|
});
|
|
147
147
|
return flowsNotInAnyResource;
|
|
148
148
|
};
|
|
149
|
+
|
|
150
|
+
/** A flow that includes a message in one of its steps, reduced to what the schema pages display. */
|
|
151
|
+
export interface FlowReference {
|
|
152
|
+
id: string;
|
|
153
|
+
version: string;
|
|
154
|
+
name?: string;
|
|
155
|
+
summary?: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Finds the flows whose steps send or receive each of the given messages. Step pointers are
|
|
160
|
+
* matched the same way flow diagrams resolve them (exact version, then semver range, then
|
|
161
|
+
* latest). Returns a map keyed by `${collection}:${id}:${version}` of the resolved message.
|
|
162
|
+
*/
|
|
163
|
+
export const getFlowsForMessages = async (
|
|
164
|
+
messages: { collection: string; data: { id: string; version: string } }[]
|
|
165
|
+
): Promise<Map<string, FlowReference[]>> => {
|
|
166
|
+
const flows = await getCollection('flows');
|
|
167
|
+
const messageMap = createVersionedMap(messages);
|
|
168
|
+
const flowsByMessage = new Map<string, FlowReference[]>();
|
|
169
|
+
|
|
170
|
+
for (const flow of flows) {
|
|
171
|
+
if (flow.data.hidden === true) continue;
|
|
172
|
+
const reference: FlowReference = {
|
|
173
|
+
id: flow.data.id,
|
|
174
|
+
version: flow.data.version,
|
|
175
|
+
...(flow.data.name ? { name: flow.data.name } : {}),
|
|
176
|
+
...(flow.data.summary ? { summary: flow.data.summary } : {}),
|
|
177
|
+
};
|
|
178
|
+
const seen = new Set<string>();
|
|
179
|
+
for (const step of flow.data.steps ?? []) {
|
|
180
|
+
if (!step.message) continue;
|
|
181
|
+
const message = findInMap(messageMap, step.message.id, step.message.version);
|
|
182
|
+
if (!message) continue;
|
|
183
|
+
const key = `${message.collection}:${message.data.id}:${message.data.version}`;
|
|
184
|
+
if (seen.has(key)) continue;
|
|
185
|
+
seen.add(key);
|
|
186
|
+
flowsByMessage.set(key, [...(flowsByMessage.get(key) ?? []), reference]);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return flowsByMessage;
|
|
191
|
+
};
|
|
@@ -49,7 +49,7 @@ export const hydrateProducersAndConsumers = ({
|
|
|
49
49
|
const toResult = <T extends CollectionEntry<'agents'> | CollectionEntry<'services'> | CollectionEntry<'data-products'>>(
|
|
50
50
|
resource: T
|
|
51
51
|
) => {
|
|
52
|
-
if (!hydrate) return { id: resource.data.id, version: resource.data.version };
|
|
52
|
+
if (!hydrate) return { id: resource.data.id, version: resource.data.version, collection: resource.collection };
|
|
53
53
|
return resource;
|
|
54
54
|
};
|
|
55
55
|
|
|
@@ -193,7 +193,7 @@ export const lookupProducersAndConsumers = ({ message, index, hydrate = true }:
|
|
|
193
193
|
const toResult = <T extends CollectionEntry<'agents'> | CollectionEntry<'services'> | CollectionEntry<'data-products'>>(
|
|
194
194
|
resource: T
|
|
195
195
|
) => {
|
|
196
|
-
if (!hydrate) return { id: resource.data.id, version: resource.data.version };
|
|
196
|
+
if (!hydrate) return { id: resource.data.id, version: resource.data.version, collection: resource.collection };
|
|
197
197
|
return resource;
|
|
198
198
|
};
|
|
199
199
|
|
|
@@ -39,6 +39,21 @@ export const getSchemaURL = (resource: CollectionEntry<PageTypes> | any) => {
|
|
|
39
39
|
return path.join(publicPath, schemaFilePath ?? '');
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
+
/** File extension EventCatalog uses for a schema format, for schemas that have no file name. */
|
|
43
|
+
export const getSchemaExtensionForFormat = (format?: string) => {
|
|
44
|
+
switch (format?.toLowerCase()) {
|
|
45
|
+
case 'jsonschema':
|
|
46
|
+
case 'json':
|
|
47
|
+
return 'json';
|
|
48
|
+
case 'avro':
|
|
49
|
+
return 'avsc';
|
|
50
|
+
case 'protobuf':
|
|
51
|
+
return 'proto';
|
|
52
|
+
default:
|
|
53
|
+
return format ?? '';
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
42
57
|
export const getSchemaFormatFromURL = (url: string) => {
|
|
43
58
|
const pathParts = url.split('.');
|
|
44
59
|
const format = pathParts[pathParts.length - 1];
|
|
@@ -515,19 +515,21 @@ const getNodesAndEdges = async ({
|
|
|
515
515
|
(receive) => receive.id === message.data.id && versionMatches(receive.version, message.data.version)
|
|
516
516
|
)?.from ?? [];
|
|
517
517
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
)
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
channelMap,
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
518
|
+
const resolvedProducerChannels = producerChannels
|
|
519
|
+
.map(
|
|
520
|
+
(producerChannel) =>
|
|
521
|
+
findInMap(channelMap, producerChannel.id, producerChannel.version) as CollectionEntry<'channels'> | undefined
|
|
522
|
+
)
|
|
523
|
+
.filter((resolved): resolved is CollectionEntry<'channels'> => resolved !== undefined);
|
|
524
|
+
const resolvedConsumerChannels = consumerChannels
|
|
525
|
+
.map(
|
|
526
|
+
(consumerChannel) =>
|
|
527
|
+
findInMap(channelMap, consumerChannel.id, consumerChannel.version) as CollectionEntry<'channels'> | undefined
|
|
528
|
+
)
|
|
529
|
+
.filter((resolved): resolved is CollectionEntry<'channels'> => resolved !== undefined);
|
|
530
|
+
|
|
531
|
+
for (const producerChannelValue of resolvedProducerChannels) {
|
|
532
|
+
for (const consumerChannelValue of resolvedConsumerChannels) {
|
|
531
533
|
const channelChainToRender = getChannelChain(producerChannelValue, consumerChannelValue, channels);
|
|
532
534
|
|
|
533
535
|
// If there is a chain between them we need to render them al
|
|
@@ -573,8 +575,8 @@ const getNodesAndEdges = async ({
|
|
|
573
575
|
}
|
|
574
576
|
}
|
|
575
577
|
|
|
576
|
-
// If producer does not have
|
|
577
|
-
if (
|
|
578
|
+
// If producer does not have any resolved channels, connect via the consumer channel
|
|
579
|
+
if (resolvedProducerChannels.length === 0 && channel) {
|
|
578
580
|
// Create the channel node
|
|
579
581
|
nodes.push(
|
|
580
582
|
createNode({
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import type { SchemaItem, SchemaDetails, Producer, Consumer } from '@components/SchemaExplorer/types';
|
|
2
|
+
import type { SchemaItem, SchemaDetails, Producer, Consumer, MessageChannel } from '@components/SchemaExplorer/types';
|
|
3
3
|
import { getEvents } from '@utils/collections/events';
|
|
4
4
|
import { getCommands } from '@utils/collections/commands';
|
|
5
5
|
import { getQueries } from '@utils/collections/queries';
|
|
6
6
|
import { getServices, getSpecificationsForService } from '@utils/collections/services';
|
|
7
7
|
import { getDomains, getSpecificationsForDomain } from '@utils/collections/domains';
|
|
8
8
|
import { getDataProducts } from '@utils/collections/data-products';
|
|
9
|
+
import { getAgents } from '@utils/collections/agents';
|
|
10
|
+
import { getFlowsForMessages } from '@utils/collections/flows';
|
|
9
11
|
import { getOwner } from '@utils/collections/owners';
|
|
10
12
|
import { buildUrl } from '@utils/url-builder';
|
|
13
|
+
import { getSchemaExtensionForFormat } from '@utils/collections/schemas';
|
|
11
14
|
import { resourceFileExists, readResourceFile } from '@utils/resource-files';
|
|
12
15
|
import { getExamplesForResource } from '@utils/collections/examples';
|
|
16
|
+
import { attachFlowGraphs, getMessageUsageGraph } from '@utils/schema-usage-graph';
|
|
13
17
|
import { getCollection } from 'astro:content';
|
|
14
18
|
import path from 'path';
|
|
15
19
|
|
|
@@ -37,12 +41,51 @@ async function buildRegistry() {
|
|
|
37
41
|
|
|
38
42
|
// Fetch all services
|
|
39
43
|
const services = await getServices({ getAllVersions: true });
|
|
44
|
+
// Producers and consumers can be services, agents, or data products. Index all three so
|
|
45
|
+
// compact { id, version } references can be resolved to a resource for display.
|
|
46
|
+
const [agents, dataProductsForRelationships] = await Promise.all([
|
|
47
|
+
getAgents({ getAllVersions: true }),
|
|
48
|
+
getDataProducts({ getAllVersions: true }),
|
|
49
|
+
]);
|
|
50
|
+
type RelationshipResource = (typeof services)[number] | (typeof agents)[number] | (typeof dataProductsForRelationships)[number];
|
|
51
|
+
const relationshipResources: RelationshipResource[] = [...services, ...agents, ...dataProductsForRelationships];
|
|
52
|
+
const resourcesByReference = new Map<string, RelationshipResource>();
|
|
53
|
+
const latestResourceByCollectionAndId = new Map<string, RelationshipResource>();
|
|
54
|
+
for (const resource of relationshipResources) {
|
|
55
|
+
const collectionAndId = `${resource.collection}:${resource.data.id}`;
|
|
56
|
+
resourcesByReference.set(`${collectionAndId}:${resource.data.version}`, resource);
|
|
57
|
+
if (!latestResourceByCollectionAndId.has(collectionAndId)) latestResourceByCollectionAndId.set(collectionAndId, resource);
|
|
58
|
+
}
|
|
59
|
+
const toSchemaRelationships = (references: unknown): Producer[] =>
|
|
60
|
+
((references as { id: string; version: string; collection?: Producer['collection'] }[] | undefined) ?? []).map(
|
|
61
|
+
(reference) => {
|
|
62
|
+
const collection = reference.collection ?? 'services';
|
|
63
|
+
const collectionAndId = `${collection}:${reference.id}`;
|
|
64
|
+
const resource =
|
|
65
|
+
resourcesByReference.get(`${collectionAndId}:${reference.version}`) ??
|
|
66
|
+
latestResourceByCollectionAndId.get(collectionAndId);
|
|
67
|
+
return {
|
|
68
|
+
id: reference.id,
|
|
69
|
+
version: reference.version,
|
|
70
|
+
collection,
|
|
71
|
+
...(resource?.data.name ? { name: resource.data.name } : {}),
|
|
72
|
+
...(resource?.data.summary ? { summary: resource.data.summary } : {}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
const toMessageChannels = (channels: unknown): MessageChannel[] =>
|
|
77
|
+
((channels as { data: { id: string; version: string; name?: string } }[] | undefined) ?? []).map((channel) => ({
|
|
78
|
+
id: channel.data.id,
|
|
79
|
+
version: channel.data.version,
|
|
80
|
+
...(channel.data.name ? { name: channel.data.name } : {}),
|
|
81
|
+
}));
|
|
40
82
|
|
|
41
83
|
// Combine all messages
|
|
42
84
|
const allMessages = [...events, ...commands, ...queries];
|
|
43
85
|
const messagesBySchemaReference = new Map(
|
|
44
86
|
allMessages.map((message) => [`${message.collection}:${message.data.id}:${message.data.version}`, message])
|
|
45
87
|
);
|
|
88
|
+
const flowsByMessage = await getFlowsForMessages(allMessages);
|
|
46
89
|
|
|
47
90
|
// Read message schemas from the generated schemas collection.
|
|
48
91
|
const messagesWithSchemas = await Promise.all(
|
|
@@ -51,11 +94,16 @@ async function buildRegistry() {
|
|
|
51
94
|
`${schema.data.message.collectionName}:${schema.data.message.id}:${schema.data.message.version}`
|
|
52
95
|
);
|
|
53
96
|
const schemaPath = schema.data.file || schema.data.source.path || '';
|
|
54
|
-
const schemaExtension = path.extname(schemaPath).slice(1) || schema.data.format;
|
|
97
|
+
const schemaExtension = path.extname(schemaPath).slice(1) || getSchemaExtensionForFormat(schema.data.format);
|
|
55
98
|
// The collection types describe raw content references. With
|
|
56
|
-
// hydrateServices: false, the loaders return compact { id, version } pairs
|
|
57
|
-
|
|
58
|
-
const
|
|
99
|
+
// hydrateServices: false, the loaders return compact { id, version } pairs,
|
|
100
|
+
// so look the service name and summary up for display.
|
|
101
|
+
const producers = toSchemaRelationships(message?.data.producers);
|
|
102
|
+
const consumers = toSchemaRelationships(message?.data.consumers);
|
|
103
|
+
const channels = toMessageChannels(message?.data.messageChannels);
|
|
104
|
+
const flows =
|
|
105
|
+
flowsByMessage.get(`${schema.data.message.collectionName}:${schema.data.message.id}:${schema.data.message.version}`) ??
|
|
106
|
+
[];
|
|
59
107
|
|
|
60
108
|
return {
|
|
61
109
|
collection: schema.data.message.collectionName,
|
|
@@ -70,22 +118,40 @@ async function buildRegistry() {
|
|
|
70
118
|
// regardless of how many resources reference the message.
|
|
71
119
|
producerName: producers[0]?.id,
|
|
72
120
|
},
|
|
73
|
-
loadDetails: () => {
|
|
121
|
+
loadDetails: async () => {
|
|
74
122
|
let examples: SchemaDetails['examples'] = [];
|
|
123
|
+
let graph: SchemaDetails['graph'];
|
|
75
124
|
if (message) {
|
|
76
125
|
try {
|
|
77
126
|
examples = getExamplesForResource(message);
|
|
78
127
|
} catch (error) {
|
|
79
128
|
console.error(`Error reading examples for ${message.data.id}:`, error);
|
|
80
129
|
}
|
|
130
|
+
try {
|
|
131
|
+
graph = await getMessageUsageGraph({
|
|
132
|
+
collection: message.collection,
|
|
133
|
+
id: message.data.id,
|
|
134
|
+
version: message.data.version,
|
|
135
|
+
});
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error(`Error building usage graph for ${message.data.id}:`, error);
|
|
138
|
+
}
|
|
81
139
|
}
|
|
82
140
|
return {
|
|
83
141
|
schemaContent: schema.data.content || '',
|
|
84
142
|
examples,
|
|
85
|
-
|
|
143
|
+
...(graph ? { graph } : {}),
|
|
144
|
+
data: {
|
|
145
|
+
producers,
|
|
146
|
+
consumers,
|
|
147
|
+
flows: await attachFlowGraphs(flows, { id: schema.data.message.id, version: schema.data.message.version }),
|
|
148
|
+
channels,
|
|
149
|
+
},
|
|
86
150
|
};
|
|
87
151
|
},
|
|
88
152
|
schemaExtension,
|
|
153
|
+
...(schema.data.name ? { schemaName: schema.data.name } : {}),
|
|
154
|
+
...(schema.data.ref ? { schemaRef: schema.data.ref, source: schema.data.source } : {}),
|
|
89
155
|
};
|
|
90
156
|
})
|
|
91
157
|
);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Edge, Node } from '@xyflow/react';
|
|
2
|
+
import config from '@config';
|
|
3
|
+
import {
|
|
4
|
+
getNodesAndEdgesForCommands,
|
|
5
|
+
getNodesAndEdgesForEvents,
|
|
6
|
+
getNodesAndEdgesForQueries,
|
|
7
|
+
} from '@utils/node-graphs/message-node-graph';
|
|
8
|
+
import { getNodesAndEdges as getNodesAndEdgesForFlow } from '@utils/node-graphs/flows-node-graph';
|
|
9
|
+
import { compactVisualiserGraph } from '@utils/node-graphs/compact-visualiser-graph';
|
|
10
|
+
|
|
11
|
+
export interface MessageUsageGraph {
|
|
12
|
+
nodes: Node[];
|
|
13
|
+
edges: Edge[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const graphBuilders = {
|
|
17
|
+
events: getNodesAndEdgesForEvents,
|
|
18
|
+
commands: getNodesAndEdgesForCommands,
|
|
19
|
+
queries: getNodesAndEdgesForQueries,
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The same producer → message → consumer graph the message docs page renders, reduced to the
|
|
24
|
+
* fields the visualiser needs so it can be sent to the schema pages.
|
|
25
|
+
*/
|
|
26
|
+
export const getMessageUsageGraph = async ({
|
|
27
|
+
collection,
|
|
28
|
+
id,
|
|
29
|
+
version,
|
|
30
|
+
}: {
|
|
31
|
+
collection: string;
|
|
32
|
+
id: string;
|
|
33
|
+
version: string;
|
|
34
|
+
}): Promise<MessageUsageGraph | undefined> => {
|
|
35
|
+
const build = graphBuilders[collection as keyof typeof graphBuilders];
|
|
36
|
+
if (!build) return undefined;
|
|
37
|
+
const { nodes, edges } = await build({
|
|
38
|
+
id,
|
|
39
|
+
version,
|
|
40
|
+
mode: 'simple',
|
|
41
|
+
channelRenderMode: config.visualiser?.channels?.renderMode === 'single' ? 'single' : 'flat',
|
|
42
|
+
});
|
|
43
|
+
return compactVisualiserGraph(nodes as Node[], edges as Edge[]);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Identifies the message a diagram is being viewed from, so its node can be highlighted. */
|
|
47
|
+
export interface FocusedMessage {
|
|
48
|
+
id: string;
|
|
49
|
+
version: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The flow diagram the flow docs page renders, compacted for the schema pages. Steps that
|
|
54
|
+
* send or receive the focused message get the visualiser's "Viewing" indicator.
|
|
55
|
+
*/
|
|
56
|
+
export const getFlowGraph = async ({
|
|
57
|
+
id,
|
|
58
|
+
version,
|
|
59
|
+
focus,
|
|
60
|
+
}: {
|
|
61
|
+
id: string;
|
|
62
|
+
version: string;
|
|
63
|
+
focus?: FocusedMessage;
|
|
64
|
+
}): Promise<MessageUsageGraph> => {
|
|
65
|
+
const { nodes, edges } = await getNodesAndEdgesForFlow({ id, version, mode: 'simple' });
|
|
66
|
+
const focused = (nodes as Node[]).map((node) => {
|
|
67
|
+
const message = node.data?.message as { id?: string; version?: string } | undefined;
|
|
68
|
+
return focus && message?.id === focus.id && message?.version === focus.version
|
|
69
|
+
? { ...node, data: { ...node.data, isFocused: true } }
|
|
70
|
+
: node;
|
|
71
|
+
});
|
|
72
|
+
return compactVisualiserGraph(focused, edges as Edge[]);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Adds each flow's diagram to the flow usages, highlighting the focused message's steps.
|
|
77
|
+
* A flow whose diagram fails to build is kept without one.
|
|
78
|
+
*/
|
|
79
|
+
export const attachFlowGraphs = <T extends { id: string; version: string }>(
|
|
80
|
+
flows: T[],
|
|
81
|
+
focus?: FocusedMessage
|
|
82
|
+
): Promise<(T & { graph?: MessageUsageGraph })[]> =>
|
|
83
|
+
Promise.all(
|
|
84
|
+
flows.map(async (flow) => {
|
|
85
|
+
try {
|
|
86
|
+
return { ...flow, graph: await getFlowGraph({ ...flow, focus }) };
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error(`Error building flow graph for ${flow.id}:`, error);
|
|
89
|
+
return flow;
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
);
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"license": "SEE LICENSE IN LICENSE",
|
|
9
9
|
"type": "module",
|
|
10
|
-
"version": "4.
|
|
10
|
+
"version": "4.11.1",
|
|
11
11
|
"publishConfig": {
|
|
12
12
|
"access": "public"
|
|
13
13
|
},
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"!eventcatalog/dist/",
|
|
28
28
|
"!eventcatalog/node_modules/",
|
|
29
29
|
"!eventcatalog/**/__tests__/",
|
|
30
|
+
"!eventcatalog/**/*.{spec,test}.{js,jsx,cjs,mjs,ts,tsx,cts,mts}",
|
|
30
31
|
"bin/",
|
|
31
32
|
"dist/"
|
|
32
33
|
],
|
|
@@ -119,9 +120,9 @@
|
|
|
119
120
|
"update-notifier": "^7.3.1",
|
|
120
121
|
"uuid": "^11.1.1",
|
|
121
122
|
"zod": "^4.3.6",
|
|
122
|
-
"@eventcatalog/linter": "1.1.
|
|
123
|
-
"@eventcatalog/
|
|
124
|
-
"@eventcatalog/
|
|
123
|
+
"@eventcatalog/linter": "1.1.20",
|
|
124
|
+
"@eventcatalog/visualiser": "^4.1.4",
|
|
125
|
+
"@eventcatalog/sdk": "2.29.1"
|
|
125
126
|
},
|
|
126
127
|
"devDependencies": {
|
|
127
128
|
"@astrojs/check": "^0.9.10",
|
package/dist/chunk-RDYYZQYU.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// src/astro-output.ts
|
|
2
|
-
var createAstroLineFilter = () => {
|
|
3
|
-
return (line) => {
|
|
4
|
-
const isIgnoredGetStaticPathsWarning = line.includes("[router]") && line.includes("getStaticPaths() ignored in dynamic page");
|
|
5
|
-
return line.includes("[glob-loader]") || isIgnoredGetStaticPathsWarning || /^\s*The collection ".*" does not exist or is empty\. Please check your content config file for errors\.\s*$/.test(line);
|
|
6
|
-
};
|
|
7
|
-
};
|
|
8
|
-
var createAstroDevLineFilter = () => {
|
|
9
|
-
const shouldFilterAstroLine = createAstroLineFilter();
|
|
10
|
-
return (line) => {
|
|
11
|
-
return shouldFilterAstroLine(line) || line.includes("[router]");
|
|
12
|
-
};
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export {
|
|
16
|
-
createAstroLineFilter,
|
|
17
|
-
createAstroDevLineFilter
|
|
18
|
-
};
|