vtex
Version:
The platform for e-commerce apps
274 lines (273 loc) • 13.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.appLink = void 0;
const tslib_1 = require("tslib");
const Builder_1 = require("../../api/clients/IOClients/apps/Builder");
const ProjectUploader_1 = require("../../api/modules/apps/ProjectUploader");
const utils_1 = require("../../api/modules/utils");
const utils_2 = require("../../api/error/utils");
const ramda_1 = require("ramda");
const readline_1 = require("readline");
const ProjectFilesManager_1 = require("../../api/files/ProjectFilesManager");
const setup_1 = tslib_1.__importDefault(require("../setup"));
const pinnedDependencies_1 = require("../../api/pinnedDependencies");
const utils_3 = require("../utils");
const manifest_1 = require("../../api/manifest");
const ManifestUtil_1 = require("../../api/manifest/ManifestUtil");
const file_1 = require("../../api/modules/apps/file");
const path_1 = require("path");
const build_1 = require("../../api/modules/build");
const crypto_1 = require("crypto");
const fs_1 = require("fs");
const SessionManager_1 = require("../../api/session/SessionManager");
const YarnFilesManager_1 = require("../../api/files/YarnFilesManager");
const login_1 = tslib_1.__importDefault(require("../auth/login"));
const chalk_1 = tslib_1.__importDefault(require("chalk"));
const chokidar_1 = tslib_1.__importDefault(require("chokidar"));
const debounce_1 = tslib_1.__importDefault(require("debounce"));
const logger_1 = tslib_1.__importDefault(require("../../api/logger"));
const moment_1 = tslib_1.__importDefault(require("moment"));
const async_retry_1 = tslib_1.__importDefault(require("async-retry"));
const use_1 = tslib_1.__importDefault(require("../../api/modules/workspace/use"));
const Messages_1 = require("../../lib/constants/Messages");
const errors_1 = require("../../api/error/errors");
let nodeNotifier;
if (process.platform !== 'win32') {
// eslint-disable-next-line @typescript-eslint/no-require-imports
nodeNotifier = require('node-notifier');
}
const DELETE_SIGN = chalk_1.default.red('D');
const UPDATE_SIGN = chalk_1.default.blue('U');
const INITIAL_LINK_CODE = 'initial_link_required';
const stabilityThreshold = process.platform === 'darwin' ? 100 : 200;
const linkID = crypto_1.randomBytes(8).toString('hex');
const buildersToRunLocalYarn = ['react', 'node'];
const RETRY_OPTS_INITIAL_LINK = {
retries: 2,
minTimeout: 1000,
factor: 2,
};
const performInitialLink = async (root, projectUploader, extraData, unsafe) => {
const yarnFilesManager = await YarnFilesManager_1.YarnFilesManager.createFilesManager(root);
extraData.yarnFilesManager = yarnFilesManager;
yarnFilesManager.logSymlinkedDependencies();
const linkApp = async (bail, tryCount) => {
var _a;
// wrapper for builder.linkApp to be used with the retry function below.
const [localFiles, linkedFiles] = await Promise.all([
file_1.listLocalFiles(root).then(paths => ramda_1.map(ProjectFilesManager_1.createPathToFileObject(root), paths)),
yarnFilesManager.getYarnLinkedFiles(),
]);
const filesWithContent = ramda_1.concat(localFiles, linkedFiles);
if (tryCount === 1) {
const linkedFilesInfo = linkedFiles.length ? `(${linkedFiles.length} from linked node modules)` : '';
logger_1.default.info(`Sending ${filesWithContent.length} file${filesWithContent.length > 1 ? 's' : ''} ${linkedFilesInfo}`);
logger_1.default.debug('Sending files');
filesWithContent.forEach(p => logger_1.default.debug(p.path));
}
if (tryCount > 1) {
logger_1.default.info(`Retrying...${tryCount - 1}`);
}
try {
logger_1.default.info(`Link ID: ${linkID}`);
const { code } = await projectUploader.sendToLink(filesWithContent, linkID, { tsErrorsAsWarnings: unsafe });
if (code !== 'build.accepted') {
bail(new Error('Please, update your builder-hub to the latest version!'));
}
}
catch (err) {
if (err instanceof ProjectUploader_1.ProjectSizeLimitError) {
logger_1.default.error(err.message);
process.exit(1);
}
const data = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.data;
if ((data === null || data === void 0 ? void 0 : data.code) === 'bad_toolbelt_version') {
const errMsg = `${data.message}\n${Messages_1.Messages.UPDATE_TOOLBELT()}`;
logger_1.default.error(errMsg);
process.exit(1);
}
if (err.status) {
const { response } = err;
const { status } = response;
const { message } = data;
const statusMessage = status ? `: Status ${status}` : '';
logger_1.default.error(`Error linking app${statusMessage} (try: ${tryCount})`);
if (message) {
logger_1.default.error(`Message: ${message}`);
}
if (status && status < 500) {
return;
}
}
throw err;
}
};
await async_retry_1.default(linkApp, RETRY_OPTS_INITIAL_LINK);
};
const warnAndLinkFromStart = (root, projectUploader, unsafe, extraData = { yarnFilesManager: null }) => {
logger_1.default.warn('Initial link requested by builder');
performInitialLink(root, projectUploader, extraData, unsafe);
return null;
};
const watchAndSendChanges = async (root, appId, projectUploader, { yarnFilesManager }, unsafe) => {
const changeQueue = [];
const onInitialLinkRequired = err => {
const data = err.response && err.response.data;
if ((data === null || data === void 0 ? void 0 : data.code) === INITIAL_LINK_CODE || (err === null || err === void 0 ? void 0 : err.code) === INITIAL_LINK_CODE) {
return warnAndLinkFromStart(root, projectUploader, unsafe, { yarnFilesManager });
}
throw err;
};
const defaultPatterns = ['*/**', 'manifest.json', 'policies.json', 'cypress.json'];
const linkedDepsPatterns = ramda_1.map(path => path_1.join(path, '**'), yarnFilesManager.symlinkedDepsDirs);
const pathModifier = ramda_1.pipe((path) => yarnFilesManager.maybeMapLocalYarnLinkedPathToProjectPath(path, root), path => path.split(path_1.sep).join('/'));
const pathToChange = (path, remove) => {
const content = remove ? null : fs_1.readFileSync(path_1.resolve(root, path)).toString('base64');
const byteSize = remove ? 0 : Buffer.byteLength(content);
return {
content,
byteSize,
path: pathModifier(path),
};
};
const sendChanges = debounce_1.default(async () => {
try {
logger_1.default.info(`Link ID: ${linkID}`);
return await projectUploader.sendToRelink(changeQueue.splice(0, changeQueue.length), linkID, {
tsErrorsAsWarnings: unsafe,
});
}
catch (err) {
const commandType = err instanceof errors_1.NewStickyHostError ? err.command : 'link';
nodeNotifier === null || nodeNotifier === void 0 ? void 0 : nodeNotifier.notify({
title: appId,
message: `${commandType} died`,
});
if (err instanceof ProjectUploader_1.ChangeSizeLimitError) {
logger_1.default.error(err.message);
process.exit(1);
}
onInitialLinkRequired(err);
}
}, 1000);
const queueChange = (path, remove) => {
console.log(`${chalk_1.default.gray(moment_1.default().format('HH:mm:ss:SSS'))} - ${remove ? DELETE_SIGN : UPDATE_SIGN} ${path}`);
changeQueue.push(pathToChange(path, remove));
sendChanges();
};
const addIgnoreNodeModulesRule = (paths) => paths.concat((path) => path.includes('node_modules'));
const watcher = chokidar_1.default.watch([...defaultPatterns, ...linkedDepsPatterns], {
atomic: stabilityThreshold,
awaitWriteFinish: {
stabilityThreshold,
},
cwd: root,
ignoreInitial: true,
ignored: addIgnoreNodeModulesRule(file_1.getIgnoredPaths(root)),
persistent: true,
usePolling: process.platform === 'win32',
});
return new Promise((resolve, reject) => {
watcher
.on('add', file => queueChange(file))
.on('change', file => queueChange(file))
.on('unlink', file => queueChange(file, true))
.on('error', reject)
.on('ready', resolve);
});
};
async function handlePreLinkLogin({ account, workspace }) {
const postLoginOps = ['releaseNotify'];
if (!SessionManager_1.SessionManager.getSingleton().checkValidCredentials()) {
return login_1.default({ account, workspace, allowUseCachedToken: true, postLoginOps });
}
if (account && workspace) {
return login_1.default({ account, workspace, allowUseCachedToken: true, postLoginOps });
}
if (workspace) {
return use_1.default(workspace);
}
}
async function appLink(options) {
await handlePreLinkLogin({ account: options.account, workspace: options.workspace });
await utils_1.validateAppAction('link');
const unsafe = !!options.unsafe;
const root = ManifestUtil_1.getAppRoot();
const manifest = await manifest_1.ManifestEditor.getManifestEditor();
await manifest.writeSchema();
const mustContinue = await utils_1.continueAfterReactTermsAndConditions(manifest);
if (!mustContinue) {
return;
}
const builderHubMessage = await utils_1.checkBuilderHubMessage('link');
if (!ramda_1.isEmpty(builderHubMessage)) {
await utils_1.showBuilderHubMessage(builderHubMessage.message, builderHubMessage.prompt, manifest);
}
const appId = manifest.appLocator;
const builder = Builder_1.Builder.createClient({}, { timeout: 60000, retries: 3 });
const projectUploader = ProjectUploader_1.ProjectUploader.getProjectUploader(appId, builder);
if (options.setup) {
await setup_1.default({ 'ignore-linked': false });
}
try {
const pinnedDeps = await builder.getPinnedDependencies();
await pinnedDependencies_1.fixPinnedDependencies(pinnedDeps, buildersToRunLocalYarn, manifest.builders);
}
catch (e) {
logger_1.default.info('Failed to check for pinned dependencies');
logger_1.default.debug(e);
}
// Always run yarn locally for some builders
ramda_1.map(utils_3.runYarnIfPathExists, buildersToRunLocalYarn);
if (options.clean) {
logger_1.default.info('Requesting to clean cache in builder.');
const { timeNano } = await builder.clean(appId);
logger_1.default.info(`Cache cleaned successfully in ${utils_3.formatNano(timeNano)}`);
}
const onError = {
// eslint-disable-next-line @typescript-eslint/camelcase
build_failed: () => {
logger_1.default.error(`App build failed. Waiting for changes...`);
},
// eslint-disable-next-line @typescript-eslint/camelcase
initial_link_required: () => warnAndLinkFromStart(root, projectUploader, unsafe),
};
logger_1.default.info(`Linking app ${appId}`);
let unlistenBuild;
const extraData = { yarnFilesManager: null };
try {
const buildTrigger = performInitialLink.bind(this, root, projectUploader, extraData, unsafe);
const [subject] = appId.split('@');
if (options.noWatch) {
await build_1.listenBuild(subject, buildTrigger, { waitCompletion: true });
return;
}
unlistenBuild = await build_1.listenBuild(subject, buildTrigger, { waitCompletion: false, onError }).then(ramda_1.prop('unlisten'));
}
catch (e) {
if (e.response) {
const { data } = e.response;
if (data.code === 'routing_error' && /app_not_found.*vtex\.builder-hub/.test(data.message)) {
return logger_1.default.error('Please install vtex.builder-hub in your account to enable app linking (vtex install vtex.builder-hub)');
}
if (data.code === 'link_on_production') {
throw utils_2.createFlowIssueError(`Please use a dev workspace to link apps. Create one with (${chalk_1.default.blue('vtex use <workspace> -rp')}) to be able to link apps`);
}
if (data.code === 'bad_toolbelt_version') {
const errMsg = `${data.message}\n${Messages_1.Messages.UPDATE_TOOLBELT}`;
return logger_1.default.error(errMsg);
}
}
throw e;
}
readline_1.createInterface({ input: process.stdin, output: process.stdout }).on('SIGINT', () => {
if (unlistenBuild) {
unlistenBuild();
}
logger_1.default.info('Your app is still in development mode.');
logger_1.default.info(`You can unlink it with: 'vtex unlink ${appId}'`);
process.exit();
});
await watchAndSendChanges(root, appId, projectUploader, extraData, unsafe);
}
exports.appLink = appLink;