From fe4439560c62235f4c2e2b72004b63ae383f7f81 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Wed, 26 Jul 2023 16:45:23 -0400 Subject: [PATCH 01/56] Add preview command support --- fileMapper.js | 3 +- lib/constants.js | 1 + lib/preview.js | 216 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 lib/preview.js diff --git a/fileMapper.js b/fileMapper.js index 2427c5c..44dfa20 100644 --- a/fileMapper.js +++ b/fileMapper.js @@ -87,7 +87,7 @@ function isAllowedExtension(filepath) { * @param {Mode} mode */ function useApiBuffer(mode) { - return mode === Mode.draft; + return mode === Mode.draft || mode === Mode.preview; } /** @@ -103,6 +103,7 @@ function getFileMapperQueryValues({ mode, options = {} }) { buffer: useApiBuffer(mode), environmentId: options.staging ? 2 : 1, version: options.assetVersion, + previewSession: options.previewSession }, }; } diff --git a/lib/constants.js b/lib/constants.js index 328d33c..31b9c68 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -34,6 +34,7 @@ const FUNCTIONS_EXTENSION = 'functions'; const Mode = { draft: 'draft', publish: 'publish', + preview: 'preview' }; const DEFAULT_MODE = Mode.publish; diff --git a/lib/preview.js b/lib/preview.js new file mode 100644 index 0000000..d0363ec --- /dev/null +++ b/lib/preview.js @@ -0,0 +1,216 @@ +const path = require('path'); +const chokidar = require('chokidar'); +const { default: PQueue } = require('p-queue'); + +const { logger } = require('../logger'); +const debounce = require('debounce'); +const { + ApiErrorContext, + logApiErrorInstance, + logApiUploadErrorInstance, + logErrorInstance, +} = require('../errorHandlers'); +const { uploadFolder } = require('./uploadFolder'); +const { shouldIgnoreFile, ignoreFile } = require('../ignoreRules'); +const { getFileMapperQueryValues } = require('../fileMapper'); +const { upload, deleteFile } = require('../api/fileMapper'); +const escapeRegExp = require('./escapeRegExp'); +const { convertToUnixPath, isAllowedExtension, getCwd } = require('../path'); +const { triggerNotify } = require('./notify'); + +const queue = new PQueue({ + concurrency: 10, +}); + +const generatePreviewUrl = (accountId, sessionToken) => { + return `https://app.hubspot.com/${accountId}/preview/${sessionToken}`; +} + +const _notifyPreviewUrl = (accountId, sessionToken) => { + if (queue.size > 0) return; + logger.log(`To preview, visit: ${generatePreviewUrl(accountId, sessionToken)}`); +}; +const notifyPreviewUrl = debounce(_notifyPreviewUrl, 1000); + +async function uploadFile(accountId, sessionToken, file, dest) { + logger.debug(`Attempting to upload file "${src}" to "${dest}"`); + const fileMapperArgs = buildFileMapperArgs(sessionToken); + + return queue.add(() => { + return upload(accountId, src, dest, fileMapperArgs) + .then(() => { + logger.log(`Uploaded file ${src} to ${dest}`); + notifyPreviewUrl(accountId, sessionToken); + }) + .catch(() => { + const uploadFailureMessage = `Uploading file ${src} to ${dest} failed`; + logger.debug(uploadFailureMessage); + logger.debug(`Retrying to upload file "${src}" to "${dest}"`); + return upload(accountId, file, dest, fileMapperArgs).catch(error => { + logger.error(uploadFailureMessage); + logApiUploadErrorInstance( + error, + new ApiErrorContext({ + accountId, + request: dest, + payload: src, + }) + ); + }); + }); + }); +} + +async function deleteRemoteFile(accountId, sessionToken, remoteFilePath) { + logger.debug(`Attempting to delete file "${remoteFilePath}"`); + const fileMapperArgs = buildFileMapperArgs(sessionToken); + + return queue.add(() => { + return deleteFile(accountId, remoteFilePath, fileMapperArgs) + .then(() => { + logger.log(`Deleted file ${remoteFilePath}`); + notifyPreviewUrl(accountId, sessionToken); + }) + .catch(error => { + logger.error(`Deleting file ${remoteFilePath} failed`); + logApiErrorInstance( + error, + new ApiErrorContext({ + accountId, + request: remoteFilePath, + }) + ); + }); + }); +} + +const getDesignManagerPath = (src, dest, file) => { + const regex = new RegExp(`^${escapeRegExp(src)}`); + const relativePath = file.replace(regex, ''); + return convertToUnixPath(path.join(dest, relativePath)); +}; + +const buildFileMapperArgs = (sessionToken) => { + return getFileMapperQueryValues({ + mode: 'preview', + previewSession: sessionToken + }); +} + +const buildDeleteFileFromPreviewBufferCallback = (sessionInfo, type) => { + const { accountId, src, dest, notify } = sessionInfo; + + return (filePath) => { + if (shouldIgnoreFile(filePath)) { + logger.debug(`Skipping ${filePath} due to an ignore rule`); + return; + } + + const remotePath = getDesignManagerPath(src, dest, filePath); + logger.debug(`Attempting to delete ${type} "${remotePath}"`, type, remotePath); + queue.add(() => { + const deletePromise = deleteRemoteFile(accountId, filePath, remotePath) + .then(() => { + logger.log(`Deleted ${type} "${remotePath}"`); + }) + .catch(error => { + logger.error(`Deleting ${type} "${remotePath}" failed`); + logApiErrorInstance( + error, + new ApiErrorContext({ + accountId, + request: remotePath, + }) + ); + }); + triggerNotify(notify, 'Removed', filePath, deletePromise); + return deletePromise; + }); + } +} + +const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { + const { accountId, src, sessionToken, notify } = sessionInfo; + + return async (filePath) => { + if (!isAllowedExtension(filePath)) { + logger.debug(`Skipping ${filePath} due to unsupported extension`); + return; + } + if (shouldIgnoreFile(filePath)) { + logger.debug(`Skipping ${filePath} due to an ignore rule`); + return; + } + const destPath = getDesignManagerPath(sessionInfo, filePath); + const fileMapperArgs = buildFileMapperArgs(sessionToken) + const uploadPromise = uploadFile(accountId, filePath, destPath, { + src, + fileMapperArgs, + commandOptions, + }); + triggerNotify(notify, notifyMessage, filePath, uploadPromise); + } +} + +const initialPreviewBufferUpload = (sessionInfo, filePaths) => { + const { accountId, sessionToken, src, dest } = sessionInfo + + const fileMapperArgs = buildFileMapperArgs(sessionToken); + // Use uploadFolder so that failures of initial upload are retried + uploadFolder(accountId, src, dest, fileMapperArgs, {}, filePaths) + .then(() => { + logger.success( + `Completed uploading files in ${src} to ${dest} in ${accountId}` + ); + }) + .catch(error => { + logger.error( + `Initial uploading of folder "${src}" to "${dest} in account ${accountId} failed` + ); + logErrorInstance(error, { + accountId, + }); + }); +} + +const preview = ( + accountId, + src, + dest, + { notify, filePaths } +) => { + const sessionToken = Date.now(); + if (notify) { + ignoreFile(notify); + } + const sessionInfo = { accountId, sessionToken, src, dest, notify }; + initialPreviewBufferUpload(sessionInfo, filePaths); + + const watcher = chokidar.watch(src, { + ignoreInitial: true, // makes initial addition of files not trigger the watcher + ignored: file => shouldIgnoreFile(file), + }); + + + const addFileCallback = buildUploadFileToPreviewBufferCallback(sessionInfo, 'Added'); + const changeFileCallback = buildUploadFileToPreviewBufferCallback(sessionInfo, 'Change'); + const deleteFileCallback = buildDeleteFileFromPreviewBufferCallback(sessionInfo, 'file'); + const deleteFolderCallback = buildDeleteFileFromPreviewBufferCallback(sessionInfo, 'folder'); + + watcher.on('ready', () => { + logger.log( + `This is the message that displays when previewing begins.` + ); + notifyPreviewUrl(accountId, sessionToken); + }); + watcher.on('add', addFileCallback); + watcher.on('change', changeFileCallback); + watcher.on('unlink', deleteFileCallback); + watcher.on('unlinkDir', deleteFolderCallback); + + return watcher; +} + +module.exports = { + preview, +}; From de74c6ae4113365f0603d2489069f72336c6ca2c Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Mon, 25 Sep 2023 13:29:14 -0400 Subject: [PATCH 02/56] Saving work --- lib/preview.js | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/lib/preview.js b/lib/preview.js index d0363ec..62882dc 100644 --- a/lib/preview.js +++ b/lib/preview.js @@ -22,9 +22,6 @@ const queue = new PQueue({ concurrency: 10, }); -const generatePreviewUrl = (accountId, sessionToken) => { - return `https://app.hubspot.com/${accountId}/preview/${sessionToken}`; -} const _notifyPreviewUrl = (accountId, sessionToken) => { if (queue.size > 0) return; @@ -91,7 +88,7 @@ const getDesignManagerPath = (src, dest, file) => { }; const buildFileMapperArgs = (sessionToken) => { - return getFileMapperQueryValues({ + return getFileMapperQueryValues({ mode: 'preview', previewSession: sessionToken }); @@ -152,12 +149,12 @@ const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { } } -const initialPreviewBufferUpload = (sessionInfo, filePaths) => { +const initialPreviewBufferUpload = async (sessionInfo, filePaths) => { const { accountId, sessionToken, src, dest } = sessionInfo const fileMapperArgs = buildFileMapperArgs(sessionToken); // Use uploadFolder so that failures of initial upload are retried - uploadFolder(accountId, src, dest, fileMapperArgs, {}, filePaths) + return uploadFolder(accountId, src, dest, fileMapperArgs, {}, filePaths) .then(() => { logger.success( `Completed uploading files in ${src} to ${dest} in ${accountId}` @@ -173,19 +170,7 @@ const initialPreviewBufferUpload = (sessionInfo, filePaths) => { }); } -const preview = ( - accountId, - src, - dest, - { notify, filePaths } -) => { - const sessionToken = Date.now(); - if (notify) { - ignoreFile(notify); - } - const sessionInfo = { accountId, sessionToken, src, dest, notify }; - initialPreviewBufferUpload(sessionInfo, filePaths); - +const startPreviewWatchingService = () => { const watcher = chokidar.watch(src, { ignoreInitial: true, // makes initial addition of files not trigger the watcher ignored: file => shouldIgnoreFile(file), @@ -201,7 +186,6 @@ const preview = ( logger.log( `This is the message that displays when previewing begins.` ); - notifyPreviewUrl(accountId, sessionToken); }); watcher.on('add', addFileCallback); watcher.on('change', changeFileCallback); @@ -211,6 +195,23 @@ const preview = ( return watcher; } +const preview = async ( + accountId, + src, + dest, + { notify, filePaths } +) => { + const sessionToken = Date.now(); + if (notify) { + ignoreFile(notify); + } + const sessionInfo = { accountId, sessionToken, src, dest, notify }; + await initialPreviewBufferUpload(sessionInfo, filePaths); + startPreviewWatchingService(); + startLocalHttpService(); + +} + module.exports = { preview, }; From d7136a429c9db9c7f8d3073c25bd04406d0f682b Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Mon, 2 Oct 2023 14:10:45 -0400 Subject: [PATCH 03/56] Move work in from cloudlfare worker, start implementation of proxy, fetching web domains, modularize routes a bit --- api/domains.js | 20 +++++++++ api/preview.js | 20 +++++++++ lib/preview.js | 81 ++++++++++++++++++++----------------- lib/preview/createRoutes.js | 15 +++++++ lib/preview/previewUtils.js | 15 +++++++ lib/preview/routes/index.js | 61 ++++++++++++++++++++++++++++ lib/preview/routes/proxy.js | 33 +++++++++++++++ 7 files changed, 209 insertions(+), 36 deletions(-) create mode 100644 api/domains.js create mode 100644 api/preview.js create mode 100644 lib/preview/createRoutes.js create mode 100644 lib/preview/previewUtils.js create mode 100644 lib/preview/routes/index.js create mode 100644 lib/preview/routes/proxy.js diff --git a/api/domains.js b/api/domains.js new file mode 100644 index 0000000..47571bd --- /dev/null +++ b/api/domains.js @@ -0,0 +1,20 @@ +const http = require('../http') + +const DOMAINS_API_PATH = `/cms/v3/domains`; + +async function fetchDomains(accountId) { + try { + const result = await http.get(accountId, { + uri: DOMAINS_API_PATH, + json: true + }); + + return result.results + } catch (err) { + throw err; + } +} + +module.exports = { + fetchDomains, +} diff --git a/api/preview.js b/api/preview.js new file mode 100644 index 0000000..47571bd --- /dev/null +++ b/api/preview.js @@ -0,0 +1,20 @@ +const http = require('../http') + +const DOMAINS_API_PATH = `/cms/v3/domains`; + +async function fetchDomains(accountId) { + try { + const result = await http.get(accountId, { + uri: DOMAINS_API_PATH, + json: true + }); + + return result.results + } catch (err) { + throw err; + } +} + +module.exports = { + fetchDomains, +} diff --git a/lib/preview.js b/lib/preview.js index 62882dc..7cfc710 100644 --- a/lib/preview.js +++ b/lib/preview.js @@ -1,5 +1,7 @@ +const http = require('http'); const path = require('path'); const chokidar = require('chokidar'); +const express = require('express'); const { default: PQueue } = require('p-queue'); const { logger } = require('../logger'); @@ -17,17 +19,9 @@ const { upload, deleteFile } = require('../api/fileMapper'); const escapeRegExp = require('./escapeRegExp'); const { convertToUnixPath, isAllowedExtension, getCwd } = require('../path'); const { triggerNotify } = require('./notify'); - -const queue = new PQueue({ - concurrency: 10, -}); - - -const _notifyPreviewUrl = (accountId, sessionToken) => { - if (queue.size > 0) return; - logger.log(`To preview, visit: ${generatePreviewUrl(accountId, sessionToken)}`); -}; -const notifyPreviewUrl = debounce(_notifyPreviewUrl, 1000); +const { getAccountConfig } = require('./config'); +const { createPreviewServerRoutes } = require('./preview/createRoutes'); +const { getPortalDomains } = require('./preview/previewUtils'); async function uploadFile(accountId, sessionToken, file, dest) { logger.debug(`Attempting to upload file "${src}" to "${dest}"`); @@ -89,8 +83,8 @@ const getDesignManagerPath = (src, dest, file) => { const buildFileMapperArgs = (sessionToken) => { return getFileMapperQueryValues({ - mode: 'preview', - previewSession: sessionToken + mode: 'publish', + //previewSession: sessionToken }); } @@ -150,27 +144,17 @@ const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { } const initialPreviewBufferUpload = async (sessionInfo, filePaths) => { - const { accountId, sessionToken, src, dest } = sessionInfo - + const { portalId, sessionToken, src, dest } = sessionInfo const fileMapperArgs = buildFileMapperArgs(sessionToken); // Use uploadFolder so that failures of initial upload are retried - return uploadFolder(accountId, src, dest, fileMapperArgs, {}, filePaths) - .then(() => { - logger.success( - `Completed uploading files in ${src} to ${dest} in ${accountId}` - ); - }) - .catch(error => { - logger.error( - `Initial uploading of folder "${src}" to "${dest} in account ${accountId} failed` - ); - logErrorInstance(error, { - accountId, - }); - }); + console.log(`Uploading ${portalId} ${src} ${dest} ${fileMapperArgs} ${filePaths}`) + const result = await uploadFolder(portalId, src, dest, fileMapperArgs, {}, filePaths); + console.log(result); } -const startPreviewWatchingService = () => { +const startPreviewWatchingService = (sessionInfo) => { + const { src } = sessionInfo; + const watcher = chokidar.watch(src, { ignoreInitial: true, // makes initial addition of files not trigger the watcher ignored: file => shouldIgnoreFile(file), @@ -195,20 +179,45 @@ const startPreviewWatchingService = () => { return watcher; } +const startLocalHttpService = async (sessionInfo) => { + const expressServer = express(); + //expressServer.use(bodyParser.json()); + expressServer.use('/', await createPreviewServerRoutes(sessionInfo)); + + const httpServer = http.createServer(expressServer); + httpServer.listen(3000); + console.log('Now listening on port 3000'); + return httpServer; +} + const preview = async ( accountId, src, dest, - { notify, filePaths } + { notify, filePaths, skipInitial } ) => { - const sessionToken = Date.now(); + const accountConfig = getAccountConfig(accountId); + const domains = await getPortalDomains(accountId); + const sessionInfo = { + src, + dest, + portalName: accountConfig.name, + portalId: accountId, + env: accountConfig.env, + personalAccessKey: accountConfig.personalAccessKey, + sessionToken: Date.now(), + domains + } + if (notify) { ignoreFile(notify); } - const sessionInfo = { accountId, sessionToken, src, dest, notify }; - await initialPreviewBufferUpload(sessionInfo, filePaths); - startPreviewWatchingService(); - startLocalHttpService(); + + if (!skipInitial) { + await initialPreviewBufferUpload(sessionInfo, filePaths); + } + startPreviewWatchingService(sessionInfo); + startLocalHttpService(sessionInfo); } diff --git a/lib/preview/createRoutes.js b/lib/preview/createRoutes.js new file mode 100644 index 0000000..d648fc5 --- /dev/null +++ b/lib/preview/createRoutes.js @@ -0,0 +1,15 @@ +const { Router } = require('express'); + +const { buildIndexRouteHandler } = require('./routes/index.js'); +const { buildProxyRouteHandler } = require('./routes/proxy.js'); + +const createPreviewServerRoutes = async (sessionInfo) => { + const previewServerRouter = Router(); + previewServerRouter.get('/', buildIndexRouteHandler(sessionInfo)) + previewServerRouter.get('/proxy', buildProxyRouteHandler(sessionInfo)) + return previewServerRouter; +} + +module.exports = { + createPreviewServerRoutes, +} diff --git a/lib/preview/previewUtils.js b/lib/preview/previewUtils.js new file mode 100644 index 0000000..aa5da7d --- /dev/null +++ b/lib/preview/previewUtils.js @@ -0,0 +1,15 @@ +const { fetchDomains } = require('../../api/domains'); + +const getPortalDomains = async (portalId) => { + try { + const result = await fetchDomains(portalId); + return result; + } catch (error) { + console.log("There was a problem fetching domains for your portal. You may be missing a scope necessary for this feature.") + return []; + } +} + +module.exports = { + getPortalDomains, +} diff --git a/lib/preview/routes/index.js b/lib/preview/routes/index.js new file mode 100644 index 0000000..c3154ca --- /dev/null +++ b/lib/preview/routes/index.js @@ -0,0 +1,61 @@ +const buildIndexRouteHandler = (sessionInfo) => { + return async (req, res) => { + const responseHTML = buildIndexHtml(sessionInfo); + res.status(200).set({ 'Content-Type': 'text/html' }).end(responseHTML); + } +} + +const buildIndexHtml = (sessionInfo) => { + const { domains } = sessionInfo; + return ` + + + + +

Hello World!

+

This markup was generated by the Cloudflare Worker.

+

This will allow us to inject scripts and UI into the resultant preview render after its fetched from the API if we want!

+

Here's an example to show the JS embed:

0

+ ${getSiteList(domains)} + + + `; +} + +const getSiteList = (domains) => { + return '
    ' + + domains.reduce((x,y) => x += `
  • ${y.domain}
  • `, '') + + '
'; +} + +const getEmbeddedJS = () => { + return ` + function nzeros(n){ + var building ='0'; + var i = 0; + while (i < n-1){ + building+='0'; + i+=1 + } + return building; + } + var z = document.querySelector('#test'); + var goingRight = true; + function makeStep() { + if (goingRight) { + z.innerHTML+='0' + if (z.innerHTML.length === 10) goingRight=false; + } else { + z.innerHTML=nzeros(z.innerHTML.length-1) + if (z.innerHTML.length === 1) goingRight=true; + } + } + var y = setInterval(makeStep, 100) + ` +} + +module.exports = { + buildIndexRouteHandler +} diff --git a/lib/preview/routes/proxy.js b/lib/preview/routes/proxy.js new file mode 100644 index 0000000..aec12e8 --- /dev/null +++ b/lib/preview/routes/proxy.js @@ -0,0 +1,33 @@ +const { get } = require('../../../http'); + +const buildProxyRouteHandler = (sessionInfo) => { + const { portalId } = sessionInfo; + + return async (req, res) => { + const { page } = req.query; + const result = await fetch(page); + const responseHTML = buildProxyHtml(sessionInfo, page) + res.status(200).set({ 'Content-Type': 'text/html' }).end(responseHTML); + } +} + +const buildProxyHtml = (sessionInfo, page) => { + // If we want to embed a script or something into the proxy + // we can wrap it here or parse the returned HTML into the render + // but that's maybe trickier... + return ` + + + + +

${page}

+ + `; +} + + + + +module.exports = { + buildProxyRouteHandler +} From f3fb919757abf607062b15bfe5691386c4c1e00c Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Tue, 3 Oct 2023 13:20:35 -0400 Subject: [PATCH 04/56] Proxying works mostly --- api/preview.js | 163 ++++++++++++++++++++++++++++++++++-- lib/preview/proxyPage.js | 93 ++++++++++++++++++++ lib/preview/routes/index.js | 34 +------- lib/preview/routes/proxy.js | 62 ++++++++++++-- 4 files changed, 306 insertions(+), 46 deletions(-) create mode 100644 lib/preview/proxyPage.js diff --git a/api/preview.js b/api/preview.js index 47571bd..7a4394d 100644 --- a/api/preview.js +++ b/api/preview.js @@ -1,20 +1,169 @@ -const http = require('../http') +const { get } = require('../http') +const http = require('http'); +const https = require('https'); +const { getAccountId } = require('../lib/config'); -const DOMAINS_API_PATH = `/cms/v3/domains`; +const CONTENT_API_PATH = `/content/api/v4/contents`; -async function fetchDomains(accountId) { +async function fetchPreviewInfo(accountId, contentId) { try { - const result = await http.get(accountId, { - uri: DOMAINS_API_PATH, + const content = await get(accountId, { + uri: `${CONTENT_API_PATH}/${contentId}`, + query: { portalId: accountId }, json: true }); - return result.results + return { + previewDomain: content.previewDomain ?? content.resolvedDomain, + previewKey: content.previewKey, + }; } catch (err) { throw err; } } +const requestContentPreview = async (url, portalId) => { + const urlObject = new URL(url); + + const proxiedQueryParams = {}; + + // Convert searchParams to object param format hsGet/request expects + for (const queryKey of urlObject.searchParams.keys()) { + const values = urlObject.searchParams.getAll(queryKey); + + if (values.length > 1) { + proxiedQueryParams[queryKey] = values; + } else { + proxiedQueryParams[queryKey] = values[0]; + } + } + + const accountId = getAccountId(portalId); + + const response = await get(accountId, { + baseUrl: urlObject.origin, + uri: urlObject.pathname, + query: proxiedQueryParams, + json: false, + resolveWithFullResponse: true, + }); + + return response; +} + +const requestPage = (url, redirects = 0, originalUrl=undefined) => { + if (redirects > 5) { + throw new Error( + `Hit too many redirects to HEAD requests for ${originalUrl}` + ); + } + + return new Promise((resolve, reject) => { + const protocolAPI = url.startsWith('https://') ? https : http; + protocolAPI + .request( + url, + { + method: 'HEAD', + headers: { + // Keep this user agent, so we don't get 403s from the request + ['User-Agent']: 'cms-dev-server', + }, + }, + async res => { + // Use a lib to better follow various redirects? + if ( + res.statusCode >= 300 && + res.statusCode < 400 && + res.headers.location + ) { + console.log(`Redirecting to ${res.headers.location} (from ${url})`); + return resolve( + requestPage( + res.headers.location, + redirects + 1, + originalUrl ?? url + ) + ); + } + + return resolve(res); + } + ) + .on('error', err => { + console.error(err); + reject(err); + }) + .end(); + }); +} + +const fetchContentMetadata = async ( + url, + portalId +) => { + let res; + if (url.includes('hs_preview')) { + console.log('hellox') + res = await requestContentPreview(url, portalId); + } else { + console.log('helloy') + res = await requestPage(url); + console.log(res.headers) + console.log('past page req') + } + + const portalIdHeader = res.headers['x-hs-hub-id']; + const contentId = res.headers['x-hs-content-id']; + console.log(`${portalIdHeader} ${contentId}`) + if (Array.isArray(portalIdHeader) || Array.isArray(contentId)) { + throw new Error( + `There were multiple hub ID or content ID headers in the HEAD request to ${url}` + ); + } else { + const portalIdFromResponse = parseInt(portalIdHeader, 10); + console.log(`${portalIdFromResponse}`) + const pageAccount = await getAccountId(portalIdFromResponse); + console.log(`${portalIdFromResponse} ${pageAccount}`) + if (!pageAccount) { + throw new Error( + `No CLI auth for portal ${portalIdFromResponse} found, please run \`hs auth\`` + ); + } + + if (res.statusCode !== 200) { + throw new Error( + `${res.statusCode} ${ + res.statusMessage + } - Unable to obtain HEAD information from ${url}. ${ + res.statusCode === 429 && res.headers['retry-after'] + ? `Retry after ${res.headers['retry-after']} seconds.` + : '' + }` + ); + } else if (!portalId || !contentId) { + throw new Error( + `Missing hub ID or content ID headers on HEAD request to ${url}` + ); + } else if (isNaN(portalId)) { + throw new Error( + `Hub ID from the HEAD request is not a number: '${portalId}' (${url})` + ); + } + + // TODO, the hubs-hublets/hublets/find/ API is internal auth only.🤔... + // Prior implementation of fetchHubletForPortalId that would work execept for an internal auth issue: + // https://git.hubteam.com/HubSpot/cms-js-platform/blob/ef6e8029df6c09fe30d65a80073606f41ca324a6/cms-dev-server/src/proxyPage/fetchHubletForPortalId.ts + // + // const hublet = await fetchHubletForPortalId(portalId); + + const hublet = 'na1'; + + return { portalId, contentId, hublet }; + } +} + module.exports = { - fetchDomains, + fetchPreviewInfo, + fetchContentMetadata } diff --git a/lib/preview/proxyPage.js b/lib/preview/proxyPage.js new file mode 100644 index 0000000..62f2da8 --- /dev/null +++ b/lib/preview/proxyPage.js @@ -0,0 +1,93 @@ +const http = require('../../http'); +const { fetchPreviewInfo } = require('../../api/preview'); + +const proxyPage = async ( + res, + portalId, + hublet, + contentId, + urlToProxy, +) => { + try { + console.log('test1') + const previewInfo = await fetchPreviewInfo(portalId, contentId); + console.log('test2') + + const { previewKey } = previewInfo; + console.log(previewKey) + console.log(urlToProxy) + const pageHtml = await makeProxyPreviewRequest( + portalId, + urlToProxy, + previewKey, + contentId + ) + console.log('test3') + + res.status(200).set({ 'Content-Type': 'text/html' }).end(pageHtml); + } catch (error) { + // TODO change error.stack to error.message before we publish + response + .status(500) + .end( + `Failed proxy render of page id = ${contentId} hub id = ${portalId}\n\n${error.stack}` + ); + return; + } +} + +const makeProxyPreviewRequest = async ( + portalId, + urlToProxy, + previewKey, + contentId +) => { + const url = new URL(urlToProxy); + console.log(`Proxying request to ${url.href}`, { + previewKey, + contentId + }); + + let proxiedQueryParams = {}; + + // Convert searchParams to object param format hsGet/request expects + for (const queryKey of url.searchParams.keys()) { + const values = url.searchParams.getAll(queryKey); + + if (values.length > 1) { + proxiedQueryParams[queryKey] = values; + } else { + proxiedQueryParams[queryKey] = values[0]; + } + } + + proxiedQueryParams = { + ...proxiedQueryParams, + hs_preview: `${previewKey}-${contentId}`, + }; + + const result = http.get(portalId, { + baseUrl: url.origin, + uri: url.pathname, + query: proxiedQueryParams, + json: false, + useQuerystring: true, + }); + + return result.catch(error => { + if ( + error.statusCode >= 400 && + error.statusCode < 500 && + !!error.response.body + ) { + return error.response.body; + } else { + throw error; + } + }); + +} + +module.exports = { + proxyPage +} diff --git a/lib/preview/routes/index.js b/lib/preview/routes/index.js index c3154ca..f8e5bcd 100644 --- a/lib/preview/routes/index.js +++ b/lib/preview/routes/index.js @@ -14,48 +14,18 @@ const buildIndexHtml = (sessionInfo) => {

Hello World!

This markup was generated by the Cloudflare Worker.

-

This will allow us to inject scripts and UI into the resultant preview render after its fetched from the API if we want!

-

Here's an example to show the JS embed:

0

+

Domains

${getSiteList(domains)} - `; } const getSiteList = (domains) => { return ''; } -const getEmbeddedJS = () => { - return ` - function nzeros(n){ - var building ='0'; - var i = 0; - while (i < n-1){ - building+='0'; - i+=1 - } - return building; - } - var z = document.querySelector('#test'); - var goingRight = true; - function makeStep() { - if (goingRight) { - z.innerHTML+='0' - if (z.innerHTML.length === 10) goingRight=false; - } else { - z.innerHTML=nzeros(z.innerHTML.length-1) - if (z.innerHTML.length === 1) goingRight=true; - } - } - var y = setInterval(makeStep, 100) - ` -} - module.exports = { buildIndexRouteHandler } diff --git a/lib/preview/routes/proxy.js b/lib/preview/routes/proxy.js index aec12e8..8fca3dc 100644 --- a/lib/preview/routes/proxy.js +++ b/lib/preview/routes/proxy.js @@ -1,17 +1,51 @@ const { get } = require('../../../http'); +const { fetchContentMetadata } = require('../../../api/preview'); +const { proxyPage } = require('../proxyPage'); const buildProxyRouteHandler = (sessionInfo) => { - const { portalId } = sessionInfo; return async (req, res) => { - const { page } = req.query; - const result = await fetch(page); - const responseHTML = buildProxyHtml(sessionInfo, page) - res.status(200).set({ 'Content-Type': 'text/html' }).end(responseHTML); + if (!req.query.page) { + res.status(200).set({ 'Content-Type': 'text/html' }).end(buildProxyIndex()); + return; + } + // parse proxy page URL from query param + let proxyPageUrl; + try { + proxyPageUrl = new URL(req.query.page); + } catch (e) { + const message = + 'Please provide a valid page query parameter, e.g., http://localhost:3000/proxy?page=https://yourdomain.com/path'; + console.warn(message); + return res.status(400).send(message); + } + // validate proxy page URL query params + if (proxyPageUrl.searchParams.has('hs_preview')) { + const message = `Can't make a proxy request, you cannot proxy URLs that include internal query params like hs_preview`; + console.warn(message); + return res.status(400).send(message); + } + + try { + console.log('hello') + const { portalId, contentId, hublet } = await fetchContentMetadata( + proxyPageUrl.href, + sessionInfo.portalId + ) + console.log(`world ${portalId} ${contentId} ${hublet}`) + await proxyPage( + res, + portalId, + hublet, + contentId, + proxyPageUrl.href + ); + } catch (e) { + } } } -const buildProxyHtml = (sessionInfo, page) => { +const buildProxyIndex = () => { // If we want to embed a script or something into the proxy // we can wrap it here or parse the returned HTML into the render // but that's maybe trickier... @@ -20,7 +54,21 @@ const buildProxyHtml = (sessionInfo, page) => { -

${page}

+
+

Local Proxy

+
+ + + +
+
`; } From 4fdace42e1dfb364c5dcab7293413d73812d65e2 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Tue, 3 Oct 2023 13:21:31 -0400 Subject: [PATCH 05/56] Remove logs --- api/preview.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/api/preview.js b/api/preview.js index 7a4394d..77957e1 100644 --- a/api/preview.js +++ b/api/preview.js @@ -104,27 +104,22 @@ const fetchContentMetadata = async ( ) => { let res; if (url.includes('hs_preview')) { - console.log('hellox') res = await requestContentPreview(url, portalId); } else { - console.log('helloy') res = await requestPage(url); - console.log(res.headers) - console.log('past page req') } const portalIdHeader = res.headers['x-hs-hub-id']; const contentId = res.headers['x-hs-content-id']; - console.log(`${portalIdHeader} ${contentId}`) + if (Array.isArray(portalIdHeader) || Array.isArray(contentId)) { throw new Error( `There were multiple hub ID or content ID headers in the HEAD request to ${url}` ); } else { const portalIdFromResponse = parseInt(portalIdHeader, 10); - console.log(`${portalIdFromResponse}`) const pageAccount = await getAccountId(portalIdFromResponse); - console.log(`${portalIdFromResponse} ${pageAccount}`) + if (!pageAccount) { throw new Error( `No CLI auth for portal ${portalIdFromResponse} found, please run \`hs auth\`` From 0aae19b02ac8dc5b3562f76e3590c153970f11a1 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Thu, 5 Oct 2023 10:26:22 -0400 Subject: [PATCH 06/56] module and template route handlers --- lib/preview/createRoutes.js | 8 +++++-- lib/preview/previewUtils.js | 13 +++++++++++ lib/preview/proxyPage.js | 6 +---- lib/preview/routes/index.js | 42 ++++++++++++++++++++++++++++++---- lib/preview/routes/module.js | 13 +++++++++++ lib/preview/routes/template.js | 13 +++++++++++ 6 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 lib/preview/routes/module.js create mode 100644 lib/preview/routes/template.js diff --git a/lib/preview/createRoutes.js b/lib/preview/createRoutes.js index d648fc5..bb6a83a 100644 --- a/lib/preview/createRoutes.js +++ b/lib/preview/createRoutes.js @@ -2,11 +2,15 @@ const { Router } = require('express'); const { buildIndexRouteHandler } = require('./routes/index.js'); const { buildProxyRouteHandler } = require('./routes/proxy.js'); +const { buildModuleRouteHandler } = require('./routes/module.js'); +const { buildTemplateRouteHandler } = require('./routes/template.js'); const createPreviewServerRoutes = async (sessionInfo) => { const previewServerRouter = Router(); - previewServerRouter.get('/', buildIndexRouteHandler(sessionInfo)) - previewServerRouter.get('/proxy', buildProxyRouteHandler(sessionInfo)) + previewServerRouter.get('/', buildIndexRouteHandler(sessionInfo)); + previewServerRouter.get('/proxy', buildProxyRouteHandler(sessionInfo)); + previewServerRouter.get('/module/:name', buildModuleRouteHandler(sessionInfo)); + previewServerRouter.get('/template/:name', buildTemplateRouteHandler(sessionInfo)); return previewServerRouter; } diff --git a/lib/preview/previewUtils.js b/lib/preview/previewUtils.js index aa5da7d..62b57a1 100644 --- a/lib/preview/previewUtils.js +++ b/lib/preview/previewUtils.js @@ -1,4 +1,5 @@ const { fetchDomains } = require('../../api/domains'); +const { getDirectoryContentsByPath } = require('../../api/fileMapper'); const getPortalDomains = async (portalId) => { try { @@ -10,6 +11,18 @@ const getPortalDomains = async (portalId) => { } } +const getModules = async (sessionInfo) => { + const modulesPath = `${sessionInfo.dest}/modules`; + return getDirectoryContentsByPath(sessionInfo.portalId, modulesPath); +} + +const getTemplates = async (sessionInfo) => { + const templatesPath = `${sessionInfo.dest}/templates`; + return getDirectoryContentsByPath(sessionInfo.portalId, templatesPath); +} + module.exports = { getPortalDomains, + getModules, + getTemplates, } diff --git a/lib/preview/proxyPage.js b/lib/preview/proxyPage.js index 62f2da8..e38d3b7 100644 --- a/lib/preview/proxyPage.js +++ b/lib/preview/proxyPage.js @@ -9,20 +9,16 @@ const proxyPage = async ( urlToProxy, ) => { try { - console.log('test1') const previewInfo = await fetchPreviewInfo(portalId, contentId); - console.log('test2') const { previewKey } = previewInfo; - console.log(previewKey) - console.log(urlToProxy) + const pageHtml = await makeProxyPreviewRequest( portalId, urlToProxy, previewKey, contentId ) - console.log('test3') res.status(200).set({ 'Content-Type': 'text/html' }).end(pageHtml); } catch (error) { diff --git a/lib/preview/routes/index.js b/lib/preview/routes/index.js index f8e5bcd..bf38f73 100644 --- a/lib/preview/routes/index.js +++ b/lib/preview/routes/index.js @@ -1,12 +1,17 @@ +const { getModules, getTemplates } = require('../previewUtils'); + const buildIndexRouteHandler = (sessionInfo) => { return async (req, res) => { - const responseHTML = buildIndexHtml(sessionInfo); + const responseHTML = await buildIndexHtml(sessionInfo); res.status(200).set({ 'Content-Type': 'text/html' }).end(responseHTML); } } -const buildIndexHtml = (sessionInfo) => { +const buildIndexHtml = async (sessionInfo) => { const { domains } = sessionInfo; + const modules = (await getModules(sessionInfo)).children; + const templates = (await getTemplates(sessionInfo)).children; + return ` @@ -14,15 +19,44 @@ const buildIndexHtml = (sessionInfo) => {

Hello World!

This markup was generated by the Cloudflare Worker.

-

Domains

+

Domains

${getSiteList(domains)} +

Modules

+ ${getModuleList(modules)} +

Templates

+ ${getTemplateList(templates)} `; } + const getSiteList = (domains) => { + return listify( + domains, + domainObj => `proxy?page=http://${domainObj.domain}`, + domainObj => domainObj.domain + ); +} + +const getModuleList = (modules) => { + return listify( + modules.map(module => module.split('.')[0]), + moduleName => `module/${moduleName}`, + moduleName => moduleName + ); +} + +const getTemplateList = (templates) => { + return listify( + templates, + templateName => `template/${templateName}`, + templateName => templateName + ) +} + +const listify = (objects, hrefPathBuilder, labelBuilder) => { return ''; } diff --git a/lib/preview/routes/module.js b/lib/preview/routes/module.js new file mode 100644 index 0000000..a6b73a7 --- /dev/null +++ b/lib/preview/routes/module.js @@ -0,0 +1,13 @@ + + +const buildModuleRouteHandler = (sessionInfo) => { + return async (req, res) => { + const { name } = req.params; + + } +} + + +module.exports = { + buildModuleRouteHandler +} diff --git a/lib/preview/routes/template.js b/lib/preview/routes/template.js new file mode 100644 index 0000000..1d6f34a --- /dev/null +++ b/lib/preview/routes/template.js @@ -0,0 +1,13 @@ + + +const buildTemplateRouteHandler = (sessionInfo) => { + return async (req, res) => { + const { name } = req.params; + + } +} + + +module.exports = { + buildTemplateRouteHandler +} From dd8970e7410f95263f5b29278245319f36f30d3f Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Wed, 11 Oct 2023 16:30:07 -0400 Subject: [PATCH 07/56] Flesh out module and template previews, add a few error responses --- api/designManager.js | 14 +++++++++ lib/preview.js | 3 +- lib/preview/previewUtils.js | 23 +++++++++++++-- lib/preview/routes/index.js | 17 ++++++----- lib/preview/routes/module.js | 54 +++++++++++++++++++++++++++++++++- lib/preview/routes/proxy.js | 6 ++-- lib/preview/routes/template.js | 54 ++++++++++++++++++++++++++++++++++ 7 files changed, 157 insertions(+), 14 deletions(-) diff --git a/api/designManager.js b/api/designManager.js index dceadc4..75a7432 100644 --- a/api/designManager.js +++ b/api/designManager.js @@ -43,9 +43,23 @@ async function fetchRawAssetByPath(accountId, path) { }); } +async function fetchModulesByPath(accountId, path) { + return http.get(accountId, { + uri: `${DESIGN_MANAGER_API_PATH}/modules/by-path/${path}?portalId=${accountId}` + }) +} + +async function fetchTemplatesByPath(accountId, path) { + return http.get(accountId, { + uri: `${DESIGN_MANAGER_API_PATH}/templates/by-path/${path}?portalId=${accountId}` + }) +} + module.exports = { fetchBuiltinMapping, fetchMenus, fetchRawAssetByPath, fetchThemes, + fetchModulesByPath, + fetchTemplatesByPath }; diff --git a/lib/preview.js b/lib/preview.js index 7cfc710..5e011bb 100644 --- a/lib/preview.js +++ b/lib/preview.js @@ -205,8 +205,9 @@ const preview = async ( portalId: accountId, env: accountConfig.env, personalAccessKey: accountConfig.personalAccessKey, + hublet: 'na1', // we find this when getting content metadata during page preview, can we get that ahead of time? sessionToken: Date.now(), - domains + domains, } if (notify) { diff --git a/lib/preview/previewUtils.js b/lib/preview/previewUtils.js index 62b57a1..d5c6a65 100644 --- a/lib/preview/previewUtils.js +++ b/lib/preview/previewUtils.js @@ -13,16 +13,35 @@ const getPortalDomains = async (portalId) => { const getModules = async (sessionInfo) => { const modulesPath = `${sessionInfo.dest}/modules`; - return getDirectoryContentsByPath(sessionInfo.portalId, modulesPath); + return getDirectoryContentsByPath(sessionInfo.portalId, modulesPath) + .then(response => response.children) + .catch(err => null); } const getTemplates = async (sessionInfo) => { const templatesPath = `${sessionInfo.dest}/templates`; - return getDirectoryContentsByPath(sessionInfo.portalId, templatesPath); + return getDirectoryContentsByPath(sessionInfo.portalId, templatesPath) + .then(response => response.children) + .catch(err => null); } +const getPreviewUrl = (sessionInfo, queryParams) => { + const { portalId, env, hublet } = sessionInfo; + + return `http://${portalId}.hubspotpreview${ + env === 'qa' ? 'qa' : '' + }-${hublet}.com/_hcms/preview/template/multi?${stringifyQuery(queryParams)}`; +} + +const stringifyQuery = (query) => { + return Object.keys(query) + .sort() + .map(key => `${key}=${query[key]}`) + .join('&'); +} module.exports = { getPortalDomains, getModules, getTemplates, + getPreviewUrl } diff --git a/lib/preview/routes/index.js b/lib/preview/routes/index.js index bf38f73..bc15bf5 100644 --- a/lib/preview/routes/index.js +++ b/lib/preview/routes/index.js @@ -8,27 +8,28 @@ const buildIndexRouteHandler = (sessionInfo) => { } const buildIndexHtml = async (sessionInfo) => { - const { domains } = sessionInfo; - const modules = (await getModules(sessionInfo)).children; - const templates = (await getTemplates(sessionInfo)).children; + const { domains, dest } = sessionInfo; + const modules = await getModules(sessionInfo); + const templates = await getTemplates(sessionInfo); return ` -

Hello World!

-

This markup was generated by the Cloudflare Worker.

Domains

- ${getSiteList(domains)} + ${ domains ? getSiteList(domains) : p('No domains found') }

Modules

- ${getModuleList(modules)} + ${ modules ? getModuleList(modules) : p(`No modules found in ${dest}/modules`) }

Templates

- ${getTemplateList(templates)} + ${ templates ? getTemplateList(templates) : p(`No modules found in ${dest}/templates`) } `; } +const p = (text) => { + return `

${text}

`; +} const getSiteList = (domains) => { return listify( diff --git a/lib/preview/routes/module.js b/lib/preview/routes/module.js index a6b73a7..fb9164b 100644 --- a/lib/preview/routes/module.js +++ b/lib/preview/routes/module.js @@ -1,12 +1,64 @@ +const http = require('http') +const { fetchModulesByPath, fetchModuleBuffer } = require('../../../api/designManager'); +const { getPreviewUrl } = require('../previewUtils'); const buildModuleRouteHandler = (sessionInfo) => { + const { portalId } = sessionInfo; + return async (req, res) => { const { name } = req.params; - + if (!name) { + res.status(200).set({ 'Content-Type': 'text/html' }).end(buildModuleIndex()); + return; + } + const customWidgetInfo = await fetchModulesByPath(portalId, `${sessionInfo.dest}/modules/${name}.module`); + if (!('moduleId' in customWidgetInfo && 'previewKey' in customWidgetInfo)) { + res.status(200).set({ 'Content-Type': 'text/html' }).end(buildErrorIndex()); + return; + } + const params = { + module_id: customWidgetInfo.moduleId, + //is_buffered: true, + //portalId: portalId, + //language: 'en', + hs_preview_key: customWidgetInfo.previewKey, + //hsPreviewerApp: 'module', + //updated: + } + const previewUrl = getPreviewUrl(sessionInfo, params); + console.log(previewUrl) + res.redirect(previewUrl); } } +const buildErrorIndex = () => { + return ` + + + + +
+

Error

+

Failed to fetch module data.

+
+ + `; +} + +const buildModuleIndex = () => { + return ` + + + + +
+

Module Index

+

Incomplete!

+
+ + `; +} module.exports = { buildModuleRouteHandler diff --git a/lib/preview/routes/proxy.js b/lib/preview/routes/proxy.js index 8fca3dc..3cc5087 100644 --- a/lib/preview/routes/proxy.js +++ b/lib/preview/routes/proxy.js @@ -9,6 +9,7 @@ const buildProxyRouteHandler = (sessionInfo) => { res.status(200).set({ 'Content-Type': 'text/html' }).end(buildProxyIndex()); return; } + // parse proxy page URL from query param let proxyPageUrl; try { @@ -27,12 +28,11 @@ const buildProxyRouteHandler = (sessionInfo) => { } try { - console.log('hello') const { portalId, contentId, hublet } = await fetchContentMetadata( proxyPageUrl.href, sessionInfo.portalId ) - console.log(`world ${portalId} ${contentId} ${hublet}`) + await proxyPage( res, portalId, @@ -41,6 +41,8 @@ const buildProxyRouteHandler = (sessionInfo) => { proxyPageUrl.href ); } catch (e) { + const message = `Failed to fetch proxy page for ${proxyPageUrl.href}`; + return res.status(400).send(message); } } } diff --git a/lib/preview/routes/template.js b/lib/preview/routes/template.js index 1d6f34a..0a27f2c 100644 --- a/lib/preview/routes/template.js +++ b/lib/preview/routes/template.js @@ -1,12 +1,66 @@ +const http = require('http') +const { fetchTemplatesByPath } = require('../../../api/designManager'); +const { getPreviewUrl } = require('../previewUtils'); const buildTemplateRouteHandler = (sessionInfo) => { + const { portalId } = sessionInfo; + return async (req, res) => { const { name } = req.params; + if (!name) { + res.status(200).set({ 'Content-Type': 'text/html' }).end(buildTemplateIndex()); + return; + } + const calculatedPath = `${sessionInfo.dest}/templates/${name}`; + const templateInfo = await fetchTemplatesByPath(portalId, calculatedPath); + if (!('previewKey' in templateInfo)) { + res.status(502).set({ 'Content-Type': 'text/html' }).end(buildErrorIndex()); + return; + } + const params = { + template_file_path: calculatedPath, + //is_buffered: true, + //portalId: portalId, + //language: 'en', + hs_preview_key: templateInfo.previewKey, + //hsPreviewerApp: 'module', + //updated + } + const previewUrl = getPreviewUrl(sessionInfo, params); + console.log(previewUrl) + res.redirect(previewUrl); } } +const buildErrorIndex = () => { + return ` + + + + +
+

Error

+

Failed to fetch template data.

+
+ + `; +} + +const buildTemplateIndex = () => { + return ` + + + + +
+

Template

+

Provide a template path in the request

+
+ + `; +} module.exports = { buildTemplateRouteHandler From fbd47017b53c6071eddb230ab68846ac0c30ffc3 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Thu, 12 Oct 2023 11:38:49 -0400 Subject: [PATCH 08/56] Remove filemapper preview changes for later --- fileMapper.js | 3 +-- lib/constants.js | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/fileMapper.js b/fileMapper.js index 44dfa20..2427c5c 100644 --- a/fileMapper.js +++ b/fileMapper.js @@ -87,7 +87,7 @@ function isAllowedExtension(filepath) { * @param {Mode} mode */ function useApiBuffer(mode) { - return mode === Mode.draft || mode === Mode.preview; + return mode === Mode.draft; } /** @@ -103,7 +103,6 @@ function getFileMapperQueryValues({ mode, options = {} }) { buffer: useApiBuffer(mode), environmentId: options.staging ? 2 : 1, version: options.assetVersion, - previewSession: options.previewSession }, }; } diff --git a/lib/constants.js b/lib/constants.js index 31b9c68..328d33c 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -34,7 +34,6 @@ const FUNCTIONS_EXTENSION = 'functions'; const Mode = { draft: 'draft', publish: 'publish', - preview: 'preview' }; const DEFAULT_MODE = Mode.publish; From b1fa76f42811e26037fe5fa42754479d0bede219 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Thu, 12 Oct 2023 14:03:36 -0400 Subject: [PATCH 09/56] Clean up preview a little bit --- lib/preview.js | 92 +++++++++++++++++++++++--------------------------- 1 file changed, 42 insertions(+), 50 deletions(-) diff --git a/lib/preview.js b/lib/preview.js index 5e011bb..c5f2d62 100644 --- a/lib/preview.js +++ b/lib/preview.js @@ -2,22 +2,18 @@ const http = require('http'); const path = require('path'); const chokidar = require('chokidar'); const express = require('express'); -const { default: PQueue } = require('p-queue'); - const { logger } = require('../logger'); -const debounce = require('debounce'); const { ApiErrorContext, logApiErrorInstance, logApiUploadErrorInstance, - logErrorInstance, } = require('../errorHandlers'); const { uploadFolder } = require('./uploadFolder'); const { shouldIgnoreFile, ignoreFile } = require('../ignoreRules'); const { getFileMapperQueryValues } = require('../fileMapper'); const { upload, deleteFile } = require('../api/fileMapper'); const escapeRegExp = require('./escapeRegExp'); -const { convertToUnixPath, isAllowedExtension, getCwd } = require('../path'); +const { convertToUnixPath, isAllowedExtension } = require('../path'); const { triggerNotify } = require('./notify'); const { getAccountConfig } = require('./config'); const { createPreviewServerRoutes } = require('./preview/createRoutes'); @@ -27,28 +23,28 @@ async function uploadFile(accountId, sessionToken, file, dest) { logger.debug(`Attempting to upload file "${src}" to "${dest}"`); const fileMapperArgs = buildFileMapperArgs(sessionToken); - return queue.add(() => { - return upload(accountId, src, dest, fileMapperArgs) - .then(() => { - logger.log(`Uploaded file ${src} to ${dest}`); - notifyPreviewUrl(accountId, sessionToken); - }) - .catch(() => { - const uploadFailureMessage = `Uploading file ${src} to ${dest} failed`; - logger.debug(uploadFailureMessage); - logger.debug(`Retrying to upload file "${src}" to "${dest}"`); - return upload(accountId, file, dest, fileMapperArgs).catch(error => { - logger.error(uploadFailureMessage); - logApiUploadErrorInstance( - error, - new ApiErrorContext({ - accountId, - request: dest, - payload: src, - }) - ); - }); - }); + return queue.add(async () => { + try { + await upload(accountId, src, dest, fileMapperArgs); + logger.log(`Uploaded file ${src} to ${dest}`); + } catch { + const uploadFailureMessage = `Uploading file ${src} to ${dest} failed`; + logger.debug(uploadFailureMessage); + logger.debug(`Retrying to upload file "${src}" to "${dest}"`); + try { + return await upload(accountId, file, dest, fileMapperArgs); + } catch (error) { + logger.error(uploadFailureMessage); + logApiUploadErrorInstance( + error, + new ApiErrorContext({ + accountId, + request: dest, + payload: src, + }) + ); + } + } }); } @@ -56,22 +52,20 @@ async function deleteRemoteFile(accountId, sessionToken, remoteFilePath) { logger.debug(`Attempting to delete file "${remoteFilePath}"`); const fileMapperArgs = buildFileMapperArgs(sessionToken); - return queue.add(() => { - return deleteFile(accountId, remoteFilePath, fileMapperArgs) - .then(() => { - logger.log(`Deleted file ${remoteFilePath}`); - notifyPreviewUrl(accountId, sessionToken); - }) - .catch(error => { - logger.error(`Deleting file ${remoteFilePath} failed`); - logApiErrorInstance( - error, - new ApiErrorContext({ - accountId, - request: remoteFilePath, - }) - ); - }); + return queue.add(async () => { + try { + await deleteFile(accountId, remoteFilePath, fileMapperArgs); + logger.log(`Deleted file ${remoteFilePath}`); + } catch (error) { + logger.error(`Deleting file ${remoteFilePath} failed`); + logApiErrorInstance( + error, + new ApiErrorContext({ + accountId, + request: remoteFilePath, + }) + ); + } }); } @@ -146,10 +140,7 @@ const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { const initialPreviewBufferUpload = async (sessionInfo, filePaths) => { const { portalId, sessionToken, src, dest } = sessionInfo const fileMapperArgs = buildFileMapperArgs(sessionToken); - // Use uploadFolder so that failures of initial upload are retried - console.log(`Uploading ${portalId} ${src} ${dest} ${fileMapperArgs} ${filePaths}`) - const result = await uploadFolder(portalId, src, dest, fileMapperArgs, {}, filePaths); - console.log(result); + return uploadFolder(portalId, src, dest, fileMapperArgs, {}, filePaths); } const startPreviewWatchingService = (sessionInfo) => { @@ -168,7 +159,7 @@ const startPreviewWatchingService = (sessionInfo) => { watcher.on('ready', () => { logger.log( - `This is the message that displays when previewing begins.` + `This is the message that displays when previewing begins! ・°˖✧◝(⁰▿⁰)◜✧˖°.` ); }); watcher.on('add', addFileCallback); @@ -205,7 +196,9 @@ const preview = async ( portalId: accountId, env: accountConfig.env, personalAccessKey: accountConfig.personalAccessKey, - hublet: 'na1', // we find this when getting content metadata during page preview, can we get that ahead of time? + // we find hublet later in the content metadata fetch + // can we get that ahead of time? hardcoding it for now + hublet: 'na1', sessionToken: Date.now(), domains, } @@ -219,7 +212,6 @@ const preview = async ( } startPreviewWatchingService(sessionInfo); startLocalHttpService(sessionInfo); - } module.exports = { From 2626400a3158284b6e77e27403e19ebcd75becb2 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Fri, 13 Oct 2023 11:10:00 -0400 Subject: [PATCH 10/56] Fixes logic around watch stuff --- lib/preview.js | 78 ++++++++++++++++++++++---------------------------- 1 file changed, 35 insertions(+), 43 deletions(-) diff --git a/lib/preview.js b/lib/preview.js index c5f2d62..bbd62b9 100644 --- a/lib/preview.js +++ b/lib/preview.js @@ -19,54 +19,50 @@ const { getAccountConfig } = require('./config'); const { createPreviewServerRoutes } = require('./preview/createRoutes'); const { getPortalDomains } = require('./preview/previewUtils'); -async function uploadFile(accountId, sessionToken, file, dest) { +async function uploadFile(accountId, sessionToken, src, dest) { logger.debug(`Attempting to upload file "${src}" to "${dest}"`); const fileMapperArgs = buildFileMapperArgs(sessionToken); - return queue.add(async () => { + try { + await upload(accountId, src, dest, fileMapperArgs); + logger.log(`Uploaded file ${src} to ${dest}`); + } catch { + const uploadFailureMessage = `Uploading file ${src} to ${dest} failed`; + logger.debug(uploadFailureMessage); + logger.debug(`Retrying to upload file "${src}" to "${dest}"`); try { await upload(accountId, src, dest, fileMapperArgs); - logger.log(`Uploaded file ${src} to ${dest}`); - } catch { - const uploadFailureMessage = `Uploading file ${src} to ${dest} failed`; - logger.debug(uploadFailureMessage); - logger.debug(`Retrying to upload file "${src}" to "${dest}"`); - try { - return await upload(accountId, file, dest, fileMapperArgs); - } catch (error) { - logger.error(uploadFailureMessage); - logApiUploadErrorInstance( - error, - new ApiErrorContext({ - accountId, - request: dest, - payload: src, - }) - ); - } - } - }); -} - -async function deleteRemoteFile(accountId, sessionToken, remoteFilePath) { - logger.debug(`Attempting to delete file "${remoteFilePath}"`); - const fileMapperArgs = buildFileMapperArgs(sessionToken); - - return queue.add(async () => { - try { - await deleteFile(accountId, remoteFilePath, fileMapperArgs); - logger.log(`Deleted file ${remoteFilePath}`); } catch (error) { - logger.error(`Deleting file ${remoteFilePath} failed`); - logApiErrorInstance( + logger.error(uploadFailureMessage); + logApiUploadErrorInstance( error, new ApiErrorContext({ accountId, - request: remoteFilePath, + request: dest, + payload: src, }) ); } - }); + } +}; + +async function deleteRemoteFile(accountId, sessionToken, remoteFilePath) { + logger.debug(`Attempting to delete file "${remoteFilePath}"`); + const fileMapperArgs = buildFileMapperArgs(sessionToken); + + try { + await deleteFile(accountId, remoteFilePath, fileMapperArgs); + logger.log(`Deleted file ${remoteFilePath}`); + } catch (error) { + logger.error(`Deleting file ${remoteFilePath} failed`); + logApiErrorInstance( + error, + new ApiErrorContext({ + accountId, + request: remoteFilePath, + }) + ); + } } const getDesignManagerPath = (src, dest, file) => { @@ -115,7 +111,7 @@ const buildDeleteFileFromPreviewBufferCallback = (sessionInfo, type) => { } const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { - const { accountId, src, sessionToken, notify } = sessionInfo; + const { portalId, src, dest, sessionToken, notify } = sessionInfo; return async (filePath) => { if (!isAllowedExtension(filePath)) { @@ -126,13 +122,9 @@ const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { logger.debug(`Skipping ${filePath} due to an ignore rule`); return; } - const destPath = getDesignManagerPath(sessionInfo, filePath); + const destPath = getDesignManagerPath(src, dest, filePath); const fileMapperArgs = buildFileMapperArgs(sessionToken) - const uploadPromise = uploadFile(accountId, filePath, destPath, { - src, - fileMapperArgs, - commandOptions, - }); + const uploadPromise = uploadFile(portalId, sessionToken, filePath, destPath); triggerNotify(notify, notifyMessage, filePath, uploadPromise); } } From 4c404b47dfd1d575da8d79bd653150e80818daeb Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Mon, 16 Oct 2023 13:15:52 -0400 Subject: [PATCH 11/56] Make initial upload off by default --- lib/preview.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/preview.js b/lib/preview.js index bbd62b9..bbd2c85 100644 --- a/lib/preview.js +++ b/lib/preview.js @@ -177,7 +177,7 @@ const preview = async ( accountId, src, dest, - { notify, filePaths, skipInitial } + { notify, filePaths, initialUpload } ) => { const accountConfig = getAccountConfig(accountId); const domains = await getPortalDomains(accountId); @@ -199,7 +199,7 @@ const preview = async ( ignoreFile(notify); } - if (!skipInitial) { + if (initialUpload) { await initialPreviewBufferUpload(sessionInfo, filePaths); } startPreviewWatchingService(sessionInfo); From dd2108d3677d9629beea3f47eace19438b42d29a Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Mon, 16 Oct 2023 15:38:43 -0400 Subject: [PATCH 12/56] Add express --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 228697f..e0153a9 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "chokidar": "^3.0.1", "content-disposition": "^0.5.3", "debounce": "^1.2.0", + "express": "^4.18.2", "extract-zip": "^1.6.7", "findup-sync": "^3.0.0", "fs-extra": "^8.1.0", From 2eae3949d0013af7a1cba6f47d4ef0929ab488db Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Mon, 16 Oct 2023 15:44:32 -0400 Subject: [PATCH 13/56] Add express dep --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 228697f..e0153a9 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "chokidar": "^3.0.1", "content-disposition": "^0.5.3", "debounce": "^1.2.0", + "express": "^4.18.2", "extract-zip": "^1.6.7", "findup-sync": "^3.0.0", "fs-extra": "^8.1.0", From d06710be04672065d6ac71089cf8c97b871290b9 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Tue, 17 Oct 2023 14:09:30 -0400 Subject: [PATCH 14/56] Add auto refreshing --- lib/preview.js | 47 +++++++++++++++++-------------------- lib/preview/createRoutes.js | 2 ++ lib/preview/previewUtils.js | 31 +++++++++++++++++++++++- lib/preview/proxyPage.js | 7 ++++-- lib/preview/routes/proxy.js | 2 +- 5 files changed, 59 insertions(+), 30 deletions(-) diff --git a/lib/preview.js b/lib/preview.js index bbd2c85..7efc10e 100644 --- a/lib/preview.js +++ b/lib/preview.js @@ -18,6 +18,7 @@ const { triggerNotify } = require('./notify'); const { getAccountConfig } = require('./config'); const { createPreviewServerRoutes } = require('./preview/createRoutes'); const { getPortalDomains } = require('./preview/previewUtils'); +const { markRemoteFsDirty } = require('./preview/routes/meta'); async function uploadFile(accountId, sessionToken, src, dest) { logger.debug(`Attempting to upload file "${src}" to "${dest}"`); @@ -26,12 +27,14 @@ async function uploadFile(accountId, sessionToken, src, dest) { try { await upload(accountId, src, dest, fileMapperArgs); logger.log(`Uploaded file ${src} to ${dest}`); + markRemoteFsDirty(); } catch { const uploadFailureMessage = `Uploading file ${src} to ${dest} failed`; logger.debug(uploadFailureMessage); logger.debug(`Retrying to upload file "${src}" to "${dest}"`); try { await upload(accountId, src, dest, fileMapperArgs); + markRemoteFsDirty(); } catch (error) { logger.error(uploadFailureMessage); logApiUploadErrorInstance( @@ -53,15 +56,23 @@ async function deleteRemoteFile(accountId, sessionToken, remoteFilePath) { try { await deleteFile(accountId, remoteFilePath, fileMapperArgs); logger.log(`Deleted file ${remoteFilePath}`); + markRemoteFsDirty(); } catch (error) { logger.error(`Deleting file ${remoteFilePath} failed`); - logApiErrorInstance( - error, - new ApiErrorContext({ - accountId, - request: remoteFilePath, - }) - ); + logger.debug(`Retrying deletion of file ${remoteFilePath}`) + try { + await deleteFile(acccountId, remoteFilePath, fileMapperArgs); + markRemoteFsDirty(); + } catch (error) { + logger.error(`Deleting file ${remoteFilePath} failed`); + logApiErrorInstance( + error, + new ApiErrorContext({ + accountId, + request: remoteFilePath, + }) + ); + } } } @@ -88,28 +99,12 @@ const buildDeleteFileFromPreviewBufferCallback = (sessionInfo, type) => { } const remotePath = getDesignManagerPath(src, dest, filePath); - logger.debug(`Attempting to delete ${type} "${remotePath}"`, type, remotePath); - queue.add(() => { - const deletePromise = deleteRemoteFile(accountId, filePath, remotePath) - .then(() => { - logger.log(`Deleted ${type} "${remotePath}"`); - }) - .catch(error => { - logger.error(`Deleting ${type} "${remotePath}" failed`); - logApiErrorInstance( - error, - new ApiErrorContext({ - accountId, - request: remotePath, - }) - ); - }); - triggerNotify(notify, 'Removed', filePath, deletePromise); - return deletePromise; - }); + const deletePromise = deleteRemoteFile(accountId, filePath, remotePath) + triggerNotify(notify, 'Removed', filePath, deletePromise); } } + const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { const { portalId, src, dest, sessionToken, notify } = sessionInfo; diff --git a/lib/preview/createRoutes.js b/lib/preview/createRoutes.js index bb6a83a..13a4df6 100644 --- a/lib/preview/createRoutes.js +++ b/lib/preview/createRoutes.js @@ -4,6 +4,7 @@ const { buildIndexRouteHandler } = require('./routes/index.js'); const { buildProxyRouteHandler } = require('./routes/proxy.js'); const { buildModuleRouteHandler } = require('./routes/module.js'); const { buildTemplateRouteHandler } = require('./routes/template.js'); +const { buildMetaRouteHandler } = require('./routes/meta.js'); const createPreviewServerRoutes = async (sessionInfo) => { const previewServerRouter = Router(); @@ -11,6 +12,7 @@ const createPreviewServerRoutes = async (sessionInfo) => { previewServerRouter.get('/proxy', buildProxyRouteHandler(sessionInfo)); previewServerRouter.get('/module/:name', buildModuleRouteHandler(sessionInfo)); previewServerRouter.get('/template/:name', buildTemplateRouteHandler(sessionInfo)); + previewServerRouter.get('/meta', buildMetaRouteHandler(sessionInfo)); return previewServerRouter; } diff --git a/lib/preview/previewUtils.js b/lib/preview/previewUtils.js index d5c6a65..6fd2709 100644 --- a/lib/preview/previewUtils.js +++ b/lib/preview/previewUtils.js @@ -39,9 +39,38 @@ const stringifyQuery = (query) => { .map(key => `${key}=${query[key]}`) .join('&'); } + +const insertAtEndOfBody = (html, script) => { + const insertAt = (baseStr, index, insertStr) => { + return `${baseStr.slice(0, index)}${insertStr}${baseStr.slice(index)}`; + } + const endOfBodyIndex = html.lastIndexOf(""); + return insertAt(html, endOfBodyIndex, script); +} + +const addRefreshScript = (html) => { + const refreshScript = ` + + `; + return insertAtEndOfBody(html, refreshScript); +} + module.exports = { getPortalDomains, getModules, getTemplates, - getPreviewUrl + getPreviewUrl, + addRefreshScript } diff --git a/lib/preview/proxyPage.js b/lib/preview/proxyPage.js index e38d3b7..b64566a 100644 --- a/lib/preview/proxyPage.js +++ b/lib/preview/proxyPage.js @@ -1,5 +1,6 @@ const http = require('../../http'); const { fetchPreviewInfo } = require('../../api/preview'); +const { addRefreshScript } = require('./previewUtils'); const proxyPage = async ( res, @@ -11,6 +12,7 @@ const proxyPage = async ( try { const previewInfo = await fetchPreviewInfo(portalId, contentId); + console.log('Successfully fetched previewInfo') const { previewKey } = previewInfo; const pageHtml = await makeProxyPreviewRequest( @@ -19,8 +21,9 @@ const proxyPage = async ( previewKey, contentId ) - - res.status(200).set({ 'Content-Type': 'text/html' }).end(pageHtml); + console.log('Successfully fetched pageHtml') + const wrappedHtml = addRefreshScript(pageHtml); + res.status(200).set({ 'Content-Type': 'text/html' }).end(wrappedHtml); } catch (error) { // TODO change error.stack to error.message before we publish response diff --git a/lib/preview/routes/proxy.js b/lib/preview/routes/proxy.js index 3cc5087..7a6e253 100644 --- a/lib/preview/routes/proxy.js +++ b/lib/preview/routes/proxy.js @@ -32,7 +32,7 @@ const buildProxyRouteHandler = (sessionInfo) => { proxyPageUrl.href, sessionInfo.portalId ) - + console.log('Successfully fetched content metadata') await proxyPage( res, portalId, From 4e20f17309a7de7d69760ccbe5eae4ca0c4b4309 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Tue, 17 Oct 2023 14:09:36 -0400 Subject: [PATCH 15/56] Add auto refreshing --- lib/preview/routes/meta.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 lib/preview/routes/meta.js diff --git a/lib/preview/routes/meta.js b/lib/preview/routes/meta.js new file mode 100644 index 0000000..804828a --- /dev/null +++ b/lib/preview/routes/meta.js @@ -0,0 +1,22 @@ +const hsServerState = { + REMOTE_FS_IS_DIRTY: false +} + +const buildMetaRouteHandler = (sessionInfo) => { + return async (req, res) => { + console.log(hsServerState) + res.json(hsServerState); + hsServerState["REMOTE_FS_IS_DIRTY"] = false; + } +} + +const markRemoteFsDirty = () => { + hsServerState["REMOTE_FS_IS_DIRTY"] = true; +} + + + +module.exports = { + buildMetaRouteHandler, + markRemoteFsDirty, +} From a3bd4d9d2fe72ff07f8879c35ee30f21c451848f Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Tue, 17 Oct 2023 14:19:47 -0400 Subject: [PATCH 16/56] Remove logs --- lib/preview/proxyPage.js | 5 ++--- lib/preview/routes/proxy.js | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/preview/proxyPage.js b/lib/preview/proxyPage.js index b64566a..4560ccb 100644 --- a/lib/preview/proxyPage.js +++ b/lib/preview/proxyPage.js @@ -1,7 +1,7 @@ const http = require('../../http'); const { fetchPreviewInfo } = require('../../api/preview'); const { addRefreshScript } = require('./previewUtils'); - +`` const proxyPage = async ( res, portalId, @@ -12,7 +12,6 @@ const proxyPage = async ( try { const previewInfo = await fetchPreviewInfo(portalId, contentId); - console.log('Successfully fetched previewInfo') const { previewKey } = previewInfo; const pageHtml = await makeProxyPreviewRequest( @@ -21,7 +20,7 @@ const proxyPage = async ( previewKey, contentId ) - console.log('Successfully fetched pageHtml') + const wrappedHtml = addRefreshScript(pageHtml); res.status(200).set({ 'Content-Type': 'text/html' }).end(wrappedHtml); } catch (error) { diff --git a/lib/preview/routes/proxy.js b/lib/preview/routes/proxy.js index 7a6e253..3cc5087 100644 --- a/lib/preview/routes/proxy.js +++ b/lib/preview/routes/proxy.js @@ -32,7 +32,7 @@ const buildProxyRouteHandler = (sessionInfo) => { proxyPageUrl.href, sessionInfo.portalId ) - console.log('Successfully fetched content metadata') + await proxyPage( res, portalId, From c7ec29b4874c183605d836cf034e4641b96b8751 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Tue, 17 Oct 2023 14:20:15 -0400 Subject: [PATCH 17/56] Whitespace --- lib/preview/routes/meta.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/preview/routes/meta.js b/lib/preview/routes/meta.js index 804828a..ce55e57 100644 --- a/lib/preview/routes/meta.js +++ b/lib/preview/routes/meta.js @@ -14,8 +14,6 @@ const markRemoteFsDirty = () => { hsServerState["REMOTE_FS_IS_DIRTY"] = true; } - - module.exports = { buildMetaRouteHandler, markRemoteFsDirty, From e05c847aef631a7592713fd313a710f0e3c01468 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Tue, 17 Oct 2023 14:22:31 -0400 Subject: [PATCH 18/56] Whitespace, remove logs --- lib/preview.js | 1 - lib/preview/previewUtils.js | 1 - lib/preview/proxyPage.js | 6 +++--- lib/preview/routes/meta.js | 1 - 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/preview.js b/lib/preview.js index 7efc10e..a726c10 100644 --- a/lib/preview.js +++ b/lib/preview.js @@ -104,7 +104,6 @@ const buildDeleteFileFromPreviewBufferCallback = (sessionInfo, type) => { } } - const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { const { portalId, src, dest, sessionToken, notify } = sessionInfo; diff --git a/lib/preview/previewUtils.js b/lib/preview/previewUtils.js index 6fd2709..2801be0 100644 --- a/lib/preview/previewUtils.js +++ b/lib/preview/previewUtils.js @@ -55,7 +55,6 @@ const addRefreshScript = (html) => { const res = fetch('/meta') .then(async (res) => { const hsServerState = await res.json(); - console.log(hsServerState) if (hsServerState["REMOTE_FS_IS_DIRTY"]) { location.reload(); } diff --git a/lib/preview/proxyPage.js b/lib/preview/proxyPage.js index 4560ccb..bd78025 100644 --- a/lib/preview/proxyPage.js +++ b/lib/preview/proxyPage.js @@ -1,7 +1,7 @@ const http = require('../../http'); const { fetchPreviewInfo } = require('../../api/preview'); const { addRefreshScript } = require('./previewUtils'); -`` + const proxyPage = async ( res, portalId, @@ -21,8 +21,8 @@ const proxyPage = async ( contentId ) - const wrappedHtml = addRefreshScript(pageHtml); - res.status(200).set({ 'Content-Type': 'text/html' }).end(wrappedHtml); + const embeddedHtml = addRefreshScript(pageHtml); + res.status(200).set({ 'Content-Type': 'text/html' }).end(embeddedHtml); } catch (error) { // TODO change error.stack to error.message before we publish response diff --git a/lib/preview/routes/meta.js b/lib/preview/routes/meta.js index ce55e57..75a026b 100644 --- a/lib/preview/routes/meta.js +++ b/lib/preview/routes/meta.js @@ -4,7 +4,6 @@ const hsServerState = { const buildMetaRouteHandler = (sessionInfo) => { return async (req, res) => { - console.log(hsServerState) res.json(hsServerState); hsServerState["REMOTE_FS_IS_DIRTY"] = false; } From 2d6676c2636a2fb34019ccae5614075454e883f1 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Thu, 19 Oct 2023 14:06:47 -0400 Subject: [PATCH 19/56] use nodefetchcommonjs, add routes --- lib/preview/createRoutes.js | 13 ++++++++++++- package.json | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/preview/createRoutes.js b/lib/preview/createRoutes.js index 13a4df6..3078d1a 100644 --- a/lib/preview/createRoutes.js +++ b/lib/preview/createRoutes.js @@ -6,13 +6,24 @@ const { buildModuleRouteHandler } = require('./routes/module.js'); const { buildTemplateRouteHandler } = require('./routes/template.js'); const { buildMetaRouteHandler } = require('./routes/meta.js'); +const { proxyResourceRedirectHandler } = require('./routes/proxyResourceRedirect.js'); + const createPreviewServerRoutes = async (sessionInfo) => { const previewServerRouter = Router(); - previewServerRouter.get('/', buildIndexRouteHandler(sessionInfo)); previewServerRouter.get('/proxy', buildProxyRouteHandler(sessionInfo)); previewServerRouter.get('/module/:name', buildModuleRouteHandler(sessionInfo)); previewServerRouter.get('/template/:name', buildTemplateRouteHandler(sessionInfo)); previewServerRouter.get('/meta', buildMetaRouteHandler(sessionInfo)); + + previewServerRouter.get('/*', proxyResourceRedirectHandler); + previewServerRouter.post('/*', proxyResourceRedirectHandler); + previewServerRouter.delete('/*', proxyResourceRedirectHandler); + previewServerRouter.head('/*', proxyResourceRedirectHandler); + previewServerRouter.put('/*', proxyResourceRedirectHandler); + previewServerRouter.options('/*', proxyResourceRedirectHandler); + + previewServerRouter.get('/', buildIndexRouteHandler(sessionInfo)); + return previewServerRouter; } diff --git a/package.json b/package.json index e0153a9..f8cc9e8 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "jest": "^26.6.3", "js-yaml": "^4.1.0", "moment": "^2.24.0", + "node-fetch-commonjs": "^3.3.2", "p-queue": "^6.0.2", "prettier": "^1.19.1", "request": "^2.87.0", From 872e25b2c1e330c19a19efa2c69b0ba28b57fa69 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Thu, 19 Oct 2023 14:07:07 -0400 Subject: [PATCH 20/56] proxyResourceRedirect.js --- lib/preview/routes/proxyResourceRedirect.js | 49 +++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 lib/preview/routes/proxyResourceRedirect.js diff --git a/lib/preview/routes/proxyResourceRedirect.js b/lib/preview/routes/proxyResourceRedirect.js new file mode 100644 index 0000000..fbf6b1f --- /dev/null +++ b/lib/preview/routes/proxyResourceRedirect.js @@ -0,0 +1,49 @@ +const fetch = require('node-fetch-commonjs'); + +const HCMS_PATH = '/_hcms/'; +const HS_FS_PATH = '/hs-fs/' +const HUB_FS_PATH = '/hubfs/'; + +const isInternalCMSRoute = (req) => + req.path.startsWith(HCMS_PATH) || + req.path.startsWith(HS_FS_PATH) || + req.path.startsWith(HUB_FS_PATH); + +const proxyResourceRedirectHandler = (req, res, next) => { + const isAHtmlRequest = req.accepts().includes('text/html'); + + // proxy when referer's path is /proxy and has page query param + let proxyPageUrl; + let refererUrl; + try { + refererUrl = new URL(req.headers.referer); + proxyPageUrl = new URL( + new URL(req.headers.referer).searchParams.get('page') + ); + } catch (e) { + next(); + return; + } + if (!refererUrl.pathname.startsWith('/proxy')) { + next(); + return; + } + + // handle anchor links unless internal CMS route + if (isAHtmlRequest && !isInternalCMSRoute(req)) { + const encodedPageUrl = encodeURIComponent( + `${proxyPageUrl.origin}${req.url}` + ); + res.redirect(`/proxy?page=${encodedPageUrl}`); + return; + } + + const urlToProxy = `https://${proxyPageUrl.host}${req.url}`; + fetch(urlToProxy) + .then(response => response.body.pipe(res)) // https://github.com/node-fetch/node-fetch#bodybody + .catch(err => console.log(`crying on ${err}`)); +} + +module.exports = { + proxyResourceRedirectHandler, +} From caaf52598a9c7854bc591107135b5545822d085e Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Thu, 19 Oct 2023 14:09:00 -0400 Subject: [PATCH 21/56] Error log --- lib/preview/routes/proxyResourceRedirect.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/preview/routes/proxyResourceRedirect.js b/lib/preview/routes/proxyResourceRedirect.js index fbf6b1f..74fd42f 100644 --- a/lib/preview/routes/proxyResourceRedirect.js +++ b/lib/preview/routes/proxyResourceRedirect.js @@ -41,7 +41,7 @@ const proxyResourceRedirectHandler = (req, res, next) => { const urlToProxy = `https://${proxyPageUrl.host}${req.url}`; fetch(urlToProxy) .then(response => response.body.pipe(res)) // https://github.com/node-fetch/node-fetch#bodybody - .catch(err => console.log(`crying on ${err}`)); + .catch(err => console.error(err)); } module.exports = { From 4818e70ff41248af0efc6e24ea5a808de9782ead Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Tue, 24 Oct 2023 14:05:30 -0400 Subject: [PATCH 22/56] @preview folder support, module/template proxying/auto-refreshing --- lib/preview.js | 6 ++++-- lib/preview/createRoutes.js | 4 ++-- lib/preview/previewUtils.js | 2 +- lib/preview/routes/module.js | 28 ++++++++++++++++------------ lib/preview/routes/template.js | 29 ++++++++++++++++------------- 5 files changed, 39 insertions(+), 30 deletions(-) diff --git a/lib/preview.js b/lib/preview.js index a726c10..0daf2e4 100644 --- a/lib/preview.js +++ b/lib/preview.js @@ -2,6 +2,7 @@ const http = require('http'); const path = require('path'); const chokidar = require('chokidar'); const express = require('express'); +const { v4: uuidv4 } = require('uuid'); const { logger } = require('../logger'); const { ApiErrorContext, @@ -175,9 +176,10 @@ const preview = async ( ) => { const accountConfig = getAccountConfig(accountId); const domains = await getPortalDomains(accountId); + const sessionToken = uuidv4(); const sessionInfo = { src, - dest, + dest: `@preview/${sessionToken}/${dest}`, portalName: accountConfig.name, portalId: accountId, env: accountConfig.env, @@ -185,7 +187,7 @@ const preview = async ( // we find hublet later in the content metadata fetch // can we get that ahead of time? hardcoding it for now hublet: 'na1', - sessionToken: Date.now(), + sessionToken, domains, } diff --git a/lib/preview/createRoutes.js b/lib/preview/createRoutes.js index 3078d1a..136f6cf 100644 --- a/lib/preview/createRoutes.js +++ b/lib/preview/createRoutes.js @@ -11,8 +11,8 @@ const { proxyResourceRedirectHandler } = require('./routes/proxyResourceRedirect const createPreviewServerRoutes = async (sessionInfo) => { const previewServerRouter = Router(); previewServerRouter.get('/proxy', buildProxyRouteHandler(sessionInfo)); - previewServerRouter.get('/module/:name', buildModuleRouteHandler(sessionInfo)); - previewServerRouter.get('/template/:name', buildTemplateRouteHandler(sessionInfo)); + previewServerRouter.get('/module/:modulePath', buildModuleRouteHandler(sessionInfo)); + previewServerRouter.get('/template/:templatePath', buildTemplateRouteHandler(sessionInfo)); previewServerRouter.get('/meta', buildMetaRouteHandler(sessionInfo)); previewServerRouter.get('/*', proxyResourceRedirectHandler); diff --git a/lib/preview/previewUtils.js b/lib/preview/previewUtils.js index 2801be0..33aa4b4 100644 --- a/lib/preview/previewUtils.js +++ b/lib/preview/previewUtils.js @@ -52,7 +52,7 @@ const addRefreshScript = (html) => { const refreshScript = ` `; return insertAtEndOfBody(html, refreshScript); @@ -78,14 +78,14 @@ const getSubDomainFromValidLocalDomain = hostname => { }; // From(ish) https://git.hubteam.com/HubSpot/cloudflare-workers/blob/master/worker-lib/src/Constants.ts -const HCMS_PATH = '/_hcms/'; -const HS_FS_PATH = '/hs-fs/'; -const HUB_FS_PATH = '/hubfs/'; +const internalRoutes = { + HCMS: '/_hcms/', + HS_FS: '/hs-fs/', + HUB_FS: '/hubfs/' +} const isInternalCMSRoute = (req) => - req.path.startsWith(HCMS_PATH) || - req.path.startsWith(HS_FS_PATH) || - req.path.startsWith(HUB_FS_PATH); + Object.values(internalRoutes).some((route => req.path.startsWith(route))); const silenceConsoleWhile = async (act, ...args) => { const tmpConsole = console; @@ -95,14 +95,32 @@ const silenceConsoleWhile = async (act, ...args) => { return result; } +const memoize = (func, cacheBustCallback) => { + const cache = {}; + return async (...args) => { + const stringArgs = args.toString(); + const storedResult = cache[stringArgs]; + if (storedResult && (cacheBustCallback ? !cacheBustCallback() : true)) { + //console.log(`Cache hit ${func}`) + return storedResult; + } + //console.log(`Cache miss ${func}`) + const res = await func(...args); + cache[stringArgs] = res; + return res; + } +} + +const lieAboutDest = (previewDest) => previewDest.split('/').slice(2).join('/'); + module.exports = { isInternalCMSRoute, VALID_PROXY_DOMAIN_SUFFIXES, getSubDomainFromValidLocalDomain, getPortalDomains, - getModules, - getTemplates, getPreviewUrl, addRefreshScript, - silenceConsoleWhile + silenceConsoleWhile, + memoize, + lieAboutDest } diff --git a/lib/preview/proxyPage.js b/lib/preview/proxyPage.js index bd78025..05cf054 100644 --- a/lib/preview/proxyPage.js +++ b/lib/preview/proxyPage.js @@ -1,6 +1,8 @@ const http = require('../../http'); const { fetchPreviewInfo } = require('../../api/preview'); -const { addRefreshScript } = require('./previewUtils'); +const { addRefreshScript, memoize } = require('./previewUtils'); + +const cachedFetchPreviewInfo = memoize(fetchPreviewInfo); const proxyPage = async ( res, @@ -10,7 +12,7 @@ const proxyPage = async ( urlToProxy, ) => { try { - const previewInfo = await fetchPreviewInfo(portalId, contentId); + const previewInfo = await cachedFetchPreviewInfo(portalId, contentId); const { previewKey } = previewInfo; @@ -41,10 +43,6 @@ const makeProxyPreviewRequest = async ( contentId ) => { const url = new URL(urlToProxy); - console.log(`Proxying request to ${url.href}`, { - previewKey, - contentId - }); let proxiedQueryParams = {}; diff --git a/lib/preview/routes/index.js b/lib/preview/routes/index.js index b6fc950..7ba4453 100644 --- a/lib/preview/routes/index.js +++ b/lib/preview/routes/index.js @@ -1,4 +1,9 @@ -const { getModules, getTemplates } = require('../previewUtils'); +const { lieAboutDest } = require('../previewUtils'); +const { + fetchTemplatesPathStartsWith, + fetchModulesPathStartsWith +} = require('../../../api/designManager'); +const { parse: pathParse } = require('path'); const buildIndexRouteHandler = (sessionInfo) => { return async (req, res) => { @@ -8,59 +13,113 @@ const buildIndexRouteHandler = (sessionInfo) => { } const buildIndexHtml = async (sessionInfo) => { - const { domains, dest } = sessionInfo; - const modules = await getModules(sessionInfo); - const templates = await getTemplates(sessionInfo); - + const { domains, dest, PORT } = sessionInfo; + const fakeDest = lieAboutDest(dest); + const modulesHtml = await getModulesForDisplayToUser(sessionInfo); + const templatesHtml = await getTemplatesForDisplayToUser(sessionInfo); return ` - - - -

Domains

- ${ domains ? getSiteList(domains) : p('No domains found') } -

Modules

- ${ modules ? getModuleList(modules) : p(`No modules found in ${dest}/modules`) } -

Templates

- ${ templates ? getTemplateList(templates) : p(`No modules found in ${dest}/templates`) } - + + + + +
+

Domains

+ ${ domains ? getSiteList(domains, PORT) : '

No domains found

' } +
+
+
+

Modules

+ ${ modulesHtml ? modulesHtml : `

No modules found in ${fakeDest}

` } +
+
+

Templates

+ ${ templatesHtml ? templatesHtml : `

No templates found in ${fakeDest}

` } +
+
+ + `; } -const p = (text) => { - return `

${text}

`; -} - -const getSiteList = (domains) => { +const getSiteList = (domains, PORT) => { return listify( domains, - domainObj => `http://${domainObj.domain}.hslocal.net:3000`, + domainObj => `http://${domainObj.domain}.hslocal.net:${PORT}`, domainObj => domainObj.domain ); } -const getModuleList = (modules) => { - return listify( - modules.map(module => module.split('.')[0]), - moduleName => `http://hslocal.net:3000/module/${moduleName}`, - moduleName => moduleName - ); -} - -const getTemplateList = (templates) => { - return listify( - templates, - templateName => `http://hslocal.net:3000/template/${templateName}`, - templateName => templateName - ) -} - const listify = (objects, hrefBuilder, labelBuilder) => { return ''; } +const getModulesForDisplayToUser = async (sessionInfo) => { + const { portalId, dest } = sessionInfo; + try { + const res = await fetchModulesPathStartsWith(portalId, dest); + const modulePaths = res + .objects + .map(moduleObj => moduleObj.path); + const filesGroupedByFolder = groupByFolder(modulePaths) + const htmlToRender = renderFilesGroupedByFolder(filesGroupedByFolder, 'module') + return htmlToRender; + } catch (err) { + console.log(`Failed to fetch modules for index page: ${err}`); + return undefined; + } +} + +const getTemplatesForDisplayToUser = async (sessionInfo) => { + const { portalId, dest } = sessionInfo; + try { + const res = await fetchTemplatesPathStartsWith(portalId, dest); + const templatePaths = res + .objects + .filter(templateObj => templateObj.filename.split('.')[1] === 'html') + .map(templateObj => templateObj.path); + const filesGroupedByFolder = groupByFolder(templatePaths) + const htmlToRender = renderFilesGroupedByFolder(filesGroupedByFolder, 'template') + return htmlToRender; + } catch (err) { + console.log(`Failed to fetch templates for index page: ${err}`); + return undefined; + } +} + +const groupByFolder = (paths) => { + return paths.reduce((acc, cur) => { + const { dir, base } = pathParse(cur); + if (Object.keys(acc).includes(dir)) { + acc[dir].push(base); + } else { + acc[dir] = [base]; + } + return acc; + }, {}) +} + +const renderFilesGroupedByFolder = (filesGroupedByFolder, endpoint) => { + const folders = Object.keys(filesGroupedByFolder); + return folders.reduce((outer_acc, folder) => { + const fakeDest = lieAboutDest(folder); + const folderDisplay = '' + + fakeDest + + '
    ' + + filesGroupedByFolder[folder].reduce((acc, cur) => { + const newListItem = `
  • ${cur}
  • ` + acc += newListItem; + return acc; + }, '') + + '
'; + outer_acc += folderDisplay; + return outer_acc; + }, ''); +} + + module.exports = { buildIndexRouteHandler } diff --git a/lib/preview/routes/meta.js b/lib/preview/routes/meta.js index 75a026b..7041d21 100644 --- a/lib/preview/routes/meta.js +++ b/lib/preview/routes/meta.js @@ -13,7 +13,12 @@ const markRemoteFsDirty = () => { hsServerState["REMOTE_FS_IS_DIRTY"] = true; } +const remoteFsIsDirty = () => { + return hsServerState["REMOTE_FS_IS_DIRTY"]; +} + module.exports = { buildMetaRouteHandler, markRemoteFsDirty, + remoteFsIsDirty } diff --git a/lib/preview/routes/module.js b/lib/preview/routes/module.js index 5883b7d..e3fb30a 100644 --- a/lib/preview/routes/module.js +++ b/lib/preview/routes/module.js @@ -1,24 +1,37 @@ const http = require('../../../http') -const { fetchModulesByPath, fetchModuleBuffer } = require('../../../api/designManager'); -const { addRefreshScript, getPreviewUrl } = require('../previewUtils'); +const { fetchModulesByPath } = require('../../../api/designManager'); +const { addRefreshScript, getPreviewUrl, memoize } = require('../previewUtils'); + +const cachedFetchModulesByPath = memoize(fetchModulesByPath); const buildModuleRouteHandler = (sessionInfo) => { - const { portalId } = sessionInfo; + const { portalId, sessionToken } = sessionInfo; return async (req, res) => { + console.log('Fetching module preview...') + const { modulePath } = req.params; + if (!modulePath) { res.status(200).set({ 'Content-Type': 'text/html' }).end(buildModuleIndex()); return; } - const customWidgetInfo = await fetchModulesByPath(portalId, `${sessionInfo.dest}/modules/${modulePath}.module`); - if (!('moduleId' in customWidgetInfo && 'previewKey' in customWidgetInfo)) { + + const calculatedPath = `@preview/${sessionToken}/${modulePath}.module`; + let customWidgetInfo; + try { + customWidgetInfo = await cachedFetchModulesByPath(portalId, calculatedPath); + } catch (err) { + console.log(`Failed to fetch module preview for ${calculatedPath}`) + } + if (!customWidgetInfo || !('moduleId' in customWidgetInfo && 'previewKey' in customWidgetInfo)) { res.status(200).set({ 'Content-Type': 'text/html' }).end(buildErrorIndex()); return; } const params = { module_id: customWidgetInfo.moduleId, hs_preview_key: customWidgetInfo.previewKey, + updated: Date.now(), ...req.query } const previewUrl = new URL(getPreviewUrl(sessionInfo, params)) ; diff --git a/lib/preview/routes/proxyPageRouteHandler.js b/lib/preview/routes/proxyPageRouteHandler.js index a42ea34..2978fc5 100644 --- a/lib/preview/routes/proxyPageRouteHandler.js +++ b/lib/preview/routes/proxyPageRouteHandler.js @@ -3,11 +3,10 @@ const { proxyPage } = require('../proxyPage.js'); const { getSubDomainFromValidLocalDomain, VALID_PROXY_DOMAIN_SUFFIXES, + memoize, } = require('../previewUtils.js'); -const fullOriginalUrlFromRequest = (request) => { - return `${request.protocol}://${request.get('host')}${request.originalUrl}`; -} +const cachedFetchContentMetadata = memoize(fetchContentMetadata); const buildProxyPageRouteHandler = (sessionInfo) => { return async (request, response, next) => { @@ -15,7 +14,6 @@ const buildProxyPageRouteHandler = (sessionInfo) => { return response.sendStatus(404); } - const fullOriginalUrl = fullOriginalUrlFromRequest(request); const domainToProxy = getSubDomainFromValidLocalDomain(request.hostname); if (!domainToProxy) { @@ -29,9 +27,10 @@ const buildProxyPageRouteHandler = (sessionInfo) => { } } const urlToProxy = `http://${domainToProxy}${request.originalUrl}`; + console.log(`Fetching preview for ${urlToProxy}`); try { - const { portalId, contentId, hublet } = await fetchContentMetadata( + const { portalId, contentId, hublet } = await cachedFetchContentMetadata( urlToProxy, sessionInfo.portalId ); @@ -42,7 +41,6 @@ const buildProxyPageRouteHandler = (sessionInfo) => { hublet, contentId, urlToProxy, - fullOriginalUrl ); } catch (e) { next(e); diff --git a/lib/preview/routes/template.js b/lib/preview/routes/template.js index 6868344..4d28520 100644 --- a/lib/preview/routes/template.js +++ b/lib/preview/routes/template.js @@ -1,21 +1,30 @@ const http = require('../../../http'); const { fetchTemplatesByPath } = require('../../../api/designManager'); -const { getPreviewUrl } = require('../previewUtils'); -const { addRefreshScript } = require('../previewUtils'); +const { getPreviewUrl, addRefreshScript, memoize } = require('../previewUtils'); +const cachedFetchTemplatesByPath = memoize(fetchTemplatesByPath); const buildTemplateRouteHandler = (sessionInfo) => { - const { portalId } = sessionInfo; + const { portalId, sessionToken } = sessionInfo; return async (req, res) => { + console.log(`Fetching template preview...`); + const { templatePath } = req.params; + console.log(templatePath) if (!templatePath) { res.status(200).set({ 'Content-Type': 'text/html' }).end(buildTemplateIndex()); return; } - const calculatedPath = `${sessionInfo.dest}/templates/${templatePath}`; - const templateInfo = await fetchTemplatesByPath(portalId, calculatedPath); - if (!('previewKey' in templateInfo)) { + + const calculatedPath = `@preview/${sessionToken}/${templatePath}`; + let templateInfo; + try { + templateInfo = await cachedFetchTemplatesByPath(portalId, calculatedPath); + } catch (err) { + console.log(`Failed to fetch template info for ${calculatedPath}`); + } + if (!templateInfo || !('previewKey' in templateInfo)) { res.status(502).set({ 'Content-Type': 'text/html' }).end(buildErrorIndex()); return; } diff --git a/lib/preview/shadowDevServer.js b/lib/preview/shadowDevServer.js index 7f11e79..b72136a 100644 --- a/lib/preview/shadowDevServer.js +++ b/lib/preview/shadowDevServer.js @@ -5,7 +5,7 @@ const cors = require("cors"); const SHADOW_PORT = 1442; -const startShadowDevService = async (sessionInfo) => { +const startShadowDevServer = async (sessionInfo) => { const portIsTaken = await new Promise((res, rej) => { console.log(`Starting proxy link server on port ${SHADOW_PORT}`); const testNetServer = net.createServer(); @@ -42,23 +42,24 @@ const startShadowDevService = async (sessionInfo) => { shadowDevServer.use( '/', - await createShadowDevServerRoutes() + await createShadowDevServerRoutes(sessionInfo) ); } -const createShadowDevServerRoutes = async () => { +const createShadowDevServerRoutes = async (sessionInfo) => { const shadowDevServerRoutes = Router(); shadowDevServerRoutes.get( '/check-if-local-dev-server', cors(), - shadowDevServerCheckHandler(), + shadowDevServerCheckHandler(sessionInfo), ); return shadowDevServerRoutes; } -const shadowDevServerCheckHandler = () => async (req, res) => { +const shadowDevServerCheckHandler = (sessionInfo) => async (req, res) => { + const { PORT } = sessionInfo; const { query } = req; if (query) { @@ -71,7 +72,7 @@ const shadowDevServerCheckHandler = () => async (req, res) => { let localProxyUrl; hasJSBuildingBlocks = false; - localProxyUrl = `http://${hostName}.hslocal.net:3000${pathName}`; + localProxyUrl = `http://${hostName}.hslocal.net:${PORT}${pathName}`; res .status(200) @@ -81,5 +82,5 @@ const shadowDevServerCheckHandler = () => async (req, res) => { } module.exports = { - startShadowDevService + startShadowDevServer } From 4a6b0453c61df9883abe91c9aebdc7cddb7c9a96 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Mon, 6 Nov 2023 10:48:44 -0500 Subject: [PATCH 27/56] Cleanup some semicolons, preview tracking --- lib/preview.js | 8 ++--- lib/preview/previewUtils.js | 36 +++++++++++++++++-- lib/preview/routes/index.js | 11 ++++-- lib/preview/routes/module.js | 1 + lib/preview/routes/proxyPageRouteHandler.js | 1 + .../routes/proxyPathPageRouteHandler.js.js | 2 +- 6 files changed, 48 insertions(+), 11 deletions(-) diff --git a/lib/preview.js b/lib/preview.js index 2a3f86d..5ee6fc3 100644 --- a/lib/preview.js +++ b/lib/preview.js @@ -101,7 +101,7 @@ const buildDeleteFileFromPreviewBufferCallback = (sessionInfo, type) => { } const remotePath = getDesignManagerPath(src, dest, filePath); - const deletePromise = deleteRemoteFile(accountId, filePath, remotePath) + const deletePromise = deleteRemoteFile(accountId, filePath, remotePath); triggerNotify(notify, 'Removed', filePath, deletePromise); } } @@ -119,14 +119,14 @@ const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { return; } const destPath = getDesignManagerPath(src, dest, filePath); - const fileMapperArgs = buildFileMapperArgs(sessionToken) + const fileMapperArgs = buildFileMapperArgs(sessionToken); const uploadPromise = uploadFile(portalId, sessionToken, filePath, destPath); triggerNotify(notify, notifyMessage, filePath, uploadPromise); } } const initialPreviewBufferUpload = async (sessionInfo, filePaths) => { - const { portalId, sessionToken, src, dest } = sessionInfo + const { portalId, sessionToken, src, dest } = sessionInfo; const fileMapperArgs = buildFileMapperArgs(sessionToken); return uploadFolder(portalId, src, dest, fileMapperArgs, {}, filePaths); } @@ -180,7 +180,7 @@ const preview = async ( src, dest, { notify, filePaths, skipUpload, noSsl, port } -) => {`x` +) => { const accountConfig = getAccountConfig(accountId); const domains = await getPortalDomains(accountId); const sessionToken = 'test' // uuidv4(); diff --git a/lib/preview/previewUtils.js b/lib/preview/previewUtils.js index c69a647..9e7b2f0 100644 --- a/lib/preview/previewUtils.js +++ b/lib/preview/previewUtils.js @@ -1,5 +1,7 @@ const { fetchDomains } = require('../../api/domains'); -const { getDirectoryContentsByPath } = require('../../api/fileMapper'); +const { getAccountId, isTrackingAllowed } = require('../config'); +const { platform, release } = require('os'); +const { trackUsage } = require('../../api/fileMapper'); const VALID_PROXY_DOMAIN_SUFFIXES = ['localhost', 'hslocal.net']; @@ -111,7 +113,34 @@ const memoize = (func, cacheBustCallback) => { } } -const lieAboutDest = (previewDest) => previewDest.split('/').slice(2).join('/'); +const hidePreviewInDest = (previewDest) => previewDest.split('/').slice(2).join('/'); + +const trackPreviewEvent = async (action) => { + if (!isTrackingAllowed()) { + return; + } + const accountId = getAccountId(); + + trackUsage( + 'cms-cli-usage', + 'INTERACTION', + { + ...options, + ...{ + applicationName: 'hubspot.preview', + os: `${platform()} ${release()}`, + authType: getAuthType(accountId), + }, + action, + }, + accountId + ).then( + (val) => console.log(`Succesfully tracked ${val}`), + (err) => { + console.error(`trackUsage failed: ${err}`); + } + ); +} module.exports = { isInternalCMSRoute, @@ -122,5 +151,6 @@ module.exports = { addRefreshScript, silenceConsoleWhile, memoize, - lieAboutDest + hidePreviewInDest, + trackPreviewEvent } diff --git a/lib/preview/routes/index.js b/lib/preview/routes/index.js index 7ba4453..1885705 100644 --- a/lib/preview/routes/index.js +++ b/lib/preview/routes/index.js @@ -1,4 +1,7 @@ -const { lieAboutDest } = require('../previewUtils'); +const { + hidePreviewInDest, + trackPreviewEvent, +} = require('../previewUtils'); const { fetchTemplatesPathStartsWith, fetchModulesPathStartsWith @@ -7,6 +10,8 @@ const { parse: pathParse } = require('path'); const buildIndexRouteHandler = (sessionInfo) => { return async (req, res) => { + trackPreviewEvent('view-index-route'); + const responseHTML = await buildIndexHtml(sessionInfo); res.status(200).set({ 'Content-Type': 'text/html' }).end(responseHTML); } @@ -14,7 +19,7 @@ const buildIndexRouteHandler = (sessionInfo) => { const buildIndexHtml = async (sessionInfo) => { const { domains, dest, PORT } = sessionInfo; - const fakeDest = lieAboutDest(dest); + const fakeDest = hidePreviewInDest(dest); const modulesHtml = await getModulesForDisplayToUser(sessionInfo); const templatesHtml = await getTemplatesForDisplayToUser(sessionInfo); return ` @@ -104,7 +109,7 @@ const groupByFolder = (paths) => { const renderFilesGroupedByFolder = (filesGroupedByFolder, endpoint) => { const folders = Object.keys(filesGroupedByFolder); return folders.reduce((outer_acc, folder) => { - const fakeDest = lieAboutDest(folder); + const fakeDest = hidePreviewInDest(folder); const folderDisplay = '' + fakeDest + '
    ' diff --git a/lib/preview/routes/module.js b/lib/preview/routes/module.js index e3fb30a..e6217ee 100644 --- a/lib/preview/routes/module.js +++ b/lib/preview/routes/module.js @@ -9,6 +9,7 @@ const buildModuleRouteHandler = (sessionInfo) => { return async (req, res) => { console.log('Fetching module preview...') + trackPreviewEvent('view-module-route'); const { modulePath } = req.params; diff --git a/lib/preview/routes/proxyPageRouteHandler.js b/lib/preview/routes/proxyPageRouteHandler.js index 2978fc5..643818d 100644 --- a/lib/preview/routes/proxyPageRouteHandler.js +++ b/lib/preview/routes/proxyPageRouteHandler.js @@ -26,6 +26,7 @@ const buildProxyPageRouteHandler = (sessionInfo) => { return response.status(400).send(message); } } + trackPreviewEvent('proxy-page-route') const urlToProxy = `http://${domainToProxy}${request.originalUrl}`; console.log(`Fetching preview for ${urlToProxy}`); diff --git a/lib/preview/routes/proxyPathPageRouteHandler.js.js b/lib/preview/routes/proxyPathPageRouteHandler.js.js index a7e8083..2216dad 100644 --- a/lib/preview/routes/proxyPathPageRouteHandler.js.js +++ b/lib/preview/routes/proxyPathPageRouteHandler.js.js @@ -25,7 +25,7 @@ const buildProxyRouteHandler = (sessionInfo) => { console.warn(message); return res.status(400).send(message); } - + trackPreviewEvent('proxy-path-route'); try { const { portalId, contentId, hublet } = await fetchContentMetadata( proxyPageUrl.href, From 5cb6ae13768111ed129bdf8c8caa0638d85a1a03 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Mon, 6 Nov 2023 11:39:57 -0500 Subject: [PATCH 28/56] Add getauthtype --- lib/preview/previewUtils.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/preview/previewUtils.js b/lib/preview/previewUtils.js index 9e7b2f0..e57da6a 100644 --- a/lib/preview/previewUtils.js +++ b/lib/preview/previewUtils.js @@ -1,5 +1,5 @@ const { fetchDomains } = require('../../api/domains'); -const { getAccountId, isTrackingAllowed } = require('../config'); +const { getAccountId, isTrackingAllowed, getAccountConfig } = require('../config'); const { platform, release } = require('os'); const { trackUsage } = require('../../api/fileMapper'); @@ -125,7 +125,6 @@ const trackPreviewEvent = async (action) => { 'cms-cli-usage', 'INTERACTION', { - ...options, ...{ applicationName: 'hubspot.preview', os: `${platform()} ${release()}`, @@ -134,14 +133,27 @@ const trackPreviewEvent = async (action) => { action, }, accountId - ).then( - (val) => console.log(`Succesfully tracked ${val}`), + ).catch( (err) => { console.error(`trackUsage failed: ${err}`); } ); } +const getAuthType = (accountId) => { + let authType = 'unknown'; + + if (accountId) { + const accountConfig = getAccountConfig(accountId); + authType = + accountConfig && accountConfig.authType + ? accountConfig.authType + : 'apikey'; + } + + return authType; +}; + module.exports = { isInternalCMSRoute, VALID_PROXY_DOMAIN_SUFFIXES, From 57bd6c38d645e8db58d0370c5cf2e646c7243308 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Mon, 6 Nov 2023 16:48:37 -0500 Subject: [PATCH 29/56] Add all changes for now... --- api/designManager.js | 18 ++-- api/domains.js | 8 +- api/preview.js | 21 ++--- lib/preview.js | 84 ++++++++++++------- lib/preview/previewUtils.js | 12 ++- lib/preview/routes/module.js | 8 +- lib/preview/routes/proxyPageRouteHandler.js | 2 +- .../routes/proxyPathPageRouteHandler.js.js | 1 + lib/preview/routes/template.js | 4 +- package.json | 3 +- 10 files changed, 92 insertions(+), 69 deletions(-) diff --git a/api/designManager.js b/api/designManager.js index 6b0d200..92904a2 100644 --- a/api/designManager.js +++ b/api/designManager.js @@ -45,26 +45,26 @@ async function fetchRawAssetByPath(accountId, path) { async function fetchModulesByPath(accountId, path) { return http.get(accountId, { - uri: `${DESIGN_MANAGER_API_PATH}/modules/by-path/${path}?portalId=${accountId}` - }) + uri: `${DESIGN_MANAGER_API_PATH}/modules/by-path/${path}?portalId=${accountId}`, + }); } async function fetchModulesPathStartsWith(accountId, path) { return http.get(accountId, { - uri: `${DESIGN_MANAGER_API_PATH}/modules?portalId=${accountId}&path__startswith=${path}` - }) + uri: `${DESIGN_MANAGER_API_PATH}/modules?portalId=${accountId}&path__startswith=${path}`, + }); } async function fetchTemplatesByPath(accountId, path) { return http.get(accountId, { - uri: `${DESIGN_MANAGER_API_PATH}/templates/by-path/${path}?portalId=${accountId}` - }) + uri: `${DESIGN_MANAGER_API_PATH}/templates/by-path/${path}?portalId=${accountId}`, + }); } async function fetchTemplatesPathStartsWith(accountId, path) { return http.get(accountId, { - uri: `${DESIGN_MANAGER_API_PATH}/templates?portalId=${accountId}&path__startswith=${path}` - }) + uri: `${DESIGN_MANAGER_API_PATH}/templates?portalId=${accountId}&path__startswith=${path}`, + }); } module.exports = { @@ -75,5 +75,5 @@ module.exports = { fetchModulesByPath, fetchModulesPathStartsWith, fetchTemplatesByPath, - fetchTemplatesPathStartsWith + fetchTemplatesPathStartsWith, }; diff --git a/api/domains.js b/api/domains.js index 47571bd..fd1f97a 100644 --- a/api/domains.js +++ b/api/domains.js @@ -1,4 +1,4 @@ -const http = require('../http') +const http = require('../http'); const DOMAINS_API_PATH = `/cms/v3/domains`; @@ -6,10 +6,10 @@ async function fetchDomains(accountId) { try { const result = await http.get(accountId, { uri: DOMAINS_API_PATH, - json: true + json: true, }); - return result.results + return result.results; } catch (err) { throw err; } @@ -17,4 +17,4 @@ async function fetchDomains(accountId) { module.exports = { fetchDomains, -} +}; diff --git a/api/preview.js b/api/preview.js index 77957e1..f73f279 100644 --- a/api/preview.js +++ b/api/preview.js @@ -1,4 +1,4 @@ -const { get } = require('../http') +const { get } = require('../http'); const http = require('http'); const https = require('https'); const { getAccountId } = require('../lib/config'); @@ -10,7 +10,7 @@ async function fetchPreviewInfo(accountId, contentId) { const content = await get(accountId, { uri: `${CONTENT_API_PATH}/${contentId}`, query: { portalId: accountId }, - json: true + json: true, }); return { @@ -49,9 +49,9 @@ const requestContentPreview = async (url, portalId) => { }); return response; -} +}; -const requestPage = (url, redirects = 0, originalUrl=undefined) => { +const requestPage = (url, redirects = 0, originalUrl = undefined) => { if (redirects > 5) { throw new Error( `Hit too many redirects to HEAD requests for ${originalUrl}` @@ -96,12 +96,9 @@ const requestPage = (url, redirects = 0, originalUrl=undefined) => { }) .end(); }); -} +}; -const fetchContentMetadata = async ( - url, - portalId -) => { +const fetchContentMetadata = async (url, portalId) => { let res; if (url.includes('hs_preview')) { res = await requestContentPreview(url, portalId); @@ -156,9 +153,9 @@ const fetchContentMetadata = async ( return { portalId, contentId, hublet }; } -} +}; module.exports = { fetchPreviewInfo, - fetchContentMetadata -} + fetchContentMetadata, +}; diff --git a/lib/preview.js b/lib/preview.js index 5ee6fc3..b46d781 100644 --- a/lib/preview.js +++ b/lib/preview.js @@ -20,8 +20,10 @@ const { getAccountConfig } = require('./config'); const { createPreviewServerRoutes } = require('./preview/createRoutes'); const { getPortalDomains } = require('./preview/previewUtils'); const { markRemoteFsDirty } = require('./preview/routes/meta'); -const { startShadowDevServer } = require('./preview/shadowDevServer') -const { createHttpsRedirectingServer } = require('./preview/httpsRedirectingServer'); +const { startShadowDevServer } = require('./preview/shadowDevServer'); +const { + createHttpsRedirectingServer, +} = require('./preview/httpsRedirectingServer'); async function uploadFile(accountId, sessionToken, src, dest) { logger.debug(`Attempting to upload file "${src}" to "${dest}"`); @@ -50,7 +52,7 @@ async function uploadFile(accountId, sessionToken, src, dest) { ); } } -}; +} async function deleteRemoteFile(accountId, sessionToken, remoteFilePath) { logger.debug(`Attempting to delete file "${remoteFilePath}"`); @@ -62,7 +64,7 @@ async function deleteRemoteFile(accountId, sessionToken, remoteFilePath) { markRemoteFsDirty(); } catch (error) { logger.error(`Deleting file ${remoteFilePath} failed`); - logger.debug(`Retrying deletion of file ${remoteFilePath}`) + logger.debug(`Retrying deletion of file ${remoteFilePath}`); try { await deleteFile(acccountId, remoteFilePath, fileMapperArgs); markRemoteFsDirty(); @@ -85,16 +87,16 @@ const getDesignManagerPath = (src, dest, file) => { return convertToUnixPath(path.join(dest, relativePath)); }; -const buildFileMapperArgs = (sessionToken) => { +const buildFileMapperArgs = sessionToken => { return getFileMapperQueryValues({ mode: 'publish', }); -} +}; const buildDeleteFileFromPreviewBufferCallback = (sessionInfo, type) => { const { accountId, src, dest, notify } = sessionInfo; - return (filePath) => { + return filePath => { if (shouldIgnoreFile(filePath)) { logger.debug(`Skipping ${filePath} due to an ignore rule`); return; @@ -103,13 +105,13 @@ const buildDeleteFileFromPreviewBufferCallback = (sessionInfo, type) => { const remotePath = getDesignManagerPath(src, dest, filePath); const deletePromise = deleteRemoteFile(accountId, filePath, remotePath); triggerNotify(notify, 'Removed', filePath, deletePromise); - } -} + }; +}; const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { const { portalId, src, dest, sessionToken, notify } = sessionInfo; - return async (filePath) => { + return async filePath => { if (!isAllowedExtension(filePath)) { logger.debug(`Skipping ${filePath} due to unsupported extension`); return; @@ -120,19 +122,23 @@ const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { } const destPath = getDesignManagerPath(src, dest, filePath); const fileMapperArgs = buildFileMapperArgs(sessionToken); - const uploadPromise = uploadFile(portalId, sessionToken, filePath, destPath); + const uploadPromise = uploadFile( + portalId, + sessionToken, + filePath, + destPath + ); triggerNotify(notify, notifyMessage, filePath, uploadPromise); - } -} + }; +}; const initialPreviewBufferUpload = async (sessionInfo, filePaths) => { const { portalId, sessionToken, src, dest } = sessionInfo; const fileMapperArgs = buildFileMapperArgs(sessionToken); return uploadFolder(portalId, src, dest, fileMapperArgs, {}, filePaths); -} - +}; -const startPreviewWatcher = async (sessionInfo) => { +const startPreviewWatcher = async sessionInfo => { const { src } = sessionInfo; let watcherIsReady = false; @@ -141,10 +147,22 @@ const startPreviewWatcher = async (sessionInfo) => { ignored: file => shouldIgnoreFile(file), }); - const addFileCallback = buildUploadFileToPreviewBufferCallback(sessionInfo, 'Added'); - const changeFileCallback = buildUploadFileToPreviewBufferCallback(sessionInfo, 'Change'); - const deleteFileCallback = buildDeleteFileFromPreviewBufferCallback(sessionInfo, 'file'); - const deleteFolderCallback = buildDeleteFileFromPreviewBufferCallback(sessionInfo, 'folder'); + const addFileCallback = buildUploadFileToPreviewBufferCallback( + sessionInfo, + 'Added' + ); + const changeFileCallback = buildUploadFileToPreviewBufferCallback( + sessionInfo, + 'Change' + ); + const deleteFileCallback = buildDeleteFileFromPreviewBufferCallback( + sessionInfo, + 'file' + ); + const deleteFolderCallback = buildDeleteFileFromPreviewBufferCallback( + sessionInfo, + 'folder' + ); watcher.on('ready', () => { console.log('Local file watching service has started!'); @@ -156,24 +174,24 @@ const startPreviewWatcher = async (sessionInfo) => { watcher.on('unlinkDir', deleteFolderCallback); function sleep(ms) { - return new Promise((resolve) => { + return new Promise(resolve => { setTimeout(resolve, ms); }); } while (!watcherIsReady) { - await sleep(1) + await sleep(1); // do nothing... surely there's a better way to do this... } return watcher; -} +}; -const createLocalHttpServer = async (sessionInfo) => { +const createLocalHttpServer = async sessionInfo => { const expressServer = express(); //expressServer.use(bodyParser.json()); expressServer.use('/', await createPreviewServerRoutes(sessionInfo)); return expressServer; -} +}; const preview = async ( accountId, @@ -183,7 +201,7 @@ const preview = async ( ) => { const accountConfig = getAccountConfig(accountId); const domains = await getPortalDomains(accountId); - const sessionToken = 'test' // uuidv4(); + const sessionToken = 'test'; // uuidv4(); const PORT = port || 3000; const protocol = noSsl ? 'http' : 'https'; @@ -201,7 +219,7 @@ const preview = async ( domains, PORT, protocol, - } + }; if (notify) { ignoreFile(notify); @@ -214,15 +232,21 @@ const preview = async ( const previewWatcher = await startPreviewWatcher(sessionInfo); if (!noSsl) { - const { server, innerHTTPServer, innerHTTPSServer } = await createHttpsRedirectingServer(expressServer, domains); + const { + server, + innerHTTPServer, + innerHTTPSServer, + } = await createHttpsRedirectingServer(expressServer, domains); server.listen(PORT); } else { const httpServer = http.createServer(expressServer); httpServer.listen(PORT); } startShadowDevServer(sessionInfo); - console.log(`HubSpot preview local dev server hosting at ${protocol}://hslocal.net:${PORT}, portalId=${accountId}`) -} + console.log( + `HubSpot preview local dev server hosting at ${protocol}://hslocal.net:${PORT}, portalId=${accountId}` + ); +}; module.exports = { preview, diff --git a/lib/preview/previewUtils.js b/lib/preview/previewUtils.js index e57da6a..c6a7eb5 100644 --- a/lib/preview/previewUtils.js +++ b/lib/preview/previewUtils.js @@ -122,20 +122,18 @@ const trackPreviewEvent = async (action) => { const accountId = getAccountId(); trackUsage( - 'cms-cli-usage', + 'cli-interaction', 'INTERACTION', { - ...{ - applicationName: 'hubspot.preview', - os: `${platform()} ${release()}`, - authType: getAuthType(accountId), - }, + applicationName: 'hubspot.preview', + os: `${platform()} ${release()}`, + authType: getAuthType(accountId), action, }, accountId ).catch( (err) => { - console.error(`trackUsage failed: ${err}`); + console.error(`trackUsage failed: ${JSON.stringify(err, null, 2)}`); } ); } diff --git a/lib/preview/routes/module.js b/lib/preview/routes/module.js index e6217ee..91c580c 100644 --- a/lib/preview/routes/module.js +++ b/lib/preview/routes/module.js @@ -1,6 +1,11 @@ const http = require('../../../http') const { fetchModulesByPath } = require('../../../api/designManager'); -const { addRefreshScript, getPreviewUrl, memoize } = require('../previewUtils'); +const { + addRefreshScript, + getPreviewUrl, + memoize, + trackPreviewEvent +} = require('../previewUtils'); const cachedFetchModulesByPath = memoize(fetchModulesByPath); @@ -8,7 +13,6 @@ const buildModuleRouteHandler = (sessionInfo) => { const { portalId, sessionToken } = sessionInfo; return async (req, res) => { - console.log('Fetching module preview...') trackPreviewEvent('view-module-route'); const { modulePath } = req.params; diff --git a/lib/preview/routes/proxyPageRouteHandler.js b/lib/preview/routes/proxyPageRouteHandler.js index 643818d..e438c97 100644 --- a/lib/preview/routes/proxyPageRouteHandler.js +++ b/lib/preview/routes/proxyPageRouteHandler.js @@ -4,6 +4,7 @@ const { getSubDomainFromValidLocalDomain, VALID_PROXY_DOMAIN_SUFFIXES, memoize, + trackPreviewEvent } = require('../previewUtils.js'); const cachedFetchContentMetadata = memoize(fetchContentMetadata); @@ -28,7 +29,6 @@ const buildProxyPageRouteHandler = (sessionInfo) => { } trackPreviewEvent('proxy-page-route') const urlToProxy = `http://${domainToProxy}${request.originalUrl}`; - console.log(`Fetching preview for ${urlToProxy}`); try { const { portalId, contentId, hublet } = await cachedFetchContentMetadata( diff --git a/lib/preview/routes/proxyPathPageRouteHandler.js.js b/lib/preview/routes/proxyPathPageRouteHandler.js.js index 2216dad..ea131ea 100644 --- a/lib/preview/routes/proxyPathPageRouteHandler.js.js +++ b/lib/preview/routes/proxyPathPageRouteHandler.js.js @@ -1,5 +1,6 @@ const { fetchContentMetadata } = require('../../../api/preview'); const { proxyPage } = require('../proxyPage'); +const { trackPreviewEvent } = require('../previewUtils'); const buildProxyRouteHandler = (sessionInfo) => { diff --git a/lib/preview/routes/template.js b/lib/preview/routes/template.js index 4d28520..9910d2b 100644 --- a/lib/preview/routes/template.js +++ b/lib/preview/routes/template.js @@ -8,10 +8,8 @@ const buildTemplateRouteHandler = (sessionInfo) => { const { portalId, sessionToken } = sessionInfo; return async (req, res) => { - console.log(`Fetching template preview...`); - const { templatePath } = req.params; - console.log(templatePath) + if (!templatePath) { res.status(200).set({ 'Content-Type': 'text/html' }).end(buildTemplateIndex()); return; diff --git a/package.json b/package.json index 66aed4e..457dfee 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "scripts": { "check-main": "branch=$(git rev-parse --abbrev-ref HEAD) && [ $branch = main ] || (echo 'Error: New release can only be published on main branch' && exit 1)", "lint": "eslint . && prettier --list-different packages/**/*.{js,json}", - "prettier:write": "prettier --write packages/**/*.{js,json}", + "prettier:write": "prettier --write **/*.{js,json}", "pub": "npm publish --tag latest", "push": "git push --atomic origin main v$npm_package_version", "release:major": "yarn check-main && yarn version --major && yarn pub && yarn push", @@ -34,6 +34,7 @@ "js-yaml": "^4.1.0", "mkcert-cli": "^1.5.0", "moment": "^2.24.0", + "net": "^1.0.2", "node-fetch-commonjs": "^3.3.2", "p-queue": "^6.0.2", "prettier": "^1.19.1", From 18b4cd53e9762c12be83603bdf4a10af27aa7d31 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Mon, 6 Nov 2023 17:46:59 -0500 Subject: [PATCH 30/56] Undo prettier --- api/designManager.js | 8 +++--- api/domains.js | 8 +++--- api/preview.js | 21 +++++++++------- lib/preview.js | 58 ++++++++++++++++++-------------------------- package.json | 1 - 5 files changed, 43 insertions(+), 53 deletions(-) diff --git a/api/designManager.js b/api/designManager.js index 92904a2..f1ccc5d 100644 --- a/api/designManager.js +++ b/api/designManager.js @@ -45,8 +45,8 @@ async function fetchRawAssetByPath(accountId, path) { async function fetchModulesByPath(accountId, path) { return http.get(accountId, { - uri: `${DESIGN_MANAGER_API_PATH}/modules/by-path/${path}?portalId=${accountId}`, - }); + uri: `${DESIGN_MANAGER_API_PATH}/modules/by-path/${path}?portalId=${accountId}` + }) } async function fetchModulesPathStartsWith(accountId, path) { @@ -57,8 +57,8 @@ async function fetchModulesPathStartsWith(accountId, path) { async function fetchTemplatesByPath(accountId, path) { return http.get(accountId, { - uri: `${DESIGN_MANAGER_API_PATH}/templates/by-path/${path}?portalId=${accountId}`, - }); + uri: `${DESIGN_MANAGER_API_PATH}/templates/by-path/${path}?portalId=${accountId}` + }) } async function fetchTemplatesPathStartsWith(accountId, path) { diff --git a/api/domains.js b/api/domains.js index fd1f97a..47571bd 100644 --- a/api/domains.js +++ b/api/domains.js @@ -1,4 +1,4 @@ -const http = require('../http'); +const http = require('../http') const DOMAINS_API_PATH = `/cms/v3/domains`; @@ -6,10 +6,10 @@ async function fetchDomains(accountId) { try { const result = await http.get(accountId, { uri: DOMAINS_API_PATH, - json: true, + json: true }); - return result.results; + return result.results } catch (err) { throw err; } @@ -17,4 +17,4 @@ async function fetchDomains(accountId) { module.exports = { fetchDomains, -}; +} diff --git a/api/preview.js b/api/preview.js index f73f279..77957e1 100644 --- a/api/preview.js +++ b/api/preview.js @@ -1,4 +1,4 @@ -const { get } = require('../http'); +const { get } = require('../http') const http = require('http'); const https = require('https'); const { getAccountId } = require('../lib/config'); @@ -10,7 +10,7 @@ async function fetchPreviewInfo(accountId, contentId) { const content = await get(accountId, { uri: `${CONTENT_API_PATH}/${contentId}`, query: { portalId: accountId }, - json: true, + json: true }); return { @@ -49,9 +49,9 @@ const requestContentPreview = async (url, portalId) => { }); return response; -}; +} -const requestPage = (url, redirects = 0, originalUrl = undefined) => { +const requestPage = (url, redirects = 0, originalUrl=undefined) => { if (redirects > 5) { throw new Error( `Hit too many redirects to HEAD requests for ${originalUrl}` @@ -96,9 +96,12 @@ const requestPage = (url, redirects = 0, originalUrl = undefined) => { }) .end(); }); -}; +} -const fetchContentMetadata = async (url, portalId) => { +const fetchContentMetadata = async ( + url, + portalId +) => { let res; if (url.includes('hs_preview')) { res = await requestContentPreview(url, portalId); @@ -153,9 +156,9 @@ const fetchContentMetadata = async (url, portalId) => { return { portalId, contentId, hublet }; } -}; +} module.exports = { fetchPreviewInfo, - fetchContentMetadata, -}; + fetchContentMetadata +} diff --git a/lib/preview.js b/lib/preview.js index b46d781..838fcdb 100644 --- a/lib/preview.js +++ b/lib/preview.js @@ -52,7 +52,7 @@ async function uploadFile(accountId, sessionToken, src, dest) { ); } } -} +}; async function deleteRemoteFile(accountId, sessionToken, remoteFilePath) { logger.debug(`Attempting to delete file "${remoteFilePath}"`); @@ -64,7 +64,7 @@ async function deleteRemoteFile(accountId, sessionToken, remoteFilePath) { markRemoteFsDirty(); } catch (error) { logger.error(`Deleting file ${remoteFilePath} failed`); - logger.debug(`Retrying deletion of file ${remoteFilePath}`); + logger.debug(`Retrying deletion of file ${remoteFilePath}`) try { await deleteFile(acccountId, remoteFilePath, fileMapperArgs); markRemoteFsDirty(); @@ -87,31 +87,31 @@ const getDesignManagerPath = (src, dest, file) => { return convertToUnixPath(path.join(dest, relativePath)); }; -const buildFileMapperArgs = sessionToken => { +const buildFileMapperArgs = (sessionToken) => { return getFileMapperQueryValues({ mode: 'publish', }); -}; +} const buildDeleteFileFromPreviewBufferCallback = (sessionInfo, type) => { const { accountId, src, dest, notify } = sessionInfo; - return filePath => { + return (filePath) => { if (shouldIgnoreFile(filePath)) { logger.debug(`Skipping ${filePath} due to an ignore rule`); return; } const remotePath = getDesignManagerPath(src, dest, filePath); - const deletePromise = deleteRemoteFile(accountId, filePath, remotePath); + const deletePromise = deleteRemoteFile(accountId, filePath, remotePath) triggerNotify(notify, 'Removed', filePath, deletePromise); - }; -}; + } +} const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { const { portalId, src, dest, sessionToken, notify } = sessionInfo; - return async filePath => { + return async (filePath) => { if (!isAllowedExtension(filePath)) { logger.debug(`Skipping ${filePath} due to unsupported extension`); return; @@ -121,7 +121,7 @@ const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { return; } const destPath = getDesignManagerPath(src, dest, filePath); - const fileMapperArgs = buildFileMapperArgs(sessionToken); + const fileMapperArgs = buildFileMapperArgs(sessionToken) const uploadPromise = uploadFile( portalId, sessionToken, @@ -129,16 +129,16 @@ const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { destPath ); triggerNotify(notify, notifyMessage, filePath, uploadPromise); - }; -}; + } +} const initialPreviewBufferUpload = async (sessionInfo, filePaths) => { - const { portalId, sessionToken, src, dest } = sessionInfo; + const { portalId, sessionToken, src, dest } = sessionInfo const fileMapperArgs = buildFileMapperArgs(sessionToken); return uploadFolder(portalId, src, dest, fileMapperArgs, {}, filePaths); -}; +} -const startPreviewWatcher = async sessionInfo => { +const startPreviewWatcher = async (sessionInfo) => { const { src } = sessionInfo; let watcherIsReady = false; @@ -147,22 +147,10 @@ const startPreviewWatcher = async sessionInfo => { ignored: file => shouldIgnoreFile(file), }); - const addFileCallback = buildUploadFileToPreviewBufferCallback( - sessionInfo, - 'Added' - ); - const changeFileCallback = buildUploadFileToPreviewBufferCallback( - sessionInfo, - 'Change' - ); - const deleteFileCallback = buildDeleteFileFromPreviewBufferCallback( - sessionInfo, - 'file' - ); - const deleteFolderCallback = buildDeleteFileFromPreviewBufferCallback( - sessionInfo, - 'folder' - ); + const addFileCallback = buildUploadFileToPreviewBufferCallback(sessionInfo, 'Added'); + const changeFileCallback = buildUploadFileToPreviewBufferCallback(sessionInfo, 'Change'); + const deleteFileCallback = buildDeleteFileFromPreviewBufferCallback(sessionInfo, 'file'); + const deleteFolderCallback = buildDeleteFileFromPreviewBufferCallback(sessionInfo, 'folder'); watcher.on('ready', () => { console.log('Local file watching service has started!'); @@ -183,7 +171,7 @@ const startPreviewWatcher = async sessionInfo => { // do nothing... surely there's a better way to do this... } return watcher; -}; +} const createLocalHttpServer = async sessionInfo => { const expressServer = express(); @@ -191,7 +179,7 @@ const createLocalHttpServer = async sessionInfo => { expressServer.use('/', await createPreviewServerRoutes(sessionInfo)); return expressServer; -}; +} const preview = async ( accountId, @@ -201,7 +189,7 @@ const preview = async ( ) => { const accountConfig = getAccountConfig(accountId); const domains = await getPortalDomains(accountId); - const sessionToken = 'test'; // uuidv4(); + const sessionToken = uuidv4(); const PORT = port || 3000; const protocol = noSsl ? 'http' : 'https'; @@ -219,7 +207,7 @@ const preview = async ( domains, PORT, protocol, - }; + } if (notify) { ignoreFile(notify); diff --git a/package.json b/package.json index 457dfee..536e280 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "js-yaml": "^4.1.0", "mkcert-cli": "^1.5.0", "moment": "^2.24.0", - "net": "^1.0.2", "node-fetch-commonjs": "^3.3.2", "p-queue": "^6.0.2", "prettier": "^1.19.1", From 3fddbe0fa75ce6dd256eead0c9da085f46c09e7b Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Mon, 6 Nov 2023 17:52:14 -0500 Subject: [PATCH 31/56] More remove --- lib/preview.js | 19 +++++-------------- lib/preview/routes/meta.js | 5 ----- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/lib/preview.js b/lib/preview.js index 838fcdb..8f184dd 100644 --- a/lib/preview.js +++ b/lib/preview.js @@ -87,12 +87,6 @@ const getDesignManagerPath = (src, dest, file) => { return convertToUnixPath(path.join(dest, relativePath)); }; -const buildFileMapperArgs = (sessionToken) => { - return getFileMapperQueryValues({ - mode: 'publish', - }); -} - const buildDeleteFileFromPreviewBufferCallback = (sessionInfo, type) => { const { accountId, src, dest, notify } = sessionInfo; @@ -121,13 +115,7 @@ const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { return; } const destPath = getDesignManagerPath(src, dest, filePath); - const fileMapperArgs = buildFileMapperArgs(sessionToken) - const uploadPromise = uploadFile( - portalId, - sessionToken, - filePath, - destPath - ); + const uploadPromise = uploadFile(portalId, sessionToken, filePath, destPath); triggerNotify(notify, notifyMessage, filePath, uploadPromise); } } @@ -135,7 +123,9 @@ const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { const initialPreviewBufferUpload = async (sessionInfo, filePaths) => { const { portalId, sessionToken, src, dest } = sessionInfo const fileMapperArgs = buildFileMapperArgs(sessionToken); - return uploadFolder(portalId, src, dest, fileMapperArgs, {}, filePaths); + return uploadFolder(portalId, src, dest, getFileMapperQueryValues({ + mode: 'publish', + }), {}, filePaths); } const startPreviewWatcher = async (sessionInfo) => { @@ -147,6 +137,7 @@ const startPreviewWatcher = async (sessionInfo) => { ignored: file => shouldIgnoreFile(file), }); + const addFileCallback = buildUploadFileToPreviewBufferCallback(sessionInfo, 'Added'); const changeFileCallback = buildUploadFileToPreviewBufferCallback(sessionInfo, 'Change'); const deleteFileCallback = buildDeleteFileFromPreviewBufferCallback(sessionInfo, 'file'); diff --git a/lib/preview/routes/meta.js b/lib/preview/routes/meta.js index 7041d21..75a026b 100644 --- a/lib/preview/routes/meta.js +++ b/lib/preview/routes/meta.js @@ -13,12 +13,7 @@ const markRemoteFsDirty = () => { hsServerState["REMOTE_FS_IS_DIRTY"] = true; } -const remoteFsIsDirty = () => { - return hsServerState["REMOTE_FS_IS_DIRTY"]; -} - module.exports = { buildMetaRouteHandler, markRemoteFsDirty, - remoteFsIsDirty } From bc206ad1a89cabd0772823573d26081ec746571c Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Tue, 7 Nov 2023 10:09:06 -0500 Subject: [PATCH 32/56] Fix filemapperarg mess I made --- lib/preview.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/preview.js b/lib/preview.js index 8f184dd..a34e3b4 100644 --- a/lib/preview.js +++ b/lib/preview.js @@ -25,9 +25,12 @@ const { createHttpsRedirectingServer, } = require('./preview/httpsRedirectingServer'); +const fileMapperArgs = getFileMapperQueryValues({ + mode: 'publish', +}); + async function uploadFile(accountId, sessionToken, src, dest) { logger.debug(`Attempting to upload file "${src}" to "${dest}"`); - const fileMapperArgs = buildFileMapperArgs(sessionToken); try { await upload(accountId, src, dest, fileMapperArgs); @@ -56,7 +59,6 @@ async function uploadFile(accountId, sessionToken, src, dest) { async function deleteRemoteFile(accountId, sessionToken, remoteFilePath) { logger.debug(`Attempting to delete file "${remoteFilePath}"`); - const fileMapperArgs = buildFileMapperArgs(sessionToken); try { await deleteFile(accountId, remoteFilePath, fileMapperArgs); @@ -122,10 +124,8 @@ const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { const initialPreviewBufferUpload = async (sessionInfo, filePaths) => { const { portalId, sessionToken, src, dest } = sessionInfo - const fileMapperArgs = buildFileMapperArgs(sessionToken); - return uploadFolder(portalId, src, dest, getFileMapperQueryValues({ - mode: 'publish', - }), {}, filePaths); + + return uploadFolder(portalId, src, dest, fileMapperArgs, {}, filePaths); } const startPreviewWatcher = async (sessionInfo) => { From 7af2be9e5180e73e52c2ef2aef6465dba2a2ffd1 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Tue, 7 Nov 2023 10:15:38 -0500 Subject: [PATCH 33/56] Remove unused args --- lib/preview.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/preview.js b/lib/preview.js index a34e3b4..eacd234 100644 --- a/lib/preview.js +++ b/lib/preview.js @@ -29,7 +29,7 @@ const fileMapperArgs = getFileMapperQueryValues({ mode: 'publish', }); -async function uploadFile(accountId, sessionToken, src, dest) { +async function uploadFile(accountId, src, dest) { logger.debug(`Attempting to upload file "${src}" to "${dest}"`); try { @@ -57,7 +57,7 @@ async function uploadFile(accountId, sessionToken, src, dest) { } }; -async function deleteRemoteFile(accountId, sessionToken, remoteFilePath) { +async function deleteRemoteFile(accountId, remoteFilePath) { logger.debug(`Attempting to delete file "${remoteFilePath}"`); try { @@ -99,13 +99,13 @@ const buildDeleteFileFromPreviewBufferCallback = (sessionInfo, type) => { } const remotePath = getDesignManagerPath(src, dest, filePath); - const deletePromise = deleteRemoteFile(accountId, filePath, remotePath) + const deletePromise = deleteRemoteFile(accountId, remotePath) triggerNotify(notify, 'Removed', filePath, deletePromise); } } const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { - const { portalId, src, dest, sessionToken, notify } = sessionInfo; + const { portalId, src, dest, notify } = sessionInfo; return async (filePath) => { if (!isAllowedExtension(filePath)) { @@ -117,13 +117,13 @@ const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { return; } const destPath = getDesignManagerPath(src, dest, filePath); - const uploadPromise = uploadFile(portalId, sessionToken, filePath, destPath); + const uploadPromise = uploadFile(portalId, filePath, destPath); triggerNotify(notify, notifyMessage, filePath, uploadPromise); } } const initialPreviewBufferUpload = async (sessionInfo, filePaths) => { - const { portalId, sessionToken, src, dest } = sessionInfo + const { portalId, src, dest } = sessionInfo return uploadFolder(portalId, src, dest, fileMapperArgs, {}, filePaths); } From d0d181b8327a33ff51ad1f82c83e463be2efdf5b Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Tue, 7 Nov 2023 10:15:58 -0500 Subject: [PATCH 34/56] Prettier --- api/designManager.js | 8 +++---- api/domains.js | 8 +++---- api/preview.js | 21 ++++++++---------- lib/preview.js | 51 +++++++++++++++++++++++++++----------------- 4 files changed, 48 insertions(+), 40 deletions(-) diff --git a/api/designManager.js b/api/designManager.js index f1ccc5d..92904a2 100644 --- a/api/designManager.js +++ b/api/designManager.js @@ -45,8 +45,8 @@ async function fetchRawAssetByPath(accountId, path) { async function fetchModulesByPath(accountId, path) { return http.get(accountId, { - uri: `${DESIGN_MANAGER_API_PATH}/modules/by-path/${path}?portalId=${accountId}` - }) + uri: `${DESIGN_MANAGER_API_PATH}/modules/by-path/${path}?portalId=${accountId}`, + }); } async function fetchModulesPathStartsWith(accountId, path) { @@ -57,8 +57,8 @@ async function fetchModulesPathStartsWith(accountId, path) { async function fetchTemplatesByPath(accountId, path) { return http.get(accountId, { - uri: `${DESIGN_MANAGER_API_PATH}/templates/by-path/${path}?portalId=${accountId}` - }) + uri: `${DESIGN_MANAGER_API_PATH}/templates/by-path/${path}?portalId=${accountId}`, + }); } async function fetchTemplatesPathStartsWith(accountId, path) { diff --git a/api/domains.js b/api/domains.js index 47571bd..fd1f97a 100644 --- a/api/domains.js +++ b/api/domains.js @@ -1,4 +1,4 @@ -const http = require('../http') +const http = require('../http'); const DOMAINS_API_PATH = `/cms/v3/domains`; @@ -6,10 +6,10 @@ async function fetchDomains(accountId) { try { const result = await http.get(accountId, { uri: DOMAINS_API_PATH, - json: true + json: true, }); - return result.results + return result.results; } catch (err) { throw err; } @@ -17,4 +17,4 @@ async function fetchDomains(accountId) { module.exports = { fetchDomains, -} +}; diff --git a/api/preview.js b/api/preview.js index 77957e1..f73f279 100644 --- a/api/preview.js +++ b/api/preview.js @@ -1,4 +1,4 @@ -const { get } = require('../http') +const { get } = require('../http'); const http = require('http'); const https = require('https'); const { getAccountId } = require('../lib/config'); @@ -10,7 +10,7 @@ async function fetchPreviewInfo(accountId, contentId) { const content = await get(accountId, { uri: `${CONTENT_API_PATH}/${contentId}`, query: { portalId: accountId }, - json: true + json: true, }); return { @@ -49,9 +49,9 @@ const requestContentPreview = async (url, portalId) => { }); return response; -} +}; -const requestPage = (url, redirects = 0, originalUrl=undefined) => { +const requestPage = (url, redirects = 0, originalUrl = undefined) => { if (redirects > 5) { throw new Error( `Hit too many redirects to HEAD requests for ${originalUrl}` @@ -96,12 +96,9 @@ const requestPage = (url, redirects = 0, originalUrl=undefined) => { }) .end(); }); -} +}; -const fetchContentMetadata = async ( - url, - portalId -) => { +const fetchContentMetadata = async (url, portalId) => { let res; if (url.includes('hs_preview')) { res = await requestContentPreview(url, portalId); @@ -156,9 +153,9 @@ const fetchContentMetadata = async ( return { portalId, contentId, hublet }; } -} +}; module.exports = { fetchPreviewInfo, - fetchContentMetadata -} + fetchContentMetadata, +}; diff --git a/lib/preview.js b/lib/preview.js index eacd234..aa8f886 100644 --- a/lib/preview.js +++ b/lib/preview.js @@ -55,7 +55,7 @@ async function uploadFile(accountId, src, dest) { ); } } -}; +} async function deleteRemoteFile(accountId, remoteFilePath) { logger.debug(`Attempting to delete file "${remoteFilePath}"`); @@ -66,7 +66,7 @@ async function deleteRemoteFile(accountId, remoteFilePath) { markRemoteFsDirty(); } catch (error) { logger.error(`Deleting file ${remoteFilePath} failed`); - logger.debug(`Retrying deletion of file ${remoteFilePath}`) + logger.debug(`Retrying deletion of file ${remoteFilePath}`); try { await deleteFile(acccountId, remoteFilePath, fileMapperArgs); markRemoteFsDirty(); @@ -92,22 +92,22 @@ const getDesignManagerPath = (src, dest, file) => { const buildDeleteFileFromPreviewBufferCallback = (sessionInfo, type) => { const { accountId, src, dest, notify } = sessionInfo; - return (filePath) => { + return filePath => { if (shouldIgnoreFile(filePath)) { logger.debug(`Skipping ${filePath} due to an ignore rule`); return; } const remotePath = getDesignManagerPath(src, dest, filePath); - const deletePromise = deleteRemoteFile(accountId, remotePath) + const deletePromise = deleteRemoteFile(accountId, remotePath); triggerNotify(notify, 'Removed', filePath, deletePromise); - } -} + }; +}; const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { const { portalId, src, dest, notify } = sessionInfo; - return async (filePath) => { + return async filePath => { if (!isAllowedExtension(filePath)) { logger.debug(`Skipping ${filePath} due to unsupported extension`); return; @@ -119,16 +119,16 @@ const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { const destPath = getDesignManagerPath(src, dest, filePath); const uploadPromise = uploadFile(portalId, filePath, destPath); triggerNotify(notify, notifyMessage, filePath, uploadPromise); - } -} + }; +}; const initialPreviewBufferUpload = async (sessionInfo, filePaths) => { - const { portalId, src, dest } = sessionInfo + const { portalId, src, dest } = sessionInfo; return uploadFolder(portalId, src, dest, fileMapperArgs, {}, filePaths); -} +}; -const startPreviewWatcher = async (sessionInfo) => { +const startPreviewWatcher = async sessionInfo => { const { src } = sessionInfo; let watcherIsReady = false; @@ -137,11 +137,22 @@ const startPreviewWatcher = async (sessionInfo) => { ignored: file => shouldIgnoreFile(file), }); - - const addFileCallback = buildUploadFileToPreviewBufferCallback(sessionInfo, 'Added'); - const changeFileCallback = buildUploadFileToPreviewBufferCallback(sessionInfo, 'Change'); - const deleteFileCallback = buildDeleteFileFromPreviewBufferCallback(sessionInfo, 'file'); - const deleteFolderCallback = buildDeleteFileFromPreviewBufferCallback(sessionInfo, 'folder'); + const addFileCallback = buildUploadFileToPreviewBufferCallback( + sessionInfo, + 'Added' + ); + const changeFileCallback = buildUploadFileToPreviewBufferCallback( + sessionInfo, + 'Change' + ); + const deleteFileCallback = buildDeleteFileFromPreviewBufferCallback( + sessionInfo, + 'file' + ); + const deleteFolderCallback = buildDeleteFileFromPreviewBufferCallback( + sessionInfo, + 'folder' + ); watcher.on('ready', () => { console.log('Local file watching service has started!'); @@ -162,7 +173,7 @@ const startPreviewWatcher = async (sessionInfo) => { // do nothing... surely there's a better way to do this... } return watcher; -} +}; const createLocalHttpServer = async sessionInfo => { const expressServer = express(); @@ -170,7 +181,7 @@ const createLocalHttpServer = async sessionInfo => { expressServer.use('/', await createPreviewServerRoutes(sessionInfo)); return expressServer; -} +}; const preview = async ( accountId, @@ -198,7 +209,7 @@ const preview = async ( domains, PORT, protocol, - } + }; if (notify) { ignoreFile(notify); From 5f91643d3b15b95122de6b2483f73ff413e12f23 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Tue, 7 Nov 2023 17:13:55 -0500 Subject: [PATCH 35/56] Add check for gate --- lib/preview.js | 13 ++++++++++--- lib/preview/previewUtils.js | 19 ++++++++++++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/lib/preview.js b/lib/preview.js index aa8f886..bb6f539 100644 --- a/lib/preview.js +++ b/lib/preview.js @@ -18,7 +18,10 @@ const { convertToUnixPath, isAllowedExtension } = require('../path'); const { triggerNotify } = require('./notify'); const { getAccountConfig } = require('./config'); const { createPreviewServerRoutes } = require('./preview/createRoutes'); -const { getPortalDomains } = require('./preview/previewUtils'); +const { + getPortalDomains, + isUngatedForPreview +} = require('./preview/previewUtils'); const { markRemoteFsDirty } = require('./preview/routes/meta'); const { startShadowDevServer } = require('./preview/shadowDevServer'); const { @@ -68,7 +71,7 @@ async function deleteRemoteFile(accountId, remoteFilePath) { logger.error(`Deleting file ${remoteFilePath} failed`); logger.debug(`Retrying deletion of file ${remoteFilePath}`); try { - await deleteFile(acccountId, remoteFilePath, fileMapperArgs); + await deleteFile(accountId, remoteFilePath, fileMapperArgs); markRemoteFsDirty(); } catch (error) { logger.error(`Deleting file ${remoteFilePath} failed`); @@ -210,7 +213,11 @@ const preview = async ( PORT, protocol, }; - + const ungated = await isUngatedForPreview(sessionInfo); + if (!ungated) { + console.log(`Portal ${accountId} is missing a required gate for this feature.`); + //process.exit(); Uncomment when this check actually works + } if (notify) { ignoreFile(notify); } diff --git a/lib/preview/previewUtils.js b/lib/preview/previewUtils.js index c6a7eb5..6d54471 100644 --- a/lib/preview/previewUtils.js +++ b/lib/preview/previewUtils.js @@ -2,8 +2,10 @@ const { fetchDomains } = require('../../api/domains'); const { getAccountId, isTrackingAllowed, getAccountConfig } = require('../config'); const { platform, release } = require('os'); const { trackUsage } = require('../../api/fileMapper'); +const unAuth = require('../../api/localDevAuth/unauthenticated'); const VALID_PROXY_DOMAIN_SUFFIXES = ['localhost', 'hslocal.net']; +const HS_PREVIEW_GATE = "Cms:LocalHublPreviews"; const getPortalDomains = async (portalId) => { try { @@ -152,6 +154,20 @@ const getAuthType = (accountId) => { return authType; }; +const isUngatedForPreview = async (sessionInfo) => { + const { portalId, env, personalAccessKey } = sessionInfo; + + const { enabledFeatures = {} } = await unAuth.fetchAccessToken( + personalAccessKey, + env, + portalId + ); + + return (Object.keys(enabledFeatures).includes(HS_PREVIEW_GATE) + && enabledFeatures[HS_PREVIEW_GATE] === true) +} + + module.exports = { isInternalCMSRoute, VALID_PROXY_DOMAIN_SUFFIXES, @@ -162,5 +178,6 @@ module.exports = { silenceConsoleWhile, memoize, hidePreviewInDest, - trackPreviewEvent + trackPreviewEvent, + isUngatedForPreview, } From cd01cda5620e95e606c84d9ada567cac0f421db4 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Tue, 7 Nov 2023 17:26:43 -0500 Subject: [PATCH 36/56] External gate name casing is different --- lib/preview/previewUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/preview/previewUtils.js b/lib/preview/previewUtils.js index 6d54471..50b78d6 100644 --- a/lib/preview/previewUtils.js +++ b/lib/preview/previewUtils.js @@ -5,7 +5,7 @@ const { trackUsage } = require('../../api/fileMapper'); const unAuth = require('../../api/localDevAuth/unauthenticated'); const VALID_PROXY_DOMAIN_SUFFIXES = ['localhost', 'hslocal.net']; -const HS_PREVIEW_GATE = "Cms:LocalHublPreviews"; +const HS_PREVIEW_GATE = "cms:localHublPreviews"; const getPortalDomains = async (portalId) => { try { From ce57effcb00449dd77fc948045c5278ac211dec6 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Wed, 8 Nov 2023 13:26:43 -0500 Subject: [PATCH 37/56] Uncomment now that BE change merged --- lib/preview.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/preview.js b/lib/preview.js index bb6f539..acdfa0f 100644 --- a/lib/preview.js +++ b/lib/preview.js @@ -20,7 +20,7 @@ const { getAccountConfig } = require('./config'); const { createPreviewServerRoutes } = require('./preview/createRoutes'); const { getPortalDomains, - isUngatedForPreview + isUngatedForPreview, } = require('./preview/previewUtils'); const { markRemoteFsDirty } = require('./preview/routes/meta'); const { startShadowDevServer } = require('./preview/shadowDevServer'); @@ -215,8 +215,10 @@ const preview = async ( }; const ungated = await isUngatedForPreview(sessionInfo); if (!ungated) { - console.log(`Portal ${accountId} is missing a required gate for this feature.`); - //process.exit(); Uncomment when this check actually works + console.log( + `Portal ${accountId} is missing a required gate for this feature.` + ); + process.exit(); } if (notify) { ignoreFile(notify); From 11210ad509ea3425c65f65362ba079d02d202060 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Wed, 15 Nov 2023 17:59:01 -0500 Subject: [PATCH 38/56] remove random token gen while testing --- lib/preview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/preview.js b/lib/preview.js index aa8f886..aa68760 100644 --- a/lib/preview.js +++ b/lib/preview.js @@ -197,7 +197,7 @@ const preview = async ( const sessionInfo = { src, - dest: `@preview/${sessionToken}/${dest}`, + dest: `@preview/test/${dest}`, portalName: accountConfig.name, portalId: accountId, env: accountConfig.env, From f5c3b1366c9107f315be468db8c5815a73739f1c Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Wed, 15 Nov 2023 18:12:32 -0500 Subject: [PATCH 39/56] Static uuid --- lib/preview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/preview.js b/lib/preview.js index aa68760..ea0c606 100644 --- a/lib/preview.js +++ b/lib/preview.js @@ -197,7 +197,7 @@ const preview = async ( const sessionInfo = { src, - dest: `@preview/test/${dest}`, + dest: `@preview/96cd331a-189d-41f2-8a4c-a12485402eff/${dest}`, portalName: accountConfig.name, portalId: accountId, env: accountConfig.env, From 1515aab917ad4e18adfb0678092dc305b11ef74b Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Thu, 16 Nov 2023 12:19:54 -0500 Subject: [PATCH 40/56] Fix bad sessionToken staticing, fix .hubl.html --- lib/preview.js | 4 ++-- lib/preview/routes/index.js | 2 +- lib/preview/routes/template.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/preview.js b/lib/preview.js index ea0c606..8e42e29 100644 --- a/lib/preview.js +++ b/lib/preview.js @@ -191,13 +191,13 @@ const preview = async ( ) => { const accountConfig = getAccountConfig(accountId); const domains = await getPortalDomains(accountId); - const sessionToken = uuidv4(); + const sessionToken = '96cd331a-189d-41f2-8a4c-a12485402eff'; const PORT = port || 3000; const protocol = noSsl ? 'http' : 'https'; const sessionInfo = { src, - dest: `@preview/96cd331a-189d-41f2-8a4c-a12485402eff/${dest}`, + dest: `@preview/${sessionToken}/${dest}`, portalName: accountConfig.name, portalId: accountId, env: accountConfig.env, diff --git a/lib/preview/routes/index.js b/lib/preview/routes/index.js index 1885705..f5fab11 100644 --- a/lib/preview/routes/index.js +++ b/lib/preview/routes/index.js @@ -83,7 +83,7 @@ const getTemplatesForDisplayToUser = async (sessionInfo) => { const res = await fetchTemplatesPathStartsWith(portalId, dest); const templatePaths = res .objects - .filter(templateObj => templateObj.filename.split('.')[1] === 'html') + .filter(templateObj => templateObj.filename.endsWith('html')) .map(templateObj => templateObj.path); const filesGroupedByFolder = groupByFolder(templatePaths) const htmlToRender = renderFilesGroupedByFolder(filesGroupedByFolder, 'template') diff --git a/lib/preview/routes/template.js b/lib/preview/routes/template.js index 9910d2b..271160b 100644 --- a/lib/preview/routes/template.js +++ b/lib/preview/routes/template.js @@ -20,7 +20,7 @@ const buildTemplateRouteHandler = (sessionInfo) => { try { templateInfo = await cachedFetchTemplatesByPath(portalId, calculatedPath); } catch (err) { - console.log(`Failed to fetch template info for ${calculatedPath}`); + console.log(`Failed to fetch template info for ${calculatedPath}: ${err}`); } if (!templateInfo || !('previewKey' in templateInfo)) { res.status(502).set({ 'Content-Type': 'text/html' }).end(buildErrorIndex()); From c3c586577c795aaceb997bae8d5916f86244c232 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Wed, 3 Jan 2024 23:28:29 -0500 Subject: [PATCH 41/56] Swap index routes to new endpoints --- api/designManager.js | 12 ++++++------ lib/preview/routes/index.js | 16 ++++++++-------- lib/preview/routes/proxyPageRouteHandler.js | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/api/designManager.js b/api/designManager.js index 92904a2..68c2597 100644 --- a/api/designManager.js +++ b/api/designManager.js @@ -49,9 +49,9 @@ async function fetchModulesByPath(accountId, path) { }); } -async function fetchModulesPathStartsWith(accountId, path) { +async function fetchPreviewModules(accountId, token) { return http.get(accountId, { - uri: `${DESIGN_MANAGER_API_PATH}/modules?portalId=${accountId}&path__startswith=${path}`, + uri: `${DESIGN_MANAGER_API_PATH}/modules/local-preview?portalId=${accountId}&previewToken=${token}`, }); } @@ -61,9 +61,9 @@ async function fetchTemplatesByPath(accountId, path) { }); } -async function fetchTemplatesPathStartsWith(accountId, path) { +async function fetchPreviewTemplates(accountId, token) { return http.get(accountId, { - uri: `${DESIGN_MANAGER_API_PATH}/templates?portalId=${accountId}&path__startswith=${path}`, + uri: `${DESIGN_MANAGER_API_PATH}/templates/local-preview?portalId=${accountId}&previewToken=${token}`, }); } @@ -73,7 +73,7 @@ module.exports = { fetchRawAssetByPath, fetchThemes, fetchModulesByPath, - fetchModulesPathStartsWith, + fetchPreviewModules, fetchTemplatesByPath, - fetchTemplatesPathStartsWith, + fetchPreviewTemplates, }; diff --git a/lib/preview/routes/index.js b/lib/preview/routes/index.js index f5fab11..0e9f67a 100644 --- a/lib/preview/routes/index.js +++ b/lib/preview/routes/index.js @@ -3,8 +3,8 @@ const { trackPreviewEvent, } = require('../previewUtils'); const { - fetchTemplatesPathStartsWith, - fetchModulesPathStartsWith + fetchPreviewTemplates, + fetchPreviewModules } = require('../../../api/designManager'); const { parse: pathParse } = require('path'); @@ -35,11 +35,11 @@ const buildIndexHtml = async (sessionInfo) => {

    Modules

    - ${ modulesHtml ? modulesHtml : `

    No modules found in ${fakeDest}

    ` } + ${ modulesHtml ? modulesHtml : `

    No modules found in ${dest}

    ` }

    Templates

    - ${ templatesHtml ? templatesHtml : `

    No templates found in ${fakeDest}

    ` } + ${ templatesHtml ? templatesHtml : `

    No templates found in ${dest}

    ` }
    @@ -62,9 +62,9 @@ const listify = (objects, hrefBuilder, labelBuilder) => { } const getModulesForDisplayToUser = async (sessionInfo) => { - const { portalId, dest } = sessionInfo; + const { portalId, sessionToken } = sessionInfo; try { - const res = await fetchModulesPathStartsWith(portalId, dest); + const res = await fetchPreviewModules(portalId, sessionToken); const modulePaths = res .objects .map(moduleObj => moduleObj.path); @@ -78,9 +78,9 @@ const getModulesForDisplayToUser = async (sessionInfo) => { } const getTemplatesForDisplayToUser = async (sessionInfo) => { - const { portalId, dest } = sessionInfo; + const { portalId, sessionToken } = sessionInfo; try { - const res = await fetchTemplatesPathStartsWith(portalId, dest); + const res = await fetchPreviewTemplates(portalId, sessionToken); const templatePaths = res .objects .filter(templateObj => templateObj.filename.endsWith('html')) diff --git a/lib/preview/routes/proxyPageRouteHandler.js b/lib/preview/routes/proxyPageRouteHandler.js index e438c97..df755bf 100644 --- a/lib/preview/routes/proxyPageRouteHandler.js +++ b/lib/preview/routes/proxyPageRouteHandler.js @@ -41,7 +41,7 @@ const buildProxyPageRouteHandler = (sessionInfo) => { portalId, hublet, contentId, - urlToProxy, + `${urlToProxy}?localPreviewToken=${sessionInfo.sessionToken}&hsCacheBuster=${Date.now()}`, ); } catch (e) { next(e); From 94e6337f5824dd226e00b5b50262a3b043f4458b Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Mon, 8 Jan 2024 00:30:09 -0500 Subject: [PATCH 42/56] Move to cos-rend endpoint --- api/preview.js | 120 +++++------------- lib/preview/proxyPage.js | 75 +---------- lib/preview/routes/proxyPageRouteHandler.js | 15 +-- .../routes/proxyPathPageRouteHandler.js.js | 12 +- 4 files changed, 45 insertions(+), 177 deletions(-) diff --git a/api/preview.js b/api/preview.js index f73f279..c5bf51b 100644 --- a/api/preview.js +++ b/api/preview.js @@ -1,28 +1,13 @@ const { get } = require('../http'); const http = require('http'); const https = require('https'); -const { getAccountId } = require('../lib/config'); const CONTENT_API_PATH = `/content/api/v4/contents`; +const COS_RENDER_PATH = `/cos-rendering/v1/public`; -async function fetchPreviewInfo(accountId, contentId) { - try { - const content = await get(accountId, { - uri: `${CONTENT_API_PATH}/${contentId}`, - query: { portalId: accountId }, - json: true, - }); +async function fetchPreviewRender(url, sessionInfo) { + const { portalId, env, sessionToken } = sessionInfo; - return { - previewDomain: content.previewDomain ?? content.resolvedDomain, - previewKey: content.previewKey, - }; - } catch (err) { - throw err; - } -} - -const requestContentPreview = async (url, portalId) => { const urlObject = new URL(url); const proxiedQueryParams = {}; @@ -38,18 +23,38 @@ const requestContentPreview = async (url, portalId) => { } } - const accountId = getAccountId(portalId); - - const response = await get(accountId, { - baseUrl: urlObject.origin, - uri: urlObject.pathname, - query: proxiedQueryParams, - json: false, - resolveWithFullResponse: true, + return get(portalId, { + baseUrl: `http://api.hubspot${env === 'qa' ? 'qa' : ''}.com`, + uri: COS_RENDER_PATH, + query: { + ...proxiedQueryParams, + portalId, + localSessionToken: sessionToken, + hsCacheBuster: Date.now(), + hsDebugOverridePublicHost: urlObject.hostname, + }, + headers: { + Accept: "text/html" + }, }); +} - return response; -}; +async function fetchPreviewInfo(accountId, contentId) { + try { + const content = await get(accountId, { + uri: `${CONTENT_API_PATH}/${contentId}`, + query: { portalId: accountId }, + json: true, + }); + + return { + previewDomain: content.previewDomain ?? content.resolvedDomain, + previewKey: content.previewKey, + }; + } catch (err) { + throw err; + } +} const requestPage = (url, redirects = 0, originalUrl = undefined) => { if (redirects > 5) { @@ -98,64 +103,7 @@ const requestPage = (url, redirects = 0, originalUrl = undefined) => { }); }; -const fetchContentMetadata = async (url, portalId) => { - let res; - if (url.includes('hs_preview')) { - res = await requestContentPreview(url, portalId); - } else { - res = await requestPage(url); - } - - const portalIdHeader = res.headers['x-hs-hub-id']; - const contentId = res.headers['x-hs-content-id']; - - if (Array.isArray(portalIdHeader) || Array.isArray(contentId)) { - throw new Error( - `There were multiple hub ID or content ID headers in the HEAD request to ${url}` - ); - } else { - const portalIdFromResponse = parseInt(portalIdHeader, 10); - const pageAccount = await getAccountId(portalIdFromResponse); - - if (!pageAccount) { - throw new Error( - `No CLI auth for portal ${portalIdFromResponse} found, please run \`hs auth\`` - ); - } - - if (res.statusCode !== 200) { - throw new Error( - `${res.statusCode} ${ - res.statusMessage - } - Unable to obtain HEAD information from ${url}. ${ - res.statusCode === 429 && res.headers['retry-after'] - ? `Retry after ${res.headers['retry-after']} seconds.` - : '' - }` - ); - } else if (!portalId || !contentId) { - throw new Error( - `Missing hub ID or content ID headers on HEAD request to ${url}` - ); - } else if (isNaN(portalId)) { - throw new Error( - `Hub ID from the HEAD request is not a number: '${portalId}' (${url})` - ); - } - - // TODO, the hubs-hublets/hublets/find/ API is internal auth only.🤔... - // Prior implementation of fetchHubletForPortalId that would work execept for an internal auth issue: - // https://git.hubteam.com/HubSpot/cms-js-platform/blob/ef6e8029df6c09fe30d65a80073606f41ca324a6/cms-dev-server/src/proxyPage/fetchHubletForPortalId.ts - // - // const hublet = await fetchHubletForPortalId(portalId); - - const hublet = 'na1'; - - return { portalId, contentId, hublet }; - } -}; - module.exports = { + fetchPreviewRender, fetchPreviewInfo, - fetchContentMetadata, }; diff --git a/lib/preview/proxyPage.js b/lib/preview/proxyPage.js index 05cf054..492655e 100644 --- a/lib/preview/proxyPage.js +++ b/lib/preview/proxyPage.js @@ -1,89 +1,28 @@ -const http = require('../../http'); -const { fetchPreviewInfo } = require('../../api/preview'); -const { addRefreshScript, memoize } = require('./previewUtils'); - -const cachedFetchPreviewInfo = memoize(fetchPreviewInfo); +const { addRefreshScript } = require('./previewUtils'); +const { fetchPreviewRender } = require('../../api/preview'); const proxyPage = async ( res, - portalId, - hublet, - contentId, urlToProxy, + sessionInfo ) => { try { - const previewInfo = await cachedFetchPreviewInfo(portalId, contentId); - - const { previewKey } = previewInfo; - - const pageHtml = await makeProxyPreviewRequest( - portalId, - urlToProxy, - previewKey, - contentId - ) + const pageHtml = await fetchPreviewRender(urlToProxy, sessionInfo); const embeddedHtml = addRefreshScript(pageHtml); res.status(200).set({ 'Content-Type': 'text/html' }).end(embeddedHtml); } catch (error) { + const { portalId } = sessionInfo; // TODO change error.stack to error.message before we publish - response + res .status(500) .end( - `Failed proxy render of page id = ${contentId} hub id = ${portalId}\n\n${error.stack}` + `Failed proxy render of page ${urlToProxy} hub id = ${portalId}\n\n${error.stack}` ); return; } } -const makeProxyPreviewRequest = async ( - portalId, - urlToProxy, - previewKey, - contentId -) => { - const url = new URL(urlToProxy); - - let proxiedQueryParams = {}; - - // Convert searchParams to object param format hsGet/request expects - for (const queryKey of url.searchParams.keys()) { - const values = url.searchParams.getAll(queryKey); - - if (values.length > 1) { - proxiedQueryParams[queryKey] = values; - } else { - proxiedQueryParams[queryKey] = values[0]; - } - } - - proxiedQueryParams = { - ...proxiedQueryParams, - hs_preview: `${previewKey}-${contentId}`, - }; - - const result = http.get(portalId, { - baseUrl: url.origin, - uri: url.pathname, - query: proxiedQueryParams, - json: false, - useQuerystring: true, - }); - - return result.catch(error => { - if ( - error.statusCode >= 400 && - error.statusCode < 500 && - !!error.response.body - ) { - return error.response.body; - } else { - throw error; - } - }); - -} - module.exports = { proxyPage } diff --git a/lib/preview/routes/proxyPageRouteHandler.js b/lib/preview/routes/proxyPageRouteHandler.js index df755bf..7873645 100644 --- a/lib/preview/routes/proxyPageRouteHandler.js +++ b/lib/preview/routes/proxyPageRouteHandler.js @@ -1,14 +1,10 @@ -const { fetchContentMetadata } = require('../../../api/preview'); const { proxyPage } = require('../proxyPage.js'); const { getSubDomainFromValidLocalDomain, VALID_PROXY_DOMAIN_SUFFIXES, - memoize, trackPreviewEvent } = require('../previewUtils.js'); -const cachedFetchContentMetadata = memoize(fetchContentMetadata); - const buildProxyPageRouteHandler = (sessionInfo) => { return async (request, response, next) => { if (!request.accepts().includes('text/html')) { @@ -31,17 +27,10 @@ const buildProxyPageRouteHandler = (sessionInfo) => { const urlToProxy = `http://${domainToProxy}${request.originalUrl}`; try { - const { portalId, contentId, hublet } = await cachedFetchContentMetadata( - urlToProxy, - sessionInfo.portalId - ); - await proxyPage( response, - portalId, - hublet, - contentId, - `${urlToProxy}?localPreviewToken=${sessionInfo.sessionToken}&hsCacheBuster=${Date.now()}`, + urlToProxy, + sessionInfo, ); } catch (e) { next(e); diff --git a/lib/preview/routes/proxyPathPageRouteHandler.js.js b/lib/preview/routes/proxyPathPageRouteHandler.js.js index ea131ea..38149f7 100644 --- a/lib/preview/routes/proxyPathPageRouteHandler.js.js +++ b/lib/preview/routes/proxyPathPageRouteHandler.js.js @@ -1,4 +1,3 @@ -const { fetchContentMetadata } = require('../../../api/preview'); const { proxyPage } = require('../proxyPage'); const { trackPreviewEvent } = require('../previewUtils'); @@ -28,17 +27,10 @@ const buildProxyRouteHandler = (sessionInfo) => { } trackPreviewEvent('proxy-path-route'); try { - const { portalId, contentId, hublet } = await fetchContentMetadata( - proxyPageUrl.href, - sessionInfo.portalId - ) - await proxyPage( res, - portalId, - hublet, - contentId, - proxyPageUrl.href + proxyPageUrl.href, + sessionInfo ); } catch (e) { const message = `Failed to fetch proxy page for ${proxyPageUrl.href}`; From 3a44f9c475a26aa79881e2f53db3550e90f0a433 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Mon, 8 Jan 2024 00:33:31 -0500 Subject: [PATCH 43/56] Axe requestpage --- api/preview.js | 49 ------------------------------------------------- 1 file changed, 49 deletions(-) diff --git a/api/preview.js b/api/preview.js index c5bf51b..19d745a 100644 --- a/api/preview.js +++ b/api/preview.js @@ -1,6 +1,4 @@ const { get } = require('../http'); -const http = require('http'); -const https = require('https'); const CONTENT_API_PATH = `/content/api/v4/contents`; const COS_RENDER_PATH = `/cos-rendering/v1/public`; @@ -56,53 +54,6 @@ async function fetchPreviewInfo(accountId, contentId) { } } -const requestPage = (url, redirects = 0, originalUrl = undefined) => { - if (redirects > 5) { - throw new Error( - `Hit too many redirects to HEAD requests for ${originalUrl}` - ); - } - - return new Promise((resolve, reject) => { - const protocolAPI = url.startsWith('https://') ? https : http; - protocolAPI - .request( - url, - { - method: 'HEAD', - headers: { - // Keep this user agent, so we don't get 403s from the request - ['User-Agent']: 'cms-dev-server', - }, - }, - async res => { - // Use a lib to better follow various redirects? - if ( - res.statusCode >= 300 && - res.statusCode < 400 && - res.headers.location - ) { - console.log(`Redirecting to ${res.headers.location} (from ${url})`); - return resolve( - requestPage( - res.headers.location, - redirects + 1, - originalUrl ?? url - ) - ); - } - - return resolve(res); - } - ) - .on('error', err => { - console.error(err); - reject(err); - }) - .end(); - }); -}; - module.exports = { fetchPreviewRender, fetchPreviewInfo, From b32b15d7f4946ca740651c3cf1f67f50a0266c3b Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Mon, 8 Jan 2024 00:43:12 -0500 Subject: [PATCH 44/56] - --- api/preview.js | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/api/preview.js b/api/preview.js index 19d745a..4fe9ed0 100644 --- a/api/preview.js +++ b/api/preview.js @@ -1,6 +1,5 @@ const { get } = require('../http'); -const CONTENT_API_PATH = `/content/api/v4/contents`; const COS_RENDER_PATH = `/cos-rendering/v1/public`; async function fetchPreviewRender(url, sessionInfo) { @@ -37,24 +36,6 @@ async function fetchPreviewRender(url, sessionInfo) { }); } -async function fetchPreviewInfo(accountId, contentId) { - try { - const content = await get(accountId, { - uri: `${CONTENT_API_PATH}/${contentId}`, - query: { portalId: accountId }, - json: true, - }); - - return { - previewDomain: content.previewDomain ?? content.resolvedDomain, - previewKey: content.previewKey, - }; - } catch (err) { - throw err; - } -} - module.exports = { fetchPreviewRender, - fetchPreviewInfo, }; From 33c1f3031e76ffdc2ed1c46879811a4159eae16f Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Mon, 8 Jan 2024 11:08:22 -0500 Subject: [PATCH 45/56] Fix paths --- api/preview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/preview.js b/api/preview.js index 4fe9ed0..6e163db 100644 --- a/api/preview.js +++ b/api/preview.js @@ -22,7 +22,7 @@ async function fetchPreviewRender(url, sessionInfo) { return get(portalId, { baseUrl: `http://api.hubspot${env === 'qa' ? 'qa' : ''}.com`, - uri: COS_RENDER_PATH, + uri: `${COS_RENDER_PATH}${urlObject.pathname}`, query: { ...proxiedQueryParams, portalId, From da5b43a34ad9d0ccc891d84f3a13c122ff87768c Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Tue, 23 Jan 2024 13:54:34 -0500 Subject: [PATCH 46/56] Swap render fetch --- api/preview.js | 35 ++++------------------------------- lib/preview/proxyPage.js | 1 - 2 files changed, 4 insertions(+), 32 deletions(-) diff --git a/api/preview.js b/api/preview.js index 6e163db..915a705 100644 --- a/api/preview.js +++ b/api/preview.js @@ -1,39 +1,12 @@ -const { get } = require('../http'); - -const COS_RENDER_PATH = `/cos-rendering/v1/public`; - async function fetchPreviewRender(url, sessionInfo) { - const { portalId, env, sessionToken } = sessionInfo; + const { sessionToken } = sessionInfo; const urlObject = new URL(url); - const proxiedQueryParams = {}; - - // Convert searchParams to object param format hsGet/request expects - for (const queryKey of urlObject.searchParams.keys()) { - const values = urlObject.searchParams.getAll(queryKey); - - if (values.length > 1) { - proxiedQueryParams[queryKey] = values; - } else { - proxiedQueryParams[queryKey] = values[0]; - } - } + urlObject.searchParams.append('localPreviewToken', sessionToken); + urlObject.searchParams.append('hsCacheBuster', Date.now()); - return get(portalId, { - baseUrl: `http://api.hubspot${env === 'qa' ? 'qa' : ''}.com`, - uri: `${COS_RENDER_PATH}${urlObject.pathname}`, - query: { - ...proxiedQueryParams, - portalId, - localSessionToken: sessionToken, - hsCacheBuster: Date.now(), - hsDebugOverridePublicHost: urlObject.hostname, - }, - headers: { - Accept: "text/html" - }, - }); + return fetch(urlObject.href).then(res => res.text()); } module.exports = { diff --git a/lib/preview/proxyPage.js b/lib/preview/proxyPage.js index 492655e..6243b21 100644 --- a/lib/preview/proxyPage.js +++ b/lib/preview/proxyPage.js @@ -8,7 +8,6 @@ const proxyPage = async ( ) => { try { const pageHtml = await fetchPreviewRender(urlToProxy, sessionInfo); - const embeddedHtml = addRefreshScript(pageHtml); res.status(200).set({ 'Content-Type': 'text/html' }).end(embeddedHtml); } catch (error) { From 404e06a40e0383cf3412172f96c7d2348746341e Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Tue, 23 Jan 2024 13:56:39 -0500 Subject: [PATCH 47/56] Proxy page --- lib/preview/proxyPage.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/preview/proxyPage.js b/lib/preview/proxyPage.js index 6243b21..492655e 100644 --- a/lib/preview/proxyPage.js +++ b/lib/preview/proxyPage.js @@ -8,6 +8,7 @@ const proxyPage = async ( ) => { try { const pageHtml = await fetchPreviewRender(urlToProxy, sessionInfo); + const embeddedHtml = addRefreshScript(pageHtml); res.status(200).set({ 'Content-Type': 'text/html' }).end(embeddedHtml); } catch (error) { From 690484a34e9769c23e829f3c3eca982dea34a388 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Mon, 5 Feb 2024 17:58:02 -0500 Subject: [PATCH 48/56] Changes from DPG review --- api/domains.js | 14 ++- api/preview.js | 4 +- lib/config.js | 15 ++++ lib/preview.js | 22 ++--- lib/preview/createRoutes.js | 8 +- lib/preview/httpsRedirectingServer.js | 9 +- lib/preview/previewUtils.js | 64 ++++++-------- lib/preview/proxyPage.js | 5 +- lib/preview/routes/index.js | 31 +++---- lib/preview/routes/module.js | 50 ++++------- .../routes/proxyPageResourceRedirect.js | 2 +- .../routes/proxyPathPageResourceRedirect.js | 7 +- .../routes/proxyPathPageRouteHandler.js.js | 54 +++++------- lib/preview/routes/template.js | 49 ++++------- lib/preview/shadowDevServer.js | 86 ------------------- lib/preview/sprocketMenuServer.js | 84 ++++++++++++++++++ package.json | 1 - personalAccessKey.js | 32 +++++-- 18 files changed, 259 insertions(+), 278 deletions(-) delete mode 100644 lib/preview/shadowDevServer.js create mode 100644 lib/preview/sprocketMenuServer.js diff --git a/api/domains.js b/api/domains.js index fd1f97a..18d7675 100644 --- a/api/domains.js +++ b/api/domains.js @@ -3,16 +3,12 @@ const http = require('../http'); const DOMAINS_API_PATH = `/cms/v3/domains`; async function fetchDomains(accountId) { - try { - const result = await http.get(accountId, { - uri: DOMAINS_API_PATH, - json: true, - }); + const result = await http.get(accountId, { + uri: DOMAINS_API_PATH, + json: true, + }); - return result.results; - } catch (err) { - throw err; - } + return result.results; } module.exports = { diff --git a/api/preview.js b/api/preview.js index 915a705..c428608 100644 --- a/api/preview.js +++ b/api/preview.js @@ -1,3 +1,5 @@ +const { request } = require('../http'); + async function fetchPreviewRender(url, sessionInfo) { const { sessionToken } = sessionInfo; @@ -6,7 +8,7 @@ async function fetchPreviewRender(url, sessionInfo) { urlObject.searchParams.append('localPreviewToken', sessionToken); urlObject.searchParams.append('hsCacheBuster', Date.now()); - return fetch(urlObject.href).then(res => res.text()); + return request(urlObject.href).then(res => res.text()); } module.exports = { diff --git a/lib/config.js b/lib/config.js index 453127a..852ef62 100644 --- a/lib/config.js +++ b/lib/config.js @@ -763,7 +763,22 @@ const isConfigFlagEnabled = flag => { return config[flag] || false; }; + +const getAuthType = accountId => { + let authType = 'unknown'; + + if (accountId) { + const accountConfig = getAccountConfig(accountId); + if (accountConfig && accountConfig.authType) { + authType = accountConfig.authType; + } + } + + return authType; +}; + module.exports = { + getAuthType, getAndLoadConfigIfNeeded, getEnv, getConfig, diff --git a/lib/preview.js b/lib/preview.js index 3017326..f0c572a 100644 --- a/lib/preview.js +++ b/lib/preview.js @@ -23,7 +23,7 @@ const { isUngatedForPreview, } = require('./preview/previewUtils'); const { markRemoteFsDirty } = require('./preview/routes/meta'); -const { startShadowDevServer } = require('./preview/shadowDevServer'); +const { startSprocketMenuServer } = require('./preview/sprocketMenuServer'); const { createHttpsRedirectingServer, } = require('./preview/httpsRedirectingServer'); @@ -108,7 +108,7 @@ const buildDeleteFileFromPreviewBufferCallback = (sessionInfo, type) => { }; const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { - const { portalId, src, dest, notify } = sessionInfo; + const { accountId, src, dest, notify } = sessionInfo; return async filePath => { if (!isAllowedExtension(filePath)) { @@ -120,15 +120,15 @@ const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { return; } const destPath = getDesignManagerPath(src, dest, filePath); - const uploadPromise = uploadFile(portalId, filePath, destPath); + const uploadPromise = uploadFile(accountId, filePath, destPath); triggerNotify(notify, notifyMessage, filePath, uploadPromise); }; }; const initialPreviewBufferUpload = async (sessionInfo, filePaths) => { - const { portalId, src, dest } = sessionInfo; + const { accountId, src, dest } = sessionInfo; - return uploadFolder(portalId, src, dest, fileMapperArgs, {}, filePaths); + return uploadFolder(accountId, src, dest, fileMapperArgs, {}, filePaths); }; const startPreviewWatcher = async sessionInfo => { @@ -158,7 +158,7 @@ const startPreviewWatcher = async sessionInfo => { ); watcher.on('ready', () => { - console.log('Local file watching service has started!'); + logger.log('Local file watching service has started!'); watcherIsReady = true; }); watcher.on('add', addFileCallback); @@ -194,7 +194,7 @@ const preview = async ( ) => { const accountConfig = getAccountConfig(accountId); const domains = await getPortalDomains(accountId); - const sessionToken = '96cd331a-189d-41f2-8a4c-a12485402eff'; + const sessionToken = '7c964654-7676-4181-93b2-daae33e0b2d5';//uuidv4(); const PORT = port || 3000; const protocol = noSsl ? 'http' : 'https'; @@ -202,7 +202,7 @@ const preview = async ( src, dest: `@preview/${sessionToken}/${dest}`, portalName: accountConfig.name, - portalId: accountId, + accountId, env: accountConfig.env, personalAccessKey: accountConfig.personalAccessKey, // we find hublet later in the content metadata fetch @@ -215,7 +215,7 @@ const preview = async ( }; const ungated = await isUngatedForPreview(sessionInfo); if (!ungated) { - console.log( + logger.log( `Portal ${accountId} is missing a required gate for this feature.` ); process.exit(); @@ -241,8 +241,8 @@ const preview = async ( const httpServer = http.createServer(expressServer); httpServer.listen(PORT); } - startShadowDevServer(sessionInfo); - console.log( + startSprocketMenuServer(sessionInfo); + logger.log( `HubSpot preview local dev server hosting at ${protocol}://hslocal.net:${PORT}, portalId=${accountId}` ); }; diff --git a/lib/preview/createRoutes.js b/lib/preview/createRoutes.js index f13b379..ba743c7 100644 --- a/lib/preview/createRoutes.js +++ b/lib/preview/createRoutes.js @@ -1,5 +1,6 @@ const { Router } = require('express'); -const cors = require('cors'); +const { logger } = require('./../../logger'); + const { buildIndexRouteHandler } = require('./routes/index.js'); const { buildModuleRouteHandler } = require('./routes/module.js'); const { buildTemplateRouteHandler } = require('./routes/template.js'); @@ -15,8 +16,9 @@ const createPreviewServerRoutes = async (sessionInfo) => { previewServerRouter.get('/proxy', buildProxyRouteHandler(sessionInfo)); previewServerRouter.get('/module/:modulePath(*)', buildModuleRouteHandler(sessionInfo)); previewServerRouter.get('/template/:templatePath(*)', buildTemplateRouteHandler(sessionInfo)); + // fetches server metadata from the client (used by refresh script to check if fs has been changed) previewServerRouter.get('/meta', buildMetaRouteHandler(sessionInfo)); - + // handles resources on the proxied page, so a fetch from relative path gets proxied too previewServerRouter.get('/*', proxyPathPageResourceRedirect) previewServerRouter.get('/*', proxyPageResourceRedirect); previewServerRouter.post('/*', proxyPageResourceRedirect); @@ -25,7 +27,7 @@ const createPreviewServerRoutes = async (sessionInfo) => { previewServerRouter.put('/*', proxyPageResourceRedirect); previewServerRouter.options('/*', proxyPageResourceRedirect); previewServerRouter.get('/*', buildProxyPageRouteHandler(sessionInfo)); - + // index route previewServerRouter.get('/', buildIndexRouteHandler(sessionInfo)); return previewServerRouter; diff --git a/lib/preview/httpsRedirectingServer.js b/lib/preview/httpsRedirectingServer.js index ed21398..604530c 100644 --- a/lib/preview/httpsRedirectingServer.js +++ b/lib/preview/httpsRedirectingServer.js @@ -1,6 +1,7 @@ const http = require('http'); const https = require('https'); const net = require('net'); +const os = require('os') const { unlinkSync } = require('fs'); const { silenceConsoleWhile } = require('./previewUtils'); @@ -14,11 +15,11 @@ const createCert = async (domainsToProxy) => { const hosts = ['localhost', 'hslocal.net', ...additionalMkcertHosts]; const { createCertificate } = await import('mkcert-cli'); const { key, cert } = await silenceConsoleWhile(createCertificate, { - keyFilePath: `${__dirname}/key.pem`, - certFilePath: `${__dirname}/cert.pem` + keyFilePath: `${os.tmpdir()}/hstmp/hsLocalSshKey.pem`, + certFilePath: `${os.tmpdir()}/hstmp/hsLocalSshCert.pem` }, hosts); - unlinkSync(`${__dirname}/key.pem`); - unlinkSync(`${__dirname}/cert.pem`); + unlinkSync(`${os.tmpdir()}/hstmp/hsLocalSshKey.pem`); + unlinkSync(`${os.tmpdir()}/hstmp/hsLocalSshCert.pem`); return { key, cert }; } diff --git a/lib/preview/previewUtils.js b/lib/preview/previewUtils.js index 50b78d6..fc968c1 100644 --- a/lib/preview/previewUtils.js +++ b/lib/preview/previewUtils.js @@ -2,34 +2,29 @@ const { fetchDomains } = require('../../api/domains'); const { getAccountId, isTrackingAllowed, getAccountConfig } = require('../config'); const { platform, release } = require('os'); const { trackUsage } = require('../../api/fileMapper'); -const unAuth = require('../../api/localDevAuth/unauthenticated'); - +const { enabledFeaturesForPersonalAccessKey } = require('../../personalAccessKey'); +const { stringify } = require('querystring'); +const { logger } = require('./../../logger'); +const { getAuthType } = require('./../../lib/config'); const VALID_PROXY_DOMAIN_SUFFIXES = ['localhost', 'hslocal.net']; const HS_PREVIEW_GATE = "cms:localHublPreviews"; -const getPortalDomains = async (portalId) => { +const getPortalDomains = async (accountId) => { try { - const result = await fetchDomains(portalId); + const result = await fetchDomains(accountId); return result; } catch (error) { - console.log("There was a problem fetching domains for your portal. You may be missing a scope necessary for this feature.") + logger.error("There was a problem fetching domains for your portal. You may be missing a scope necessary for this feature.") return []; } } const getPreviewUrl = (sessionInfo, queryParams) => { - const { portalId, env, hublet } = sessionInfo; + const { accountId, env, hublet } = sessionInfo; - return `http://${portalId}.hubspotpreview${ + return `http://${accountId}.hubspotpreview${ env === 'qa' ? 'qa' : '' - }-${hublet}.com/_hcms/preview/template/multi?${stringifyQuery(queryParams)}`; -} - -const stringifyQuery = (query) => { - return Object.keys(query) - .sort() - .map(key => `${key}=${query[key]}`) - .join('&'); + }-${hublet}.com/_hcms/preview/template/multi?${stringify(queryParams)}`; } const insertAtEndOfBody = (html, script) => { @@ -78,10 +73,8 @@ const getSubDomainFromValidLocalDomain = hostname => { return hostname.slice(0, -1 * validProxyDomainSuffix.length - 1); } } - return undefined; }; -// From(ish) https://git.hubteam.com/HubSpot/cloudflare-workers/blob/master/worker-lib/src/Constants.ts const internalRoutes = { HCMS: '/_hcms/', HS_FS: '/hs-fs/', @@ -93,7 +86,7 @@ const isInternalCMSRoute = (req) => const silenceConsoleWhile = async (act, ...args) => { const tmpConsole = console; - console = { log: () => {} } + console = { log: () => {} } // ! const result = await act(...args); console = tmpConsole; return result; @@ -135,38 +128,30 @@ const trackPreviewEvent = async (action) => { accountId ).catch( (err) => { - console.error(`trackUsage failed: ${JSON.stringify(err, null, 2)}`); + logger.error(`trackUsage failed: ${JSON.stringify(err, null, 2)}`); } ); } -const getAuthType = (accountId) => { - let authType = 'unknown'; - - if (accountId) { - const accountConfig = getAccountConfig(accountId); - authType = - accountConfig && accountConfig.authType - ? accountConfig.authType - : 'apikey'; - } - - return authType; -}; - const isUngatedForPreview = async (sessionInfo) => { - const { portalId, env, personalAccessKey } = sessionInfo; + const { accountId } = sessionInfo; - const { enabledFeatures = {} } = await unAuth.fetchAccessToken( - personalAccessKey, - env, - portalId - ); + const enabledFeatures = await enabledFeaturesForPersonalAccessKey(accountId); return (Object.keys(enabledFeatures).includes(HS_PREVIEW_GATE) && enabledFeatures[HS_PREVIEW_GATE] === true) } +const buildHTMLResponse = (content) => { + return ` + + + + + ${content} + + `; + } module.exports = { isInternalCMSRoute, @@ -180,4 +165,5 @@ module.exports = { hidePreviewInDest, trackPreviewEvent, isUngatedForPreview, + buildHTMLResponse, } diff --git a/lib/preview/proxyPage.js b/lib/preview/proxyPage.js index 492655e..d2c6c28 100644 --- a/lib/preview/proxyPage.js +++ b/lib/preview/proxyPage.js @@ -12,12 +12,11 @@ const proxyPage = async ( const embeddedHtml = addRefreshScript(pageHtml); res.status(200).set({ 'Content-Type': 'text/html' }).end(embeddedHtml); } catch (error) { - const { portalId } = sessionInfo; - // TODO change error.stack to error.message before we publish + const { accountId } = sessionInfo; res .status(500) .end( - `Failed proxy render of page ${urlToProxy} hub id = ${portalId}\n\n${error.stack}` + `Failed proxy render of page ${urlToProxy} hub id = ${accountId}\n\n${error.message}` ); return; } diff --git a/lib/preview/routes/index.js b/lib/preview/routes/index.js index 0e9f67a..34b32a0 100644 --- a/lib/preview/routes/index.js +++ b/lib/preview/routes/index.js @@ -7,6 +7,8 @@ const { fetchPreviewModules } = require('../../../api/designManager'); const { parse: pathParse } = require('path'); +const { logger } = require('./../../../logger'); +const { isCodedFile } = require('./../../../templates'); const buildIndexRouteHandler = (sessionInfo) => { return async (req, res) => { @@ -62,9 +64,9 @@ const listify = (objects, hrefBuilder, labelBuilder) => { } const getModulesForDisplayToUser = async (sessionInfo) => { - const { portalId, sessionToken } = sessionInfo; + const { accountId, sessionToken } = sessionInfo; try { - const res = await fetchPreviewModules(portalId, sessionToken); + const res = await fetchPreviewModules(accountId, sessionToken); const modulePaths = res .objects .map(moduleObj => moduleObj.path); @@ -72,25 +74,24 @@ const getModulesForDisplayToUser = async (sessionInfo) => { const htmlToRender = renderFilesGroupedByFolder(filesGroupedByFolder, 'module') return htmlToRender; } catch (err) { - console.log(`Failed to fetch modules for index page: ${err}`); + logger.error(`Failed to fetch modules for index page: ${err}`); return undefined; } } const getTemplatesForDisplayToUser = async (sessionInfo) => { - const { portalId, sessionToken } = sessionInfo; + const { accountId, sessionToken } = sessionInfo; try { - const res = await fetchPreviewTemplates(portalId, sessionToken); + const res = await fetchPreviewTemplates(accountId, sessionToken); const templatePaths = res .objects - .filter(templateObj => templateObj.filename.endsWith('html')) + .filter(templateObj => isCodedFile(templateObj.filename)) .map(templateObj => templateObj.path); const filesGroupedByFolder = groupByFolder(templatePaths) const htmlToRender = renderFilesGroupedByFolder(filesGroupedByFolder, 'template') return htmlToRender; } catch (err) { - console.log(`Failed to fetch templates for index page: ${err}`); - return undefined; + logger.error(`Failed to fetch templates for index page: ${err}`); } } @@ -110,15 +111,15 @@ const renderFilesGroupedByFolder = (filesGroupedByFolder, endpoint) => { const folders = Object.keys(filesGroupedByFolder); return folders.reduce((outer_acc, folder) => { const fakeDest = hidePreviewInDest(folder); + console.log(filesGroupedByFolder[folder]); const folderDisplay = '' + fakeDest - + '
      ' - + filesGroupedByFolder[folder].reduce((acc, cur) => { - const newListItem = `
    • ${cur}
    • ` - acc += newListItem; - return acc; - }, '') - + '
    '; + + '' + + listify( + filesGroupedByFolder[folder], + x => `/${endpoint}/${fakeDest}/${x}`, + x => x + ); outer_acc += folderDisplay; return outer_acc; }, ''); diff --git a/lib/preview/routes/module.js b/lib/preview/routes/module.js index 91c580c..1386894 100644 --- a/lib/preview/routes/module.js +++ b/lib/preview/routes/module.js @@ -4,13 +4,15 @@ const { addRefreshScript, getPreviewUrl, memoize, - trackPreviewEvent + trackPreviewEvent, + buildHTMLResponse } = require('../previewUtils'); +const { logger } = require('./../../../logger'); const cachedFetchModulesByPath = memoize(fetchModulesByPath); const buildModuleRouteHandler = (sessionInfo) => { - const { portalId, sessionToken } = sessionInfo; + const { accountId, sessionToken } = sessionInfo; return async (req, res) => { trackPreviewEvent('view-module-route'); @@ -25,9 +27,9 @@ const buildModuleRouteHandler = (sessionInfo) => { const calculatedPath = `@preview/${sessionToken}/${modulePath}.module`; let customWidgetInfo; try { - customWidgetInfo = await cachedFetchModulesByPath(portalId, calculatedPath); + customWidgetInfo = await cachedFetchModulesByPath(accountId, calculatedPath); } catch (err) { - console.log(`Failed to fetch module preview for ${calculatedPath}`) + logger.error(`Failed to fetch module preview for ${calculatedPath}`) } if (!customWidgetInfo || !('moduleId' in customWidgetInfo && 'previewKey' in customWidgetInfo)) { res.status(200).set({ 'Content-Type': 'text/html' }).end(buildErrorIndex()); @@ -40,7 +42,7 @@ const buildModuleRouteHandler = (sessionInfo) => { ...req.query } const previewUrl = new URL(getPreviewUrl(sessionInfo, params)) ; - const result = await http.get(portalId, { + const result = await http.get(accountId, { baseUrl: previewUrl.origin, uri: previewUrl.pathname, query: params, @@ -52,33 +54,19 @@ const buildModuleRouteHandler = (sessionInfo) => { } } -const buildErrorIndex = () => { - return ` - - - - -
    -

    Error

    -

    Failed to fetch module data.

    -
    - - `; -} +const buildErrorIndex = () => buildHTMLResponse(` +
    +

    Error

    +

    Failed to fetch module data.

    +
    +`); -const buildModuleIndex = () => { - return ` - - - - -
    -

    Module Index

    -

    Incomplete!

    -
    - - `; -} +const buildModuleIndex = () => buildHTMLResponse(` +
    +

    Module Index

    +

    Incomplete!

    +
    +`); module.exports = { buildModuleRouteHandler diff --git a/lib/preview/routes/proxyPageResourceRedirect.js b/lib/preview/routes/proxyPageResourceRedirect.js index e345e97..143651d 100644 --- a/lib/preview/routes/proxyPageResourceRedirect.js +++ b/lib/preview/routes/proxyPageResourceRedirect.js @@ -35,7 +35,7 @@ const proxyPageResourceRedirect = (req, res, next) => { !header.startsWith('sec-') ); - let body = undefined; + let body; if (req.body) { if (isSendingJSON) { diff --git a/lib/preview/routes/proxyPathPageResourceRedirect.js b/lib/preview/routes/proxyPathPageResourceRedirect.js index 0fdb560..bfd5a0e 100644 --- a/lib/preview/routes/proxyPathPageResourceRedirect.js +++ b/lib/preview/routes/proxyPathPageResourceRedirect.js @@ -1,4 +1,3 @@ -const fetch = require('node-fetch-commonjs'); const { isInternalCMSRoute } = require('../previewUtils'); const proxyPathPageResourceRedirect = (req, res, next) => { @@ -10,7 +9,7 @@ const proxyPathPageResourceRedirect = (req, res, next) => { try { refererUrl = new URL(req.headers.referer); proxyPageUrl = new URL( - new URL(req.headers.referer).searchParams.get('page') + refererUrl.searchParams.get('page') ); } catch (e) { next(); @@ -31,8 +30,8 @@ const proxyPathPageResourceRedirect = (req, res, next) => { } const urlToProxy = `https://${proxyPageUrl.host}${req.url}`; - fetch(urlToProxy) - .then(response => response.body.pipe(res)) // https://github.com/node-fetch/node-fetch#bodybody + require(urlToProxy) + .then(response => response.body.pipe(res)) .catch(err => console.error(err)); } diff --git a/lib/preview/routes/proxyPathPageRouteHandler.js.js b/lib/preview/routes/proxyPathPageRouteHandler.js.js index 38149f7..efad899 100644 --- a/lib/preview/routes/proxyPathPageRouteHandler.js.js +++ b/lib/preview/routes/proxyPathPageRouteHandler.js.js @@ -1,5 +1,6 @@ const { proxyPage } = require('../proxyPage'); -const { trackPreviewEvent } = require('../previewUtils'); +const { trackPreviewEvent, buildHTMLResponse } = require('../previewUtils'); +const { logger } = require('./../../../logger'); const buildProxyRouteHandler = (sessionInfo) => { @@ -16,13 +17,13 @@ const buildProxyRouteHandler = (sessionInfo) => { } catch (e) { const message = 'Please provide a valid page query parameter, e.g., http://localhost:3000/proxy?page=https://yourdomain.com/path'; - console.warn(message); + logger.warn(message); return res.status(400).send(message); } // validate proxy page URL query params if (proxyPageUrl.searchParams.has('hs_preview')) { const message = `Can't make a proxy request, you cannot proxy URLs that include internal query params like hs_preview`; - console.warn(message); + logger.warn(message); return res.status(400).send(message); } trackPreviewEvent('proxy-path-route'); @@ -39,36 +40,23 @@ const buildProxyRouteHandler = (sessionInfo) => { } } -const buildProxyIndex = () => { - // If we want to embed a script or something into the proxy - // we can wrap it here or parse the returned HTML into the render - // but that's maybe trickier... - return ` - - - - -
    -

    Local Proxy

    -
    - - - -
    -
    - - `; -} - - - +const buildProxyIndex = () => buildHTMLResponse(` +
    +

    Local Proxy

    +
    + + + +
    +
    +`); module.exports = { buildProxyRouteHandler diff --git a/lib/preview/routes/template.js b/lib/preview/routes/template.js index 271160b..39da3b0 100644 --- a/lib/preview/routes/template.js +++ b/lib/preview/routes/template.js @@ -1,11 +1,12 @@ const http = require('../../../http'); const { fetchTemplatesByPath } = require('../../../api/designManager'); -const { getPreviewUrl, addRefreshScript, memoize } = require('../previewUtils'); +const { getPreviewUrl, addRefreshScript, memoize, buildHTMLResponse } = require('../previewUtils'); +const { logger } = require('./../../../logger'); const cachedFetchTemplatesByPath = memoize(fetchTemplatesByPath); const buildTemplateRouteHandler = (sessionInfo) => { - const { portalId, sessionToken } = sessionInfo; + const { accountId, sessionToken } = sessionInfo; return async (req, res) => { const { templatePath } = req.params; @@ -18,9 +19,9 @@ const buildTemplateRouteHandler = (sessionInfo) => { const calculatedPath = `@preview/${sessionToken}/${templatePath}`; let templateInfo; try { - templateInfo = await cachedFetchTemplatesByPath(portalId, calculatedPath); + templateInfo = await cachedFetchTemplatesByPath(accountId, calculatedPath); } catch (err) { - console.log(`Failed to fetch template info for ${calculatedPath}: ${err}`); + logger.error(`Failed to fetch template info for ${calculatedPath}: ${err}`); } if (!templateInfo || !('previewKey' in templateInfo)) { res.status(502).set({ 'Content-Type': 'text/html' }).end(buildErrorIndex()); @@ -32,7 +33,7 @@ const buildTemplateRouteHandler = (sessionInfo) => { ...req.query } const previewUrl = new URL(getPreviewUrl(sessionInfo, params)); - const result = await http.get(portalId, { + const result = await http.get(accountId, { baseUrl: previewUrl.origin, uri: previewUrl.pathname, query: params, @@ -44,33 +45,19 @@ const buildTemplateRouteHandler = (sessionInfo) => { } } -const buildErrorIndex = () => { - return ` - - - - -
    -

    Error

    -

    Failed to fetch template data.

    -
    - - `; -} +const buildErrorIndex = () => buildHTMLResponse(` +
    +

    Error

    +

    Failed to fetch template data.

    +
    +`) -const buildTemplateIndex = () => { - return ` - - - - -
    -

    Template

    -

    Provide a template path in the request

    -
    - - `; -} +const buildTemplateIndex = () => buildHTMLResponse(` +
    +

    Template

    +

    Provide a template path in the request

    +
    +`) module.exports = { buildTemplateRouteHandler diff --git a/lib/preview/shadowDevServer.js b/lib/preview/shadowDevServer.js deleted file mode 100644 index b72136a..0000000 --- a/lib/preview/shadowDevServer.js +++ /dev/null @@ -1,86 +0,0 @@ -const net = require('net'); -const express = require('express'); -const { Router } = require('express'); -const cors = require("cors"); - -const SHADOW_PORT = 1442; - -const startShadowDevServer = async (sessionInfo) => { - const portIsTaken = await new Promise((res, rej) => { - console.log(`Starting proxy link server on port ${SHADOW_PORT}`); - const testNetServer = net.createServer(); - - testNetServer.once('error', err => { - if (err['code'] === 'EADDRINUSE') { - console.error( - `Port ${SHADOW_PORT} is in use. HubSpot is unable to automatically create proxy links in the Sprocket Menu` - ); - res(true); - } else { - rej(err); - } - }); - - testNetServer.once('listening', () => { - testNetServer.close(); - }); - - testNetServer.once('close', () => { - res(false); - }); - - testNetServer.listen(SHADOW_PORT); - }); - - if (portIsTaken) { - return; - } - - const shadowDevServer = express(); - - shadowDevServer.listen(SHADOW_PORT); - - shadowDevServer.use( - '/', - await createShadowDevServerRoutes(sessionInfo) - ); -} - -const createShadowDevServerRoutes = async (sessionInfo) => { - const shadowDevServerRoutes = Router(); - - shadowDevServerRoutes.get( - '/check-if-local-dev-server', - cors(), - shadowDevServerCheckHandler(sessionInfo), - ); - - return shadowDevServerRoutes; -} - -const shadowDevServerCheckHandler = (sessionInfo) => async (req, res) => { - const { PORT } = sessionInfo; - const { query } = req; - - if (query) { - const { - hostName, - pathName, - } = query; - - let hasJSBuildingBlocks; - let localProxyUrl; - - hasJSBuildingBlocks = false; - localProxyUrl = `http://${hostName}.hslocal.net:${PORT}${pathName}`; - - res - .status(200) - .set({ 'Content-Type': 'application/json' }) - .json({ hasJSBuildingBlocks, localProxyUrl }); -}; -} - -module.exports = { - startShadowDevServer -} diff --git a/lib/preview/sprocketMenuServer.js b/lib/preview/sprocketMenuServer.js new file mode 100644 index 0000000..9bd25c2 --- /dev/null +++ b/lib/preview/sprocketMenuServer.js @@ -0,0 +1,84 @@ +const net = require('net'); +const express = require('express'); +const { Router } = require('express'); +const cors = require("cors"); +const { logger } = require('../../logger'); + +const SPROCKET_MENU_PORT = 1442; + +const startSprocketMenuServer = async (sessionInfo) => { + const portIsTaken = await new Promise((res, rej) => { + logger.log(`Starting proxy link server on port ${SPROCKET_MENU_PORT}`); + const testNetServer = net.createServer(); + + testNetServer.once('error', err => { + if (err['code'] === 'EADDRINUSE') { + logger.error( + `Port ${SPROCKET_MENU_PORT} is in use. HubSpot is unable to automatically create proxy links in the Sprocket Menu` + ); + res(true); + } else { + rej(err); + } + }); + + testNetServer.once('listening', () => { + testNetServer.close(); + }); + + testNetServer.once('close', () => { + res(false); + }); + + testNetServer.listen(SPROCKET_MENU_PORT); + }); + + if (portIsTaken) { + return; + } + + const sprocketMenuServer = express(); + + sprocketMenuServer.listen(SPROCKET_MENU_PORT); + + sprocketMenuServer.use( + '/', + await createSprocketMenuServerRoutes(sessionInfo) + ); +} + +const createSprocketMenuServerRoutes = async (sessionInfo) => { + const sprocketMenuServerRoutes = Router(); + + sprocketMenuServerRoutes.get( + '/check-if-local-dev-server', + cors(), + sprocketMenuServerCheckHandler(sessionInfo), + ); + + return sprocketMenuServerRoutes; +} + +const sprocketMenuServerCheckHandler = (sessionInfo) => async (req, res) => { + const { PORT } = sessionInfo; + const { query } = req; + + if (query) { + const { + hostName, + pathName, + } = query; + + let hasJSBuildingBlocks = false; + let localProxyUrl = `http://${hostName}.hslocal.net:${PORT}${pathName}`; + + res + .status(200) + .set({ 'Content-Type': 'application/json' }) + .json({ hasJSBuildingBlocks, localProxyUrl }); +}; +} + +module.exports = { + startSprocketMenuServer +} diff --git a/package.json b/package.json index 32d1998..f425d67 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "js-yaml": "^4.1.0", "mkcert-cli": "^1.5.0", "moment": "^2.24.0", - "node-fetch-commonjs": "^3.3.2", "p-queue": "^6.0.2", "prettier": "^1.19.1", "request": "^2.88.2", diff --git a/personalAccessKey.js b/personalAccessKey.js index d93096b..ef981a1 100644 --- a/personalAccessKey.js +++ b/personalAccessKey.js @@ -43,6 +43,7 @@ async function getAccessToken( accessToken: response.oauthAccessToken, expiresAt: moment(response.expiresAtMillis), scopeGroups: response.scopeGroups, + enabledFeatures: response.enabledFeatures, encodedOauthRefreshToken: response.encodedOauthRefreshToken, }; } @@ -52,11 +53,12 @@ async function refreshAccessToken( personalAccessKey, env = ENVIRONMENTS.PROD ) { - const { accessToken, expiresAt } = await getAccessToken( + const accessTokenResponse = await getAccessToken( personalAccessKey, env, accountId ); + const { accessToken, expiresAt } = accessTokenResponse const config = getAccountConfig(accountId); updateAccountConfig({ @@ -69,15 +71,15 @@ async function refreshAccessToken( }); writeConfig(); - return accessToken; + return accessTokenResponse; } -async function getNewAccessToken(accountId, personalAccessKey, expiresAt, env) { +async function getNewAccessToken(accountId, personalAccessKey, expiresAt, env, fullAPIResponse=false) { const key = getRefreshKey(personalAccessKey, expiresAt); if (refreshRequests.has(key)) { return refreshRequests.get(key); } - let accessToken; + let accessTokenResponse; try { const refreshAccessPromise = refreshAccessToken( accountId, @@ -87,14 +89,17 @@ async function getNewAccessToken(accountId, personalAccessKey, expiresAt, env) { if (key) { refreshRequests.set(key, refreshAccessPromise); } - accessToken = await refreshAccessPromise; + accessTokenResponse = await refreshAccessPromise; } catch (e) { if (key) { refreshRequests.delete(key); } throw e; } - return accessToken; + if (fullAPIResponse) { + return accessTokenResponse; + } + return accessTokenResponse.accessToken; } async function accessTokenForPersonalAccessKey(accountId) { @@ -118,6 +123,20 @@ async function accessTokenForPersonalAccessKey(accountId) { return auth.tokenInfo.accessToken; } +async function enabledFeaturesForPersonalAccessKey(accountId) { + const { auth, personalAccessKey, env } = getAccountConfig(accountId); + const authTokenInfo = auth && auth.tokenInfo; + + const accessTokenResponse = await getNewAccessToken( + accountId, + personalAccessKey, + authTokenInfo && authTokenInfo.expiresAt, + env, + fullAPIResponse=true + ) + return accessTokenResponse.enabledFeatures; +} + /** * Adds a account to the config using authType: personalAccessKey @@ -179,6 +198,7 @@ const updateConfigWithPersonalAccessKey = async (configData, makeDefault) => { module.exports = { accessTokenForPersonalAccessKey, + enabledFeaturesForPersonalAccessKey, updateConfigWithPersonalAccessKey, getAccessToken, }; From 09f13a20286fae10cdd10f5bd172ad64151c9986 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Wed, 7 Feb 2024 14:30:47 -0500 Subject: [PATCH 49/56] Changes for pre-empathy test --- api/preview.js | 2 +- lang/en.lyaml | 7 ++- lib/preview.js | 66 ++++++++++++++++++++++--- lib/preview/previewUtils.js | 7 ++- lib/preview/routes/index.js | 10 ++-- lib/preview/sprocketMenuServer.js | 3 +- lib/uploadFolder.js | 81 ++++++++++++++++++++----------- 7 files changed, 130 insertions(+), 46 deletions(-) diff --git a/api/preview.js b/api/preview.js index c428608..cbc3d9b 100644 --- a/api/preview.js +++ b/api/preview.js @@ -8,7 +8,7 @@ async function fetchPreviewRender(url, sessionInfo) { urlObject.searchParams.append('localPreviewToken', sessionToken); urlObject.searchParams.append('hsCacheBuster', Date.now()); - return request(urlObject.href).then(res => res.text()); + return request(urlObject.href); } module.exports = { diff --git a/lang/en.lyaml b/lang/en.lyaml index 9051199..78e8bfd 100644 --- a/lang/en.lyaml +++ b/lang/en.lyaml @@ -14,6 +14,11 @@ en: debug: templates: creatingPath: "Making {{ path }} if needed" + preview: + initialProgressBar: + start: "Starting..." + uploading: "Uploading..." + finish: "Complete!" commands: create: subcommands: @@ -52,5 +57,3 @@ en: fieldsJsSyntaxError: "There was an error converting JS file \"{{ path }}\"" fieldsJsNotReturnArray: "There was an error loading JS file \"{{ path }}\". Expected type \"Array\" but received type \"{{ returned }}\" . Make sure that your function returns an array" fieldsJsNotFunction: "There was an error loading JS file \"{{ path }}\". Expected type \"Function\" but received type \"{{ returned }}\". Make sure that your default export is a function." - - diff --git a/lib/preview.js b/lib/preview.js index f0c572a..adf1454 100644 --- a/lib/preview.js +++ b/lib/preview.js @@ -9,7 +9,7 @@ const { logApiErrorInstance, logApiUploadErrorInstance, } = require('../errorHandlers'); -const { uploadFolder } = require('./uploadFolder'); +const { uploadFolder, FileUploadResultType } = require('./uploadFolder'); const { shouldIgnoreFile, ignoreFile } = require('../ignoreRules'); const { getFileMapperQueryValues } = require('../fileMapper'); const { upload, deleteFile } = require('../api/fileMapper'); @@ -27,6 +27,10 @@ const { startSprocketMenuServer } = require('./preview/sprocketMenuServer'); const { createHttpsRedirectingServer, } = require('./preview/httpsRedirectingServer'); +const { i18n } = require('../lib/lang'); +const cliProgress = require('cli-progress'); + +const i18nKey = 'cli.lib.preview'; const fileMapperArgs = getFileMapperQueryValues({ mode: 'publish', @@ -127,8 +131,53 @@ const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { const initialPreviewBufferUpload = async (sessionInfo, filePaths) => { const { accountId, src, dest } = sessionInfo; - - return uploadFolder(accountId, src, dest, fileMapperArgs, {}, filePaths); + let uploadOptions = {}; + const progressBar = new cliProgress.SingleBar( + { + gracefulExit: true, + format: '[{bar}] {percentage}% | {value}/{total} | {label}', + hideCursor: true + }, + cliProgress.Presets.rect + ); + progressBar.start(filePaths.length, 0, { + label: i18n(`${i18nKey}.initialProgressBar.start`), + }); + let uploadsHaveStarted = false; + uploadOptions = { + onAttemptCallback: () => {}, + onSuccessCallback: () => { + progressBar.increment() + if (!uploadsHaveStarted) { + uploadsHaveStarted = true; + progressBar.update(0, { + label: i18n(`${i18nKey}.initialProgressBar.uploading`) + }) + } + }, + onFirstErrorCallback: () => { /* Intentionally blank */ }, + onRetryCallback: () => { /* Intentionally blank */}, + onFinalErrorCallback: () => progressBar.increment() + }; + const results = await uploadFolder(accountId, src, dest, fileMapperArgs, uploadOptions, filePaths); + progressBar.update(filePaths.length, { + label: i18n(`${i18nKey}.initialProgressBar.finish`) + }); + progressBar.stop(); + results.forEach((result) => { + if (result.resultType == FileUploadResultType.FAILURE) { + logger.error('Uploading file "%s" to "%s" failed', result.file, dest); + logApiUploadErrorInstance( + result.error, + new ApiErrorContext({ + accountId, + request: dest, + payload: result.file, + }) + ); + } + } + ) }; const startPreviewWatcher = async sessionInfo => { @@ -158,11 +207,14 @@ const startPreviewWatcher = async sessionInfo => { ); watcher.on('ready', () => { - logger.log('Local file watching service has started!'); watcherIsReady = true; }); watcher.on('add', addFileCallback); watcher.on('change', changeFileCallback); + watcher.on('error', error => + logger.error(`Error in file watching: ${error}`) + ); + watcher.on('unlink', deleteFileCallback); watcher.on('unlinkDir', deleteFolderCallback); @@ -180,7 +232,6 @@ const startPreviewWatcher = async sessionInfo => { const createLocalHttpServer = async sessionInfo => { const expressServer = express(); - //expressServer.use(bodyParser.json()); expressServer.use('/', await createPreviewServerRoutes(sessionInfo)); return expressServer; @@ -194,13 +245,14 @@ const preview = async ( ) => { const accountConfig = getAccountConfig(accountId); const domains = await getPortalDomains(accountId); - const sessionToken = '7c964654-7676-4181-93b2-daae33e0b2d5';//uuidv4(); + const sessionToken = uuidv4(); const PORT = port || 3000; const protocol = noSsl ? 'http' : 'https'; const sessionInfo = { src, dest: `@preview/${sessionToken}/${dest}`, + fakeDest: dest, portalName: accountConfig.name, accountId, env: accountConfig.env, @@ -243,7 +295,7 @@ const preview = async ( } startSprocketMenuServer(sessionInfo); logger.log( - `HubSpot preview local dev server hosting at ${protocol}://hslocal.net:${PORT}, portalId=${accountId}` + `Local dev server started at ${protocol}://hslocal.net:${PORT} for portal ${accountId}` ); }; diff --git a/lib/preview/previewUtils.js b/lib/preview/previewUtils.js index fc968c1..fc91901 100644 --- a/lib/preview/previewUtils.js +++ b/lib/preview/previewUtils.js @@ -7,6 +7,7 @@ const { stringify } = require('querystring'); const { logger } = require('./../../logger'); const { getAuthType } = require('./../../lib/config'); const VALID_PROXY_DOMAIN_SUFFIXES = ['localhost', 'hslocal.net']; + const HS_PREVIEW_GATE = "cms:localHublPreviews"; const getPortalDomains = async (accountId) => { @@ -14,7 +15,6 @@ const getPortalDomains = async (accountId) => { const result = await fetchDomains(accountId); return result; } catch (error) { - logger.error("There was a problem fetching domains for your portal. You may be missing a scope necessary for this feature.") return []; } } @@ -39,6 +39,7 @@ const addRefreshScript = (html) => { const refreshScript = `