UNPKG

@magento/pwa-buildpack

Version:

Build/Layout optimization tooling and Peregrine framework adapters for the Magento PWA

241 lines (226 loc) 8.43 kB
const debug = require('../util/debug').makeFileLogger(__filename); const { join } = require('path'); const { createHash } = require('crypto'); const devcert = require('devcert'); const os = require('os'); const chalk = require('chalk'); const execa = require('execa'); const pkgDir = require('pkg-dir'); const { username } = os.userInfo(); /** * Monkeypatch devcert to fix * https://github.com/magento/pwa-studio/issues/679 which is blocked by * https://github.com/davewasmer/devcert/pull/30. * TODO: Remove this when a release of devcert without this bug is available */ const devCertUtils = require('devcert/dist/utils'); const MacOSPlatform = require('devcert/dist/platforms/darwin'); /* istanbul ignore next: temporary until we switch to devcert fork */ const proto = (MacOSPlatform.default || MacOSPlatform).prototype; /* istanbul ignore next: temporary until we switch to devcert fork */ proto.isNSSInstalled = function() { try { return devCertUtils .run('brew list -1') .toString() .includes('\nnss\n'); } catch (e) { return false; } }; const DEFAULT_NAME = 'my-pwa'; const DEV_DOMAIN = 'local.pwadev'; const isSudoSession = () => execa .shell('sudo -n true') .then(() => true) .catch(() => false); const alreadyProvisioned = hostname => { const configuredDomains = devcert.configuredDomains(); debug( 'checking for %s in devcert.configuredDomains() === %o', hostname, configuredDomains ); const isProvisioned = configuredDomains.includes(hostname); debug('isProvisioned? %s', isProvisioned); return isProvisioned; }; function getCert(hostname) { // Manually create a Promise here to obtain a "reject" function in closure, // so we can use a setTimeout to reject the promise after 30 seconds. // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { const timeout = setTimeout( () => reject( new Error( 'Timed out waiting for SSL certificate generation and trust.' ) ), 30000 ); debug('set UI timeout for getCert("%s")', hostname); try { if (!alreadyProvisioned(hostname)) { if (process.stdin.isTTY) { if (!(await isSudoSession())) { console.warn( chalk.greenBright(`Creating a local development domain requires temporary administrative privileges. Please enter the password for ${chalk.whiteBright( username )} on ${chalk.whiteBright(os.hostname())}.`) ); } else { debug('appears to be a sudo session already'); } } else { debug( 'non-interactive! cleared UI timeout for getCert("%s")', hostname ); clearTimeout(timeout); return reject( new Error( 'Creating a local development domain requires an interactive terminal for the user to answer prompts. Run the development server (e.g. `yarn run watch:venia`) by itself in the terminal to continue.' ) ); } } const certBuffers = await devcert.certificateFor(hostname); debug('certBuffers arrived with %s', Object.keys(certBuffers)); debug('success! cleared UI timeout for getCert("%s")', hostname); clearTimeout(timeout); resolve({ key: certBuffers.key.toString('utf8'), cert: certBuffers.cert.toString('utf8') }); } catch (e) { clearTimeout(timeout); debug( 'failure! cleared UI timeout for getCert("%s"): %o', hostname, e ); reject(e); } }); } function getUniqueDomainAndPorts(directory, customName, addUniqueHash) { debug( 'getUniqueDomainAndPorts(directory %s, customName %s, addUniqueHash %s', directory, customName, addUniqueHash ); let name = DEFAULT_NAME; if (customName && typeof customName === 'string') { name = customName; } else { const packageDir = pkgDir.sync(directory); debug( 'try getting package name from pkgDir.sync(%s), which is %s', directory, packageDir ); const pkgLoc = join(packageDir, 'package.json'); try { // eslint-disable-next-line node/no-missing-require const pkg = require(pkgLoc); debug('retrieved %s: %O', pkgLoc, pkg); if (!pkg.name || typeof pkg.name !== 'string') { throw new Error( `package.json does not have a usable "name" field!` ); } name = pkg.name; debug('retrieved project name %s from %s', name, pkgLoc); } catch (e) { console.warn( debug.errorMsg( `Using default "${name}" prefix. Could not autodetect project name from package.json: ` ) ); debug('pkgDir failed %s', e); } } const dirHash = createHash('md4'); // Using a hash of the current directory is a natural way of preserving // the same "unique" ID for each project, and changing it only when its // location on disk has changed. dirHash.update(directory); const digest = dirHash.digest('base64'); debug('digest created %s', digest); const subdomain = addUniqueHash ? `${name}-${digest.slice(0, 5)}` : name; debug('subdomain created %s', subdomain); // Base64 truncated to 5 characters, stripped of special characters, // and lowercased to be a valid domain, is about 36^5 unique values. // There is therefore a chance of a duplicate ID and host collision, // specifically a 1 in 60466176 chance. // Use the same current directory hash to create a "unique" port number. // This creates a number from 1 to 1000 that wil stay constant for the // current directory. We'll create dev and staging ports for it. const uniquePortOffset = parseInt( Buffer.from(digest, 'base64') .toString('hex') .slice(-5), 16 ) % 1000; const ports = { development: 8000 + uniquePortOffset, staging: 9000 + uniquePortOffset }; debug('ports created %o', ports); // In contrast, port collisions are more likely (1 in 1000), It could be a // lower probability if we allowed more possible ports, but for convenience // and developer recognition, we limit ports to the 8xxx range for // development and 9xxx range for staging. Fortunately, unlike domains, // ports are easy to rebind at runtime if a collision occurs. return { uniqueSubdomain: subdomain .toLowerCase() .replace(/[^a-zA-Z0-9]/g, '-') .replace(/^-+/, ''), ports }; } async function configureHost(options) { debug('options %o', options); const { addUniqueHash = true, dir, subdomain, exactDomain, interactive = true } = options; const { uniqueSubdomain, ports } = getUniqueDomainAndPorts( dir, exactDomain || subdomain, addUniqueHash ); let hostname; if (exactDomain) { hostname = exactDomain; } else { hostname = uniqueSubdomain + '.' + DEV_DOMAIN; } if (!alreadyProvisioned(hostname) && interactive === false) { return false; } try { return { hostname, ports, ssl: await getCert(hostname) }; } catch (e) { throw Error( debug.errorMsg(`Could not setup development domain: \n${e.stack}`) ); } } configureHost.getUniqueDomainAndPorts = getUniqueDomainAndPorts; configureHost.DEFAULT_NAME = DEFAULT_NAME; configureHost.DEV_DOMAIN = DEV_DOMAIN; module.exports = configureHost;