Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 5 additions & 9 deletions api/domains.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we just throw here we can remove the catch and rethrow

}

module.exports = {
Expand Down
4 changes: 3 additions & 1 deletion api/preview.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const { request } = require('../http');

async function fetchPreviewRender(url, sessionInfo) {
const { sessionToken } = sessionInfo;

Expand All @@ -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);
}

module.exports = {
Expand Down
2 changes: 0 additions & 2 deletions lang/en.lyaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,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."


15 changes: 15 additions & 0 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving this to config to be useable elsewhere

getAndLoadConfigIfNeeded,
getEnv,
getConfig,
Expand Down
37 changes: 21 additions & 16 deletions lib/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shadowDevServer -> sprocketMenuServer to be a little more obvious what it's for

const {
createHttpsRedirectingServer,
} = require('./preview/httpsRedirectingServer');
Expand Down Expand Up @@ -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)) {
Expand All @@ -120,15 +120,17 @@ 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 initialPreviewBufferUpload = async (sessionInfo, filePaths, uploadOptions) => {
const { accountId, src, dest } = sessionInfo;

@jsines jsines Feb 7, 2024

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the initial upload to be less spammy and only draw attention if there's actually a problem - instead display a progress bar & print out any errors at the end

Screen.Recording.2024-02-07.at.3.17.21.PM.mov

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generally like this pattern better, because it is cleaner in the terminal -- but I'm not sure if there is discussion to be had about this pattern being globalized as the standard 'upload' pattern for the CLI

cc @brandenrodgers - we have the cli guidelines being formed right now and I saw on the doc that the component library was slotted to include progress indicators - We are somewhat straying from what is the current pattern for uploading here for the moment, do you have any concerns about that for the interim? I'm assuming that once the guidelines are established, we can circle back and match the decided upon pattern.

const { onFinishCallback, ...rest } = uploadOptions;

return uploadFolder(portalId, src, dest, fileMapperArgs, {}, filePaths);
const results = await uploadFolder(accountId, src, dest, fileMapperArgs, rest, filePaths);
onFinishCallback(results);
};

const startPreviewWatcher = async sessionInfo => {
Expand Down Expand Up @@ -158,11 +160,14 @@ const startPreviewWatcher = async sessionInfo => {
);

watcher.on('ready', () => {
console.log('Local file watching service has started!');
watcherIsReady = true;
});
watcher.on('add', addFileCallback);
watcher.on('change', changeFileCallback);
watcher.on('error', error =>
logger.error(`An error occurred while watching files: ${error}`)
);

watcher.on('unlink', deleteFileCallback);
watcher.on('unlinkDir', deleteFolderCallback);

Expand All @@ -180,7 +185,6 @@ const startPreviewWatcher = async sessionInfo => {

const createLocalHttpServer = async sessionInfo => {
const expressServer = express();
//expressServer.use(bodyParser.json());
expressServer.use('/', await createPreviewServerRoutes(sessionInfo));

return expressServer;
Expand All @@ -190,19 +194,20 @@ const preview = async (
accountId,
src,
dest,
{ notify, filePaths, skipUpload, noSsl, port }
{ notify, filePaths, skipUpload, noSsl, port, uploadOptions }
) => {
const accountConfig = getAccountConfig(accountId);
const domains = await getPortalDomains(accountId);
const sessionToken = '96cd331a-189d-41f2-8a4c-a12485402eff';
const sessionToken = uuidv4();
const PORT = port || 3000;
const protocol = noSsl ? 'http' : 'https';

const sessionInfo = {
src,
dest: `@preview/${sessionToken}/${dest}`,
fakeDest: dest,
portalName: accountConfig.name,
portalId: accountId,
accountId,
env: accountConfig.env,
personalAccessKey: accountConfig.personalAccessKey,
// we find hublet later in the content metadata fetch
Expand All @@ -215,7 +220,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();
Expand All @@ -225,7 +230,7 @@ const preview = async (
}

if (!skipUpload) {
await initialPreviewBufferUpload(sessionInfo, filePaths);
await initialPreviewBufferUpload(sessionInfo, filePaths, uploadOptions);
}
const expressServer = await createLocalHttpServer(sessionInfo);
const previewWatcher = await startPreviewWatcher(sessionInfo);
Expand All @@ -241,9 +246,9 @@ const preview = async (
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}`
startSprocketMenuServer(sessionInfo);
logger.log(
`Local dev server started at ${protocol}://hslocal.net:${PORT} for portal ${accountId}`
);
};

Expand Down
8 changes: 5 additions & 3 deletions lib/preview/createRoutes.js
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -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);
Expand All @@ -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;
Expand Down
9 changes: 5 additions & 4 deletions lib/preview/httpsRedirectingServer.js
Original file line number Diff line number Diff line change
@@ -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');

Expand All @@ -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`);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put these in the os temp dir feels a little cleaner pattern for the millisecond they exist before they get deleted. Should be cross platform but I'll make sure

return { key, cert };
}

Expand Down
69 changes: 29 additions & 40 deletions lib/preview/previewUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Comment thread
TanyaScales marked this conversation as resolved.
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.")
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) => {
Expand All @@ -44,6 +39,7 @@ const addRefreshScript = (html) => {
const refreshScript = `
<script>
(() => {
const MAX_WAIT = 16000;
const NORMAL_WAIT_MS = 1000;
const BACKOFF_RATIO = 2;
let nextWait = NORMAL_WAIT_MS;
Expand All @@ -62,7 +58,9 @@ const addRefreshScript = (html) => {
nextWait = NORMAL_WAIT_MS;
})
.catch(err => {
nextWait *= BACKOFF_RATIO;
if (nextWait * BACKOFF_RATIO <= MAX_WAIT) {
nextWait *= BACKOFF_RATIO;
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Figured I'd cap this while I'm in here and close an open issue

console.log('Disconnected from local server... (retrying in ' + nextWait / 1000 + 's)');
});
}, NORMAL_WAIT_MS);
Expand All @@ -78,10 +76,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/',
Expand All @@ -93,7 +89,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;
Expand Down Expand Up @@ -135,38 +131,30 @@ const trackPreviewEvent = async (action) => {
accountId
).catch(
(err) => {
console.error(`trackUsage failed: ${JSON.stringify(err, null, 2)}`);
logger.debug(`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 `
<!DOCTYPE html>
<head>
</head>
<body>
${content}
</body>
`;
}

module.exports = {
isInternalCMSRoute,
Expand All @@ -180,4 +168,5 @@ module.exports = {
hidePreviewInDest,
trackPreviewEvent,
isUngatedForPreview,
buildHTMLResponse,
}
5 changes: 2 additions & 3 deletions lib/preview/proxyPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Loading