Skip to content

Commit

Permalink
update according to latest version of studio
Browse files Browse the repository at this point in the history
  • Loading branch information
helios2003 committed Nov 13, 2024
1 parent cd9081f commit e87ce69
Show file tree
Hide file tree
Showing 10 changed files with 1,154 additions and 72 deletions.
24 changes: 24 additions & 0 deletions apps/studio/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Config } from 'jest';
import nextJest from 'next/jest.js';

const createJestConfig = nextJest();
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
transform: {
'^.+\\.(ts|tsx)?$': 'ts-jest',
'^.+\\.yml$': 'jest-transform-yaml',
'^.+\\.yaml$': 'jest-transform-yaml',
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
};

const asyncConfig = createJestConfig(config);

module.exports = async () => {
const config = await asyncConfig();
config.transformIgnorePatterns = ['node_modules/.pnpm/(?!@asyncapi/react-component|monaco-editor)/'];
return config;
}
4 changes: 4 additions & 0 deletions apps/studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,12 @@
"@types/react": "18.2.18",
"@types/react-dom": "18.2.7",
"autoprefixer": "10.4.14",
"axios": "^1.7.7",
"cheerio": "^1.0.0",
"codemirror": "^6.0.1",
"crawler-user-agents": "^1.0.154",
"driver.js": "^1.3.1",
"jest": "^29.7.0",
"js-base64": "^3.7.3",
"js-file-download": "^0.4.12",
"js-yaml": "^4.1.0",
Expand Down
103 changes: 103 additions & 0 deletions apps/studio/src/app/api/v1/crawler/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { NextRequest, NextResponse } from "next/server";
import parseURL from "@/helpers/parser";
import { DocumentInfo } from "@/types";
import axios from "axios";
import { metadata } from "@/app/page";

export async function GET(request: NextRequest) {
const Base64searchParams = request.nextUrl.searchParams.get('base64');
const URLsearchParams = request.nextUrl.searchParams.get('url');

try {
if (!Base64searchParams && !URLsearchParams) return new NextResponse(null, { status: 200 });
let info: DocumentInfo | null = null;

if (Base64searchParams) {
// directly run the parsing function
info = await parseURL(Base64searchParams);
}
if (URLsearchParams) {
// fetch the document information from the URL
try {
const response = await axios.get(URLsearchParams);
if (response.status === 200) {
info = await parseURL(response.data);
} else {
return new NextResponse("Not a valid URL", { status: 500 });
}
} catch (error) {
return new NextResponse("Not a valid URL", { status: 500 });
}
}

if (!info) {
const ogImage = "https://raw.githubusercontent.com/asyncapi/studio/master/apps/studio-next/public/img/meta-studio-og-image.jpeg";

const crawlerInfo = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>"${metadata.openGraph?.title}"</title>
<meta property="og:title" content="${metadata.openGraph?.title}" />
<meta property="og:description" content="${metadata.openGraph?.description}" />
<meta property="og:url" content="${metadata.openGraph?.url}" />
<meta property="og:image" content="${ogImage}" />
`
return new NextResponse(crawlerInfo, {
headers: {
'Content-Type': 'text/html',
},
})
}

let ogImageParams = new URLSearchParams();

if (info.title) {
ogImageParams.append('title', info.title.toString());
}
if (info.description) {
ogImageParams.append('description', info.description.toString());
}
if (info.numServers) {
ogImageParams.append('numServers', info.numServers.toString());
}
if (info.numChannels) {
ogImageParams.append('numChannels', info.numChannels.toString());
}
if (info.numOperations) {
ogImageParams.append('numOperations', info.numOperations.toString());
}
if (info.numMessages) {
ogImageParams.append('numMessages', info.numMessages.toString());
}

const ogImageurl = `https://ogp-studio.vercel.app/api/og?${ogImageParams.toString()}`;

const crawlerInfo = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${info.title}</title>
${info.title ? `<meta property="og:title" content="${info.title}" />` : ''}
${info.description ? `<meta property="og:description" content="${info.description}" />` : ''}
<meta property="og:image" content=${ogImageurl} />
</head>
</html>
`;
console.log(crawlerInfo);
return new NextResponse(crawlerInfo, {
status: 200,
headers: {
'Content-Type': 'text/html',
},
});
} catch (err) {
return new NextResponse("Not a valid URL", { status: 500 });
}
}
54 changes: 54 additions & 0 deletions apps/studio/src/helpers/parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Input, Parser } from '@asyncapi/parser';
import { DocumentInfo } from '@/types';

export default async function parseURL(asyncapiDocument: string): Promise<DocumentInfo | null> {
const parser = new Parser();

const base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/;

let decodedDocument: Input = "";

if (base64Regex.test(asyncapiDocument)) {
decodedDocument = Buffer.from(asyncapiDocument, "base64").toString("utf-8");
} else {
decodedDocument = asyncapiDocument;
}

const { document, diagnostics } = await parser.parse(decodedDocument);

if (diagnostics.length) {
return null;
}

let title = document?.info().title();
if (title) {
title = title.length <= 20 ? title : title.slice(0, 20) + "...";
}
const version = document?.info().version();

let description = document?.info().description();
if (description) {
description = description.length <= 100 ? description : description.slice(0, 100) + "...";
}

const servers = document?.allServers();
const channels = document?.allChannels();
const operations = document?.allOperations();
const messages = document?.allMessages();

const numServers = servers?.length;
const numChannels = channels?.length;
const numOperations = operations?.length;
const numMessages = messages?.length;

const response = {
title,
version,
description,
numServers,
numChannels,
numOperations,
numMessages
};
return response;
}
26 changes: 26 additions & 0 deletions apps/studio/src/helpers/tests/fetchogtags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import axios from 'axios';
import * as cheerio from 'cheerio';

export async function fetchOpenGraphTags(url: string, userAgent: string) {
try {
const { data } = await axios.get(url, {
headers: {
'User-Agent': userAgent
}
});

const $ = cheerio.load(data);
const ogTags: { [key: string]: string } = {};

$('meta').each((_, element) => {
const property = $(element).attr('property');
if (property && property.startsWith('og:')) {
ogTags[property] = $(element).attr('content') || '';
}
});

return ogTags;
} catch (error) {
console.error(error);
}
}
64 changes: 64 additions & 0 deletions apps/studio/src/helpers/tests/og.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* @jest-environment node
*/

import { fetchOpenGraphTags } from './fetchogtags';

// list of sample crawlers to test with
const facebookCrawler = 'facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)';
const XCrawler = 'Twitterbot/1.0';
const SlackCrawler = 'Slackbot-LinkExpanding 1.0 (+https://api.slack.com/robots)';

// Testing with base64 query param
const base64url = "https://studio-next.netlify.app/?base64=YXN5bmNhcGk6IDMuMC4wCmluZm86CiAgdGl0bGU6IEFjY291bnQgU2VydmljZQogIHZlcnNpb246IDEuMC4wCiAgZGVzY3JpcHRpb246IFRoaXMgc2VydmljZSBpcyBpbiBjaGFyZ2Ugb2YgcHJvY2Vzc2luZyB1c2VyIHNpZ251cHMKY2hhbm5lbHM6CiAgdXNlclNpZ25lZHVwOgogICAgYWRkcmVzczogdXNlci9zaWduZWR1cAogICAgbWVzc2FnZXM6CiAgICAgIFVzZXJTaWduZWRVcDoKICAgICAgICAkcmVmOiAnIy9jb21wb25lbnRzL21lc3NhZ2VzL1VzZXJTaWduZWRVcCcKb3BlcmF0aW9uczoKICBzZW5kVXNlclNpZ25lZHVwOgogICAgYWN0aW9uOiBzZW5kCiAgICBjaGFubmVsOgogICAgICAkcmVmOiAnIy9jaGFubmVscy91c2VyU2lnbmVkdXAnCiAgICBtZXNzYWdlczoKICAgICAgLSAkcmVmOiAnIy9jaGFubmVscy91c2VyU2lnbmVkdXAvbWVzc2FnZXMvVXNlclNpZ25lZFVwJwpjb21wb25lbnRzOgogIG1lc3NhZ2VzOgogICAgVXNlclNpZ25lZFVwOgogICAgICBwYXlsb2FkOgogICAgICAgIHR5cGU6IG9iamVjdAogICAgICAgIHByb3BlcnRpZXM6CiAgICAgICAgICBkaXNwbGF5TmFtZToKICAgICAgICAgICAgdHlwZTogc3RyaW5nCiAgICAgICAgICAgIGRlc2NyaXB0aW9uOiBOYW1lIG9mIHRoZSB1c2VyCiAgICAgICAgICBlbWFpbDoKICAgICAgICAgICAgdHlwZTogc3RyaW5nCiAgICAgICAgICAgIGZvcm1hdDogZW1haWwKICAgICAgICAgICAgZGVzY3JpcHRpb246IEVtYWlsIG9mIHRoZSB1c2Vy";
const accountServiceTags = {
"og:title": 'Account Service',
"og:description": 'This service is in charge of processing user signups',
"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'
}

describe('Testing the document with base64 query parameter for various open graph crawlers', () => {
jest.setTimeout(30000);

test('Test Open Graph tags for Facebook', async () => {
const openGraphTags = await fetchOpenGraphTags(base64url, facebookCrawler);
expect(openGraphTags).equal(accountServiceTags);
});

test('Test Open Graph tags for X', async () => {
const openGraphTags = await fetchOpenGraphTags(base64url, XCrawler);
expect(openGraphTags).equal(accountServiceTags);
});

test('Test Open Graph tags for Slack', async () => {
const openGraphTags = await fetchOpenGraphTags(base64url, SlackCrawler);
expect(openGraphTags).equal(accountServiceTags);
});
})

// Testing with url query param
const externalDocUrl = 'https://studio-next.netlify.app/?url=https://raw.githubusercontent.com/asyncapi/spec/master/examples/mercure-asyncapi.yml';
const mercurHubTags = {
"og:title": 'Mercure Hub Example',
"og:description": 'This example demonstrates how to define a Mercure hub.',
"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'
}

describe('Testing the document with url query parameter for various open graph crawlers', () => {
jest.setTimeout(30000);

test('Test Open Graph tags for Facebook', async () => {
const openGraphTags = await fetchOpenGraphTags(externalDocUrl, facebookCrawler);
expect(openGraphTags).equal(mercurHubTags);
});

test('Test Open Graph tags for X', async () => {
const openGraphTags = await fetchOpenGraphTags(externalDocUrl, XCrawler);
expect(openGraphTags).equal(mercurHubTags);
});

test('Test Open Graph tags for Slack', async () => {
const openGraphTags = await fetchOpenGraphTags(externalDocUrl, SlackCrawler);
expect(openGraphTags).equal(mercurHubTags);
});
})
27 changes: 27 additions & 0 deletions apps/studio/src/middleware.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { NextRequest, NextResponse, userAgent } from "next/server";
import crawlers from 'crawler-user-agents';

export async function middleware(request: NextRequest) {
const userAgents = crawlers.map(crawler => crawler.pattern);
const requestInfo = userAgent(request);
const res = NextResponse.next();

for (const ua of userAgents) {
if (requestInfo.ua.toLowerCase().includes(ua.toLowerCase())) {

const documentURL = request.nextUrl.searchParams.get("url");
const encodedDocument = request.nextUrl.searchParams.get("base64");

if (!encodedDocument && !documentURL) {
return res;
}
if (encodedDocument) {
return NextResponse.rewrite(new URL(`/api/v1/crawler?base64=${encodeURIComponent(encodedDocument)}`, request.url));
}
if (documentURL) {
return NextResponse.rewrite(new URL(`/api/v1/crawler?url=${encodeURIComponent(documentURL)}`, request.url));
}
}
}
return res;
}
10 changes: 10 additions & 0 deletions apps/studio/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
import type specs from '@asyncapi/specs';

export type SpecVersions = keyof typeof specs.schemas;

export interface DocumentInfo {
title? : string,
version? : string,
description? : string,
numServers? : number,
numChannels? : number,
numOperations? : number,
numMessages?: number
}
5 changes: 4 additions & 1 deletion apps/studio/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,15 @@
]
}
},
"baseUrl": ".",
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"build/types/**/*.ts"
"build/types/**/*.ts",
"src/services/tests/**/*.ts",
"src/helpers/tests/**/*.ts"
],
"exclude": [
"node_modules"
Expand Down
Loading

0 comments on commit e87ce69

Please sign in to comment.