Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add preview command support #21

Open
wants to merge 70 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 58 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
fe44395
Add preview command support
jsines Jul 26, 2023
de74c6a
Saving work
jsines Sep 25, 2023
d7136a4
Move work in from cloudlfare worker, start implementation of proxy, f…
jsines Oct 2, 2023
f3fb919
Proxying works mostly
jsines Oct 3, 2023
4fdace4
Remove logs
jsines Oct 3, 2023
0aae19b
module and template route handlers
jsines Oct 5, 2023
dd8970e
Flesh out module and template previews, add a few error responses
jsines Oct 11, 2023
fbd4701
Remove filemapper preview changes for later
jsines Oct 12, 2023
b1fa76f
Clean up preview a little bit
jsines Oct 12, 2023
2626400
Fixes logic around watch stuff
jsines Oct 13, 2023
4c404b4
Make initial upload off by default
jsines Oct 16, 2023
dd2108d
Add express
jsines Oct 16, 2023
2eae394
Add express dep
jsines Oct 16, 2023
5ad2ed7
Merge branch 'js/hs-preview' into js/hs-preview-auto-refresh
jsines Oct 16, 2023
d06710b
Add auto refreshing
jsines Oct 17, 2023
4e20f17
Add auto refreshing
jsines Oct 17, 2023
a3bd4d9
Remove logs
jsines Oct 17, 2023
c7ec29b
Whitespace
jsines Oct 17, 2023
e05c847
Whitespace, remove logs
jsines Oct 17, 2023
2d6676c
use nodefetchcommonjs, add routes
jsines Oct 19, 2023
872e25b
proxyResourceRedirect.js
jsines Oct 19, 2023
caaf525
Error log
jsines Oct 19, 2023
81e65a2
Merge pull request #29 from HubSpot/js/hs-preview-auto-refresh
jsines Oct 23, 2023
4818e70
@preview folder support, module/template proxying/auto-refreshing
jsines Oct 24, 2023
8ce1d7e
Merge pull request #30 from HubSpot/js/hs-preview-resource-fetching
jsines Oct 27, 2023
d2b7a87
Merge pull request #31 from HubSpot/js/hs-preview-asset-proxy
jsines Oct 27, 2023
f0651dd
Add hslocal support, shadow dev server, https support
jsines Oct 27, 2023
2e0175c
Fix index linking, change hasJSBuildingBlocks to false, remove consol…
jsines Oct 27, 2023
02a651a
Remove net dep, add req
jsines Oct 30, 2023
b43f515
Committing before I leave...
jsines Nov 3, 2023
4a6b045
Cleanup some semicolons, preview tracking
jsines Nov 6, 2023
5cb6ae1
Add getauthtype
jsines Nov 6, 2023
57bd6c3
Add all changes for now...
jsines Nov 6, 2023
18b4cd5
Undo prettier
jsines Nov 6, 2023
3fddbe0
More remove
jsines Nov 6, 2023
bc206ad
Fix filemapperarg mess I made
jsines Nov 7, 2023
7af2be9
Remove unused args
jsines Nov 7, 2023
d0d181b
Prettier
jsines Nov 7, 2023
5f91643
Add check for gate
jsines Nov 7, 2023
cd01cda
External gate name casing is different
jsines Nov 7, 2023
ce57eff
Uncomment now that BE change merged
jsines Nov 8, 2023
8d95d02
Merge pull request #32 from HubSpot/js/hs-preview-hslocal
jsines Nov 15, 2023
5b3d5ce
Merge pull request #35 from HubSpot/js/hs-preview-improvements
jsines Nov 15, 2023
38280c4
Merge pull request #36 from HubSpot/js/hs-preview-prettier
jsines Nov 15, 2023
11210ad
remove random token gen while testing
jsines Nov 15, 2023
f5c3b13
Static uuid
jsines Nov 15, 2023
1515aab
Fix bad sessionToken staticing, fix .hubl.html
jsines Nov 16, 2023
c3c5865
Swap index routes to new endpoints
jsines Jan 4, 2024
6576623
Merge pull request #37 from HubSpot/js/hs-preview-gate
jsines Jan 4, 2024
ba84f0c
Merge pull request #41 from HubSpot/js/swap-index-routes
jsines Jan 4, 2024
94e6337
Move to cos-rend endpoint
jsines Jan 8, 2024
3a44f9c
Axe requestpage
jsines Jan 8, 2024
b32b15d
-
jsines Jan 8, 2024
33c1f30
Fix paths
jsines Jan 8, 2024
73166ab
Merge pull request #43 from HubSpot/js/swap-index-routes
jsines Jan 8, 2024
da5b43a
Swap render fetch
jsines Jan 23, 2024
404e06a
Proxy page
jsines Jan 23, 2024
f77eeba
Merge pull request #46 from HubSpot/js/renderchange
jsines Jan 23, 2024
e54ae21
Merge branch 'main' into js/hs-preview
jsines Feb 2, 2024
690484a
Changes from DPG review
jsines Feb 5, 2024
09f13a2
Changes for pre-empathy test
jsines Feb 7, 2024
cb5f80a
Fix resource redirect
jsines Feb 7, 2024
44abf0b
Update lib/preview.js
jsines Feb 8, 2024
7c07088
Fixes
jsines Feb 8, 2024
832ccae
Merge pull request #48 from HubSpot/js/hs-preview-preempathytest
jsines Feb 8, 2024
876be35
Move authtype stuff out of config
jsines Feb 8, 2024
0a64658
Merge main
jsines Feb 8, 2024
8662e8a
Missed a var on this callback
jsines Feb 8, 2024
c9e3704
Revert request package version bump
jsines Feb 8, 2024
dcd2068
Remove caching for the moment
jsines Feb 9, 2024
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
28 changes: 28 additions & 0 deletions api/designManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,37 @@ 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 fetchPreviewModules(accountId, token) {
return http.get(accountId, {
uri: `${DESIGN_MANAGER_API_PATH}/modules/local-preview?portalId=${accountId}&previewToken=${token}`,
});
}

async function fetchTemplatesByPath(accountId, path) {
return http.get(accountId, {
uri: `${DESIGN_MANAGER_API_PATH}/templates/by-path/${path}?portalId=${accountId}`,
});
}

async function fetchPreviewTemplates(accountId, token) {
return http.get(accountId, {
uri: `${DESIGN_MANAGER_API_PATH}/templates/local-preview?portalId=${accountId}&previewToken=${token}`,
});
}

module.exports = {
fetchBuiltinMapping,
fetchMenus,
fetchRawAssetByPath,
fetchThemes,
fetchModulesByPath,
fetchPreviewModules,
fetchTemplatesByPath,
fetchPreviewTemplates,
};
20 changes: 20 additions & 0 deletions api/domains.js
Original file line number Diff line number Diff line change
@@ -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;
jsines marked this conversation as resolved.
Show resolved Hide resolved
}
}

module.exports = {
fetchDomains,
};
14 changes: 14 additions & 0 deletions api/preview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
async function fetchPreviewRender(url, sessionInfo) {
const { sessionToken } = sessionInfo;

const urlObject = new URL(url);

urlObject.searchParams.append('localPreviewToken', sessionToken);
urlObject.searchParams.append('hsCacheBuster', Date.now());

return fetch(urlObject.href).then(res => res.text());
jsines marked this conversation as resolved.
Show resolved Hide resolved
}

module.exports = {
fetchPreviewRender,
};
252 changes: 252 additions & 0 deletions lib/preview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
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,
logApiErrorInstance,
logApiUploadErrorInstance,
} = 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 } = require('../path');
const { triggerNotify } = require('./notify');
const { getAccountConfig } = require('./config');
const { createPreviewServerRoutes } = require('./preview/createRoutes');
const {
getPortalDomains,
isUngatedForPreview,
} = require('./preview/previewUtils');
const { markRemoteFsDirty } = require('./preview/routes/meta');
const { startShadowDevServer } = require('./preview/shadowDevServer');
const {
createHttpsRedirectingServer,
} = require('./preview/httpsRedirectingServer');

const fileMapperArgs = getFileMapperQueryValues({
mode: 'publish',
});

async function uploadFile(accountId, src, dest) {
logger.debug(`Attempting to upload file "${src}" to "${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(
error,
new ApiErrorContext({
accountId,
request: dest,
payload: src,
})
);
}
}
}

async function deleteRemoteFile(accountId, remoteFilePath) {
logger.debug(`Attempting to delete file "${remoteFilePath}"`);

try {
await deleteFile(accountId, remoteFilePath, fileMapperArgs);
logger.log(`Deleted file ${remoteFilePath}`);
markRemoteFsDirty();
} catch (error) {
logger.error(`Deleting file ${remoteFilePath} failed`);
logger.debug(`Retrying deletion of file ${remoteFilePath}`);
try {
await deleteFile(accountId, remoteFilePath, fileMapperArgs);
markRemoteFsDirty();
} 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 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);
const deletePromise = deleteRemoteFile(accountId, remotePath);
triggerNotify(notify, 'Removed', filePath, deletePromise);
};
};

const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => {
const { portalId, src, dest, 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(src, dest, filePath);
const uploadPromise = uploadFile(portalId, filePath, destPath);
triggerNotify(notify, notifyMessage, filePath, uploadPromise);
};
};

const initialPreviewBufferUpload = async (sessionInfo, filePaths) => {
const { portalId, src, dest } = sessionInfo;

return uploadFolder(portalId, src, dest, fileMapperArgs, {}, filePaths);
};

const startPreviewWatcher = async sessionInfo => {
const { src } = sessionInfo;
let watcherIsReady = false;

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', () => {
console.log('Local file watching service has started!');
watcherIsReady = true;
});
watcher.on('add', addFileCallback);
watcher.on('change', changeFileCallback);
watcher.on('unlink', deleteFileCallback);
watcher.on('unlinkDir', deleteFolderCallback);

function sleep(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
while (!watcherIsReady) {
await sleep(1);
// do nothing... surely there's a better way to do this...
}
return watcher;
};

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

return expressServer;
};

const preview = async (
accountId,
src,
dest,
{ notify, filePaths, skipUpload, noSsl, port }
) => {
const accountConfig = getAccountConfig(accountId);
const domains = await getPortalDomains(accountId);
const sessionToken = '96cd331a-189d-41f2-8a4c-a12485402eff';
const PORT = port || 3000;
const protocol = noSsl ? 'http' : 'https';

const sessionInfo = {
src,
dest: `@preview/${sessionToken}/${dest}`,
portalName: accountConfig.name,
portalId: accountId,
jsines marked this conversation as resolved.
Show resolved Hide resolved
env: accountConfig.env,
personalAccessKey: accountConfig.personalAccessKey,
// we find hublet later in the content metadata fetch
// can we get that ahead of time? hardcoding it for now
hublet: 'na1',
sessionToken,
domains,
PORT,
protocol,
};
const ungated = await isUngatedForPreview(sessionInfo);
if (!ungated) {
console.log(
`Portal ${accountId} is missing a required gate for this feature.`
);
process.exit();
}
if (notify) {
ignoreFile(notify);
}

if (!skipUpload) {
await initialPreviewBufferUpload(sessionInfo, filePaths);
}
const expressServer = await createLocalHttpServer(sessionInfo);
const previewWatcher = await startPreviewWatcher(sessionInfo);

if (!noSsl) {
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}`
);
};

module.exports = {
preview,
};
36 changes: 36 additions & 0 deletions lib/preview/createRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const { Router } = require('express');
const cors = require('cors');
const { buildIndexRouteHandler } = require('./routes/index.js');
const { buildModuleRouteHandler } = require('./routes/module.js');
const { buildTemplateRouteHandler } = require('./routes/template.js');
const { buildMetaRouteHandler } = require('./routes/meta.js');

const { buildProxyRouteHandler } = require('./routes/proxyPathPageRouteHandler.js');
const { buildProxyPageRouteHandler } = require('./routes/proxyPageRouteHandler.js');
const { proxyPathPageResourceRedirect } = require('./routes/proxyPathPageResourceRedirect.js');
const { proxyPageResourceRedirect } = require('./routes/proxyPageResourceRedirect.js');

const createPreviewServerRoutes = async (sessionInfo) => {
jsines marked this conversation as resolved.
Show resolved Hide resolved
const previewServerRouter = Router();
previewServerRouter.get('/proxy', buildProxyRouteHandler(sessionInfo));
previewServerRouter.get('/module/:modulePath(*)', buildModuleRouteHandler(sessionInfo));
previewServerRouter.get('/template/:templatePath(*)', buildTemplateRouteHandler(sessionInfo));
previewServerRouter.get('/meta', buildMetaRouteHandler(sessionInfo));

previewServerRouter.get('/*', proxyPathPageResourceRedirect)
previewServerRouter.get('/*', proxyPageResourceRedirect);
previewServerRouter.post('/*', proxyPageResourceRedirect);
previewServerRouter.delete('/*', proxyPageResourceRedirect);
previewServerRouter.head('/*', proxyPageResourceRedirect);
previewServerRouter.put('/*', proxyPageResourceRedirect);
previewServerRouter.options('/*', proxyPageResourceRedirect);
previewServerRouter.get('/*', buildProxyPageRouteHandler(sessionInfo));

previewServerRouter.get('/', buildIndexRouteHandler(sessionInfo));

return previewServerRouter;
}

module.exports = {
createPreviewServerRoutes,
}
Loading
Loading