Skip to content

Commit e87ce69

Browse files
committed
update according to latest version of studio
1 parent cd9081f commit e87ce69

File tree

10 files changed

+1154
-72
lines changed

10 files changed

+1154
-72
lines changed

apps/studio/jest.config.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { Config } from 'jest';
2+
import nextJest from 'next/jest.js';
3+
4+
const createJestConfig = nextJest();
5+
const config: Config = {
6+
preset: 'ts-jest',
7+
testEnvironment: 'jsdom',
8+
transform: {
9+
'^.+\\.(ts|tsx)?$': 'ts-jest',
10+
'^.+\\.yml$': 'jest-transform-yaml',
11+
'^.+\\.yaml$': 'jest-transform-yaml',
12+
},
13+
moduleNameMapper: {
14+
'^@/(.*)$': '<rootDir>/src/$1',
15+
},
16+
};
17+
18+
const asyncConfig = createJestConfig(config);
19+
20+
module.exports = async () => {
21+
const config = await asyncConfig();
22+
config.transformIgnorePatterns = ['node_modules/.pnpm/(?!@asyncapi/react-component|monaco-editor)/'];
23+
return config;
24+
}

apps/studio/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,12 @@
5656
"@types/react": "18.2.18",
5757
"@types/react-dom": "18.2.7",
5858
"autoprefixer": "10.4.14",
59+
"axios": "^1.7.7",
60+
"cheerio": "^1.0.0",
5961
"codemirror": "^6.0.1",
62+
"crawler-user-agents": "^1.0.154",
6063
"driver.js": "^1.3.1",
64+
"jest": "^29.7.0",
6165
"js-base64": "^3.7.3",
6266
"js-file-download": "^0.4.12",
6367
"js-yaml": "^4.1.0",
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import parseURL from "@/helpers/parser";
3+
import { DocumentInfo } from "@/types";
4+
import axios from "axios";
5+
import { metadata } from "@/app/page";
6+
7+
export async function GET(request: NextRequest) {
8+
const Base64searchParams = request.nextUrl.searchParams.get('base64');
9+
const URLsearchParams = request.nextUrl.searchParams.get('url');
10+
11+
try {
12+
if (!Base64searchParams && !URLsearchParams) return new NextResponse(null, { status: 200 });
13+
let info: DocumentInfo | null = null;
14+
15+
if (Base64searchParams) {
16+
// directly run the parsing function
17+
info = await parseURL(Base64searchParams);
18+
}
19+
if (URLsearchParams) {
20+
// fetch the document information from the URL
21+
try {
22+
const response = await axios.get(URLsearchParams);
23+
if (response.status === 200) {
24+
info = await parseURL(response.data);
25+
} else {
26+
return new NextResponse("Not a valid URL", { status: 500 });
27+
}
28+
} catch (error) {
29+
return new NextResponse("Not a valid URL", { status: 500 });
30+
}
31+
}
32+
33+
if (!info) {
34+
const ogImage = "https://raw.githubusercontent.com/asyncapi/studio/master/apps/studio-next/public/img/meta-studio-og-image.jpeg";
35+
36+
const crawlerInfo = `
37+
<!DOCTYPE html>
38+
<html lang="en">
39+
<head>
40+
<meta charset="UTF-8">
41+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
42+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
43+
<title>"${metadata.openGraph?.title}"</title>
44+
<meta property="og:title" content="${metadata.openGraph?.title}" />
45+
<meta property="og:description" content="${metadata.openGraph?.description}" />
46+
<meta property="og:url" content="${metadata.openGraph?.url}" />
47+
<meta property="og:image" content="${ogImage}" />
48+
`
49+
return new NextResponse(crawlerInfo, {
50+
headers: {
51+
'Content-Type': 'text/html',
52+
},
53+
})
54+
}
55+
56+
let ogImageParams = new URLSearchParams();
57+
58+
if (info.title) {
59+
ogImageParams.append('title', info.title.toString());
60+
}
61+
if (info.description) {
62+
ogImageParams.append('description', info.description.toString());
63+
}
64+
if (info.numServers) {
65+
ogImageParams.append('numServers', info.numServers.toString());
66+
}
67+
if (info.numChannels) {
68+
ogImageParams.append('numChannels', info.numChannels.toString());
69+
}
70+
if (info.numOperations) {
71+
ogImageParams.append('numOperations', info.numOperations.toString());
72+
}
73+
if (info.numMessages) {
74+
ogImageParams.append('numMessages', info.numMessages.toString());
75+
}
76+
77+
const ogImageurl = `https://ogp-studio.vercel.app/api/og?${ogImageParams.toString()}`;
78+
79+
const crawlerInfo = `
80+
<!DOCTYPE html>
81+
<html lang="en">
82+
<head>
83+
<meta charset="UTF-8">
84+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
85+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
86+
<title>${info.title}</title>
87+
${info.title ? `<meta property="og:title" content="${info.title}" />` : ''}
88+
${info.description ? `<meta property="og:description" content="${info.description}" />` : ''}
89+
<meta property="og:image" content=${ogImageurl} />
90+
</head>
91+
</html>
92+
`;
93+
console.log(crawlerInfo);
94+
return new NextResponse(crawlerInfo, {
95+
status: 200,
96+
headers: {
97+
'Content-Type': 'text/html',
98+
},
99+
});
100+
} catch (err) {
101+
return new NextResponse("Not a valid URL", { status: 500 });
102+
}
103+
}

apps/studio/src/helpers/parser.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Input, Parser } from '@asyncapi/parser';
2+
import { DocumentInfo } from '@/types';
3+
4+
export default async function parseURL(asyncapiDocument: string): Promise<DocumentInfo | null> {
5+
const parser = new Parser();
6+
7+
const base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/;
8+
9+
let decodedDocument: Input = "";
10+
11+
if (base64Regex.test(asyncapiDocument)) {
12+
decodedDocument = Buffer.from(asyncapiDocument, "base64").toString("utf-8");
13+
} else {
14+
decodedDocument = asyncapiDocument;
15+
}
16+
17+
const { document, diagnostics } = await parser.parse(decodedDocument);
18+
19+
if (diagnostics.length) {
20+
return null;
21+
}
22+
23+
let title = document?.info().title();
24+
if (title) {
25+
title = title.length <= 20 ? title : title.slice(0, 20) + "...";
26+
}
27+
const version = document?.info().version();
28+
29+
let description = document?.info().description();
30+
if (description) {
31+
description = description.length <= 100 ? description : description.slice(0, 100) + "...";
32+
}
33+
34+
const servers = document?.allServers();
35+
const channels = document?.allChannels();
36+
const operations = document?.allOperations();
37+
const messages = document?.allMessages();
38+
39+
const numServers = servers?.length;
40+
const numChannels = channels?.length;
41+
const numOperations = operations?.length;
42+
const numMessages = messages?.length;
43+
44+
const response = {
45+
title,
46+
version,
47+
description,
48+
numServers,
49+
numChannels,
50+
numOperations,
51+
numMessages
52+
};
53+
return response;
54+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import axios from 'axios';
2+
import * as cheerio from 'cheerio';
3+
4+
export async function fetchOpenGraphTags(url: string, userAgent: string) {
5+
try {
6+
const { data } = await axios.get(url, {
7+
headers: {
8+
'User-Agent': userAgent
9+
}
10+
});
11+
12+
const $ = cheerio.load(data);
13+
const ogTags: { [key: string]: string } = {};
14+
15+
$('meta').each((_, element) => {
16+
const property = $(element).attr('property');
17+
if (property && property.startsWith('og:')) {
18+
ogTags[property] = $(element).attr('content') || '';
19+
}
20+
});
21+
22+
return ogTags;
23+
} catch (error) {
24+
console.error(error);
25+
}
26+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
5+
import { fetchOpenGraphTags } from './fetchogtags';
6+
7+
// list of sample crawlers to test with
8+
const facebookCrawler = 'facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)';
9+
const XCrawler = 'Twitterbot/1.0';
10+
const SlackCrawler = 'Slackbot-LinkExpanding 1.0 (+https://api.slack.com/robots)';
11+
12+
// Testing with base64 query param
13+
const base64url = "https://studio-next.netlify.app/?base64=YXN5bmNhcGk6IDMuMC4wCmluZm86CiAgdGl0bGU6IEFjY291bnQgU2VydmljZQogIHZlcnNpb246IDEuMC4wCiAgZGVzY3JpcHRpb246IFRoaXMgc2VydmljZSBpcyBpbiBjaGFyZ2Ugb2YgcHJvY2Vzc2luZyB1c2VyIHNpZ251cHMKY2hhbm5lbHM6CiAgdXNlclNpZ25lZHVwOgogICAgYWRkcmVzczogdXNlci9zaWduZWR1cAogICAgbWVzc2FnZXM6CiAgICAgIFVzZXJTaWduZWRVcDoKICAgICAgICAkcmVmOiAnIy9jb21wb25lbnRzL21lc3NhZ2VzL1VzZXJTaWduZWRVcCcKb3BlcmF0aW9uczoKICBzZW5kVXNlclNpZ25lZHVwOgogICAgYWN0aW9uOiBzZW5kCiAgICBjaGFubmVsOgogICAgICAkcmVmOiAnIy9jaGFubmVscy91c2VyU2lnbmVkdXAnCiAgICBtZXNzYWdlczoKICAgICAgLSAkcmVmOiAnIy9jaGFubmVscy91c2VyU2lnbmVkdXAvbWVzc2FnZXMvVXNlclNpZ25lZFVwJwpjb21wb25lbnRzOgogIG1lc3NhZ2VzOgogICAgVXNlclNpZ25lZFVwOgogICAgICBwYXlsb2FkOgogICAgICAgIHR5cGU6IG9iamVjdAogICAgICAgIHByb3BlcnRpZXM6CiAgICAgICAgICBkaXNwbGF5TmFtZToKICAgICAgICAgICAgdHlwZTogc3RyaW5nCiAgICAgICAgICAgIGRlc2NyaXB0aW9uOiBOYW1lIG9mIHRoZSB1c2VyCiAgICAgICAgICBlbWFpbDoKICAgICAgICAgICAgdHlwZTogc3RyaW5nCiAgICAgICAgICAgIGZvcm1hdDogZW1haWwKICAgICAgICAgICAgZGVzY3JpcHRpb246IEVtYWlsIG9mIHRoZSB1c2Vy";
14+
const accountServiceTags = {
15+
"og:title": 'Account Service',
16+
"og:description": 'This service is in charge of processing user signups',
17+
"og:image": 'https://ogp-studio.vercel.app/api/og?title=Account+Service&description=This+service+is+in+charge+of+processing+user+signups&numChannels=1&numOperations=1&numMessages=1'
18+
}
19+
20+
describe('Testing the document with base64 query parameter for various open graph crawlers', () => {
21+
jest.setTimeout(30000);
22+
23+
test('Test Open Graph tags for Facebook', async () => {
24+
const openGraphTags = await fetchOpenGraphTags(base64url, facebookCrawler);
25+
expect(openGraphTags).equal(accountServiceTags);
26+
});
27+
28+
test('Test Open Graph tags for X', async () => {
29+
const openGraphTags = await fetchOpenGraphTags(base64url, XCrawler);
30+
expect(openGraphTags).equal(accountServiceTags);
31+
});
32+
33+
test('Test Open Graph tags for Slack', async () => {
34+
const openGraphTags = await fetchOpenGraphTags(base64url, SlackCrawler);
35+
expect(openGraphTags).equal(accountServiceTags);
36+
});
37+
})
38+
39+
// Testing with url query param
40+
const externalDocUrl = 'https://studio-next.netlify.app/?url=https://raw.githubusercontent.com/asyncapi/spec/master/examples/mercure-asyncapi.yml';
41+
const mercurHubTags = {
42+
"og:title": 'Mercure Hub Example',
43+
"og:description": 'This example demonstrates how to define a Mercure hub.',
44+
"og:image": 'https://ogp-studio.vercel.app/api/og?title=Mercure+Hub+Example&description=This+example+demonstrates+how+to+define+a+Mercure+hub.&numServers=1&numChannels=1&numOperations=2&numMessages=1'
45+
}
46+
47+
describe('Testing the document with url query parameter for various open graph crawlers', () => {
48+
jest.setTimeout(30000);
49+
50+
test('Test Open Graph tags for Facebook', async () => {
51+
const openGraphTags = await fetchOpenGraphTags(externalDocUrl, facebookCrawler);
52+
expect(openGraphTags).equal(mercurHubTags);
53+
});
54+
55+
test('Test Open Graph tags for X', async () => {
56+
const openGraphTags = await fetchOpenGraphTags(externalDocUrl, XCrawler);
57+
expect(openGraphTags).equal(mercurHubTags);
58+
});
59+
60+
test('Test Open Graph tags for Slack', async () => {
61+
const openGraphTags = await fetchOpenGraphTags(externalDocUrl, SlackCrawler);
62+
expect(openGraphTags).equal(mercurHubTags);
63+
});
64+
})

apps/studio/src/middleware.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { NextRequest, NextResponse, userAgent } from "next/server";
2+
import crawlers from 'crawler-user-agents';
3+
4+
export async function middleware(request: NextRequest) {
5+
const userAgents = crawlers.map(crawler => crawler.pattern);
6+
const requestInfo = userAgent(request);
7+
const res = NextResponse.next();
8+
9+
for (const ua of userAgents) {
10+
if (requestInfo.ua.toLowerCase().includes(ua.toLowerCase())) {
11+
12+
const documentURL = request.nextUrl.searchParams.get("url");
13+
const encodedDocument = request.nextUrl.searchParams.get("base64");
14+
15+
if (!encodedDocument && !documentURL) {
16+
return res;
17+
}
18+
if (encodedDocument) {
19+
return NextResponse.rewrite(new URL(`/api/v1/crawler?base64=${encodeURIComponent(encodedDocument)}`, request.url));
20+
}
21+
if (documentURL) {
22+
return NextResponse.rewrite(new URL(`/api/v1/crawler?url=${encodeURIComponent(documentURL)}`, request.url));
23+
}
24+
}
25+
}
26+
return res;
27+
}

apps/studio/src/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
11
import type specs from '@asyncapi/specs';
22

33
export type SpecVersions = keyof typeof specs.schemas;
4+
5+
export interface DocumentInfo {
6+
title? : string,
7+
version? : string,
8+
description? : string,
9+
numServers? : number,
10+
numChannels? : number,
11+
numOperations? : number,
12+
numMessages?: number
13+
}

apps/studio/tsconfig.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,15 @@
3030
]
3131
}
3232
},
33+
"baseUrl": ".",
3334
"include": [
3435
"next-env.d.ts",
3536
"**/*.ts",
3637
"**/*.tsx",
3738
".next/types/**/*.ts",
38-
"build/types/**/*.ts"
39+
"build/types/**/*.ts",
40+
"src/services/tests/**/*.ts",
41+
"src/helpers/tests/**/*.ts"
3942
],
4043
"exclude": [
4144
"node_modules"

0 commit comments

Comments
 (0)