import BN from 'bn.js' import fetch from 'cross-fetch' import parseHtml, { HTMLElement } from 'node-html-parser' import { DataObjectFieldsFragment } from './api/__generated__/sdk' import { AppData, MetaTags } from './types' const DISTRIBUTOR_ASSET_PATH = 'api/v1/assets' export const joinUrlFragments = (...fragments: string[]) => { const strippedFragments = fragments.map((f) => f.replace(/^\/|\/$/, '')) return strippedFragments.join('/') } export const generateAssetUrl = (asset: DataObjectFieldsFragment) => { const workingBuckets = asset.storageBag.distributionBuckets.filter((bucket) => bucket.distributing) const distributorsEndpoints = workingBuckets.reduce((acc, bucket) => { const endpoints = bucket.operators .filter((operator) => !!operator.metadata?.nodeEndpoint) .map((operator) => operator.metadata?.nodeEndpoint) as string[] return [...acc, ...endpoints] }, [] as string[]) const assetIdBn = new BN(asset.id) const endpointsCountBn = new BN(distributorsEndpoints.length) const distributorIndex = assetIdBn.mod(endpointsCountBn).toNumber() const endpoint = distributorsEndpoints[distributorIndex] return joinUrlFragments(endpoint, DISTRIBUTOR_ASSET_PATH, asset.id) } export const getEnvVariable = (varName: string, required?: boolean) => { if (!process.env[varName] && required) { // eslint-disable-next-line no-console console.error(`Missing required ${varName} env variable`) process.exit(1) } return process.env[varName] || '' } export const generateMetaHtml = (tags: MetaTags, addHelmetAttr = false) => { return Object.entries(tags) .map( ([name, content]) => `` ) .join('\n') } export const applyMetaTagsToHtml = (html: HTMLElement, metaTags: MetaTags) => { // remove already present meta tags const metaTagsLookup = Object.keys(metaTags).reduce>((acc, key) => { acc[key.toLowerCase()] = true return acc }, {}) const metaTagsToRemove = html.querySelectorAll('meta').filter((metaTag) => { const name = metaTag.getAttribute('name') || metaTag.getAttribute('property') return name && metaTagsLookup[name.toLowerCase()] }) metaTagsToRemove.forEach((metaTag) => metaTag.remove()) // add new meta tags const head = html.querySelector('head') const metaTagsHtml = generateMetaHtml(metaTags) head?.insertAdjacentHTML('beforeend', metaTagsHtml) } export const applySchemaTagsToHtml = (html: HTMLElement, schemaTags: string) => { const head = html.querySelector('head') head?.insertAdjacentHTML('beforeend', schemaTags) } export const fetchHtmlAndAppData = async (url: string): Promise<[HTMLElement, AppData]> => { // fetch and parse html const response = await fetch(url) const html = await response.text() const parsedHtml = parseHtml(html) const getMetaTagContent = (name: string) => { const metaTag = parsedHtml.querySelector(`meta[name="${name}"]`) return metaTag?.getAttribute('content') } // extract app data from the HTML const siteName = getMetaTagContent('og:site_name') if (!siteName) { throw new Error('Missing site name') } const orionUrl = getMetaTagContent('atlas:orion_url') if (!orionUrl) { throw new Error('Missing Orion URL in fetched HTML') } const appData: AppData = { name: siteName, orionUrl: orionUrl, twitterId: getMetaTagContent('twitter:site'), yppOgTitle: getMetaTagContent('atlas:ypp_og_title'), yppOgDescription: getMetaTagContent('atlas:ypp_og_description'), yppOgImage: getMetaTagContent('atlas:ypp_og_image'), } return [parsedHtml, appData] }