From becfca55db4b260ec5a696f0d85d571623687e66 Mon Sep 17 00:00:00 2001 From: James Singleton Date: Tue, 29 Sep 2020 13:28:08 -0700 Subject: [PATCH] feat(prod-sample): deploy module util (#308) (#323) --- package-lock.json | 58 ++----------- package.json | 1 + prod-sample/README.md | 21 +++++ scripts/build-sample-modules.js | 50 ++++------- scripts/deploy-prod-sample-module.js | 125 +++++++++++++++++++++++++++ scripts/utils.js | 40 ++++++++- 6 files changed, 211 insertions(+), 84 deletions(-) create mode 100644 scripts/deploy-prod-sample-module.js diff --git a/package-lock.json b/package-lock.json index 579605e49..3ecd500db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -151,6 +151,15 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, + "read-pkg-up": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", + "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "requires": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + } + }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -21583,55 +21592,6 @@ "path-type": "^3.0.0" } }, - "read-pkg-up": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", - "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", - "requires": { - "find-up": "^3.0.0", - "read-pkg": "^3.0.0" - }, - "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "requires": { - "locate-path": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" - } - } - }, "readable-stream": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", diff --git a/package.json b/package.json index 98ef1b884..aa1199b93 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "test:lockfile": "lockfile-lint -p package-lock.json -t npm -a npm -o https: -c -i", "test:lint": "eslint --ext js,jsx,md,snap .", "start": "node lib/server/index.js", + "start:inspect": "node --inspect --expose-gc lib/server/index.js", "test:unit": "jest --testPathIgnorePatterns integration --config jest.config.js", "pretest:integration": "concurrently \"npm run build:prod-sample\" \"docker-compose -f ./prod-sample/docker-compose.yml pull nginx selenium-chrome\" --kill-others-on-fail -n build-prod-sample,build-dependency-images", "test:integration": "JEST_TEST_REPORT_PATH=./test-results/integration-test-report.html jest integration --config jest.integration.config.js --forceExit", diff --git a/prod-sample/README.md b/prod-sample/README.md index 0df93db95..81db5e2e3 100644 --- a/prod-sample/README.md +++ b/prod-sample/README.md @@ -57,3 +57,24 @@ local CDN server. Similarly, the source code to any existing Modules inside the `sample-modules` directory can be modified to then be built and bundled from source when `npm run start:prod-sample` is run. + + +## Manually Deploying modules + +To aid local production testing use the `deploy-prod-sample-module` script to deploy any module to +`prod-sample`. This script will create a production build of the module and publish it to the `prod-sample` cdn. +After it's been published the script will enable a module to be deployed to the `prod-sample` one-app server by +updating the `prod-sample` module map. + +```bash +$ node ./scripts/deploy-prod-sample-module.js --module-path="../path-to-module/[your-module]" +``` + +### Options + +| Argument | Description | Example | Required | +|---- |--------- |----------|- | +| `--module-path` | relative path to the module |`--module-path="../relative-path/[your-module]"` | X | +| `--skip-install` | don't install before building module |`--skip-install=true` | | +| `--skip-build` | don't build the module |`--skip-build=true` | | + diff --git a/scripts/build-sample-modules.js b/scripts/build-sample-modules.js index 906866e3c..0dd250f53 100644 --- a/scripts/build-sample-modules.js +++ b/scripts/build-sample-modules.js @@ -18,18 +18,16 @@ const fs = require('fs-extra'); const path = require('path'); -const util = require('util'); -const childProcess = require('child_process'); const { argv } = require('yargs'); -const promisifiedExec = util.promisify(childProcess.exec); - const { sanitizeEnvVars, nginxOriginStaticsRootDir, sampleModulesDir, - promisifySpawn, sampleProdDir, + npmInstall, + npmProductionBuild, + getGitSha, } = require('./utils'); const nginxOriginStaticsModulesDir = path.resolve(nginxOriginStaticsRootDir, 'modules'); @@ -41,31 +39,6 @@ const bundleStaticsOrigin = argv.bundleStaticsOrigin || 'https://sample-cdn.fran const sanitizedEnvVars = sanitizeEnvVars(); -async function npmInstall(directory, moduleName, version) { - console.time(`${moduleName}@${version}`); - console.log(`⬇️ Installing ${moduleName}@${version}...`); - try { - await promisifySpawn('npm ci', { cwd: directory, shell: true, env: { ...sanitizedEnvVars, NODE_ENV: 'development', NPM_CONFIG_PRODUCTION: false } }); - } catch (error) { - console.error(`🚨 ${moduleName}@${version} failed to install:`); - throw error; - } - console.log(`✅ ‍${moduleName}@${version} Installed!`); - console.timeEnd(`${moduleName}@${version}`); -} - -async function npmProductionBuild(directory, moduleName, version) { - console.time(`${moduleName}@${version}`); - console.log(`🛠 Building ${moduleName}@${version}...`); - try { - await promisifySpawn('npm run build', { shell: true, cwd: directory, env: { ...sanitizedEnvVars, NODE_ENV: 'production' } }); - } catch (error) { - console.error(`🚨 ${moduleName}@${version} failed to build:`); - throw error; - } - console.log(`✅ ‍${moduleName}@${version} Built!`); - console.timeEnd(`${moduleName}@${version}`); -} async function updateModuleVersion(directory, moduleVersion) { const packageJsonPath = path.resolve(directory, 'package.json'); @@ -91,11 +64,20 @@ const buildModule = async (pathToModule) => { const moduleVersion = path.basename(directory); const moduleName = path.basename(path.resolve(directory, '..')); await updateModuleVersion(directory, moduleVersion); - await npmInstall(directory, moduleName, moduleVersion); - await npmProductionBuild(directory, moduleName, moduleVersion); + await npmInstall({ + directory, + moduleName, + moduleVersion, + envVars: sanitizedEnvVars, + }); + await npmProductionBuild({ + directory, + moduleName, + moduleVersion, + envVars: sanitizedEnvVars, + }); // use one app git commit sha as module version - const { stdout } = await promisifiedExec('git rev-parse --short HEAD'); - const gitSha = stdout.trim(); + const gitSha = getGitSha(); const pathToModuleBuildDir = path.resolve(`${pathToModule}/build/`); const pathToBundleIntegrityManifest = path.join(`${pathToModule}/bundle.integrity.manifest.json`); const pathToOriginModuleStatics = path.resolve(`${nginxOriginStaticsModulesDir}/${gitSha}/${moduleName}`); diff --git a/scripts/deploy-prod-sample-module.js b/scripts/deploy-prod-sample-module.js new file mode 100644 index 000000000..36dfce365 --- /dev/null +++ b/scripts/deploy-prod-sample-module.js @@ -0,0 +1,125 @@ +#!/usr/bin/env node + +/* + * Copyright 2020 American Express Travel Related Services Company, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +const fs = require('fs-extra'); +const path = require('path'); +const { argv } = require('yargs'); + +const { + sanitizeEnvVars, + nginxOriginStaticsRootDir, + npmInstall, + npmProductionBuild, + getGitSha, +} = require('./utils'); + +const { + modulePath, + skipBuild, + skipInstall, + bundleStaticsOrigin = 'https://sample-cdn.frank', +} = argv; + +const sanitizedEnvVars = sanitizeEnvVars(); +const nginxOriginStaticsModulesDir = path.resolve(nginxOriginStaticsRootDir, 'modules'); +const originModuleMapPath = path.resolve(nginxOriginStaticsRootDir, 'module-map.json'); + +const getPreBuiltModuleInfo = (pathToModule) => { + const pkgPath = path.resolve(pathToModule, 'package.json'); + // eslint-disable-next-line import/no-dynamic-require, global-require + const { name: moduleName, version: moduleVersion } = require(pkgPath); + + const pathToBundleIntegrityManifest = path.resolve(`${pathToModule}/bundle.integrity.manifest.json`); + // eslint-disable-next-line global-require,import/no-dynamic-require + const integrityDigests = require(pathToBundleIntegrityManifest); + + return { moduleName, moduleVersion, integrityDigests }; +}; + +const buildModule = async (pathToModule) => { + const pkgPath = path.resolve(pathToModule, 'package.json'); + // eslint-disable-next-line import/no-dynamic-require, global-require + const { name: moduleName, version: moduleVersion } = require(pkgPath); + + if (!skipInstall) { + await npmInstall({ + directory: pathToModule, + moduleName, + moduleVersion, + envVars: sanitizedEnvVars, + }); + } + + await npmProductionBuild({ + directory: pathToModule, + moduleName, + moduleVersion, + envVars: sanitizedEnvVars, + }); + + const pathToBundleIntegrityManifest = path.resolve(`${pathToModule}/bundle.integrity.manifest.json`); + // eslint-disable-next-line global-require,import/no-dynamic-require + const integrityDigests = require(pathToBundleIntegrityManifest); + + return { + moduleName, moduleVersion, integrityDigests, + }; +}; + +const deployModuleToProdSampleCDN = async (pathToModule, moduleName) => { + const pathToModuleBuildDir = path.resolve(`${pathToModule}/build/`); + // use one app git commit sha to namespace modules + const gitSha = getGitSha(); + const pathToOriginModuleStatics = path.resolve(`${nginxOriginStaticsModulesDir}/${gitSha}/${moduleName}`); + await fs.ensureDir(pathToOriginModuleStatics); + await fs.copy(pathToModuleBuildDir, pathToOriginModuleStatics, { overwrite: true }); + return pathToOriginModuleStatics; +}; + +const updateModuleMap = async ({ moduleName, moduleVersion, integrityDigests }) => { + // eslint-disable-next-line global-require,import/no-dynamic-require + const moduleMap = require(originModuleMapPath); + console.log(`Updating module map for ${moduleName}@${moduleVersion}`); + const gitSha = getGitSha(); + const moduleBundles = { + browser: { + url: `${bundleStaticsOrigin}/modules/${gitSha}/${moduleName}/${moduleVersion}/${moduleName}.browser.js`, + integrity: integrityDigests.browser, + }, + legacyBrowser: { + url: `${bundleStaticsOrigin}/modules/${gitSha}/${moduleName}/${moduleVersion}/${moduleName}.legacy.browser.js`, + integrity: integrityDigests.legacyBrowser, + }, + node: { + url: `${bundleStaticsOrigin}/modules/${gitSha}/${moduleName}/${moduleVersion}/${moduleName}.node.js`, + integrity: integrityDigests.node, + }, + }; + moduleMap.modules[moduleName] = moduleBundles; + fs.writeFile(originModuleMapPath, JSON.stringify(moduleMap, null, 2)); +}; + +const deployModule = async () => { + console.time('Deploying module', modulePath); + const moduleInfo = skipBuild ? getPreBuiltModuleInfo(modulePath) : await buildModule(modulePath); + await deployModuleToProdSampleCDN(modulePath, moduleInfo.moduleName); + await updateModuleMap(moduleInfo); + console.timeEnd('Deploying module', modulePath); +}; + +deployModule(); diff --git a/scripts/utils.js b/scripts/utils.js index a49573092..20026e7b4 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -15,7 +15,7 @@ */ const { resolve } = require('path'); -const { spawn } = require('child_process'); +const { spawn, execSync } = require('child_process'); const sampleProdDir = resolve('./prod-sample/'); const sampleModulesDir = resolve(sampleProdDir, 'sample-modules'); @@ -54,10 +54,48 @@ const promisifySpawn = (...args) => new Promise((res, rej) => { }); }); +async function npmInstall({ + directory, moduleName, moduleVersion, envVars = {}, +}) { + console.time(`${moduleName}@${moduleVersion}`); + console.log(`⬇️ Installing ${moduleName}@${moduleVersion}...`); + try { + await promisifySpawn('npm ci', { cwd: directory, shell: true, env: { ...envVars, NODE_ENV: 'development', NPM_CONFIG_PRODUCTION: false } }); + } catch (error) { + console.error(`🚨 ${moduleName}@${moduleVersion} failed to install:`); + throw error; + } + console.log(`✅ ‍${moduleName}@${moduleVersion} Installed!`); + console.timeEnd(`${moduleName}@${moduleVersion}`); +} + +async function npmProductionBuild({ + directory, moduleName, moduleVersion, envVars = {}, +}) { + console.time(`${moduleName}@${moduleVersion}`); + console.log(`🛠 Building ${moduleName}@${moduleVersion}...`); + try { + await promisifySpawn('npm run build', { shell: true, cwd: directory, env: { ...envVars, NODE_ENV: 'production' } }); + } catch (error) { + console.error(`🚨 ${moduleName}@${moduleVersion} failed to build:`); + throw error; + } + console.log(`✅ ‍${moduleName}@${moduleVersion} Built!`); + console.timeEnd(`${moduleName}@${moduleVersion}`); +} + +const getGitSha = () => { + const stdout = execSync('git rev-parse --short HEAD').toString(); + return stdout.trim(); +}; + module.exports = { sampleProdDir, sampleModulesDir, nginxOriginStaticsRootDir, sanitizeEnvVars, promisifySpawn, + npmProductionBuild, + npmInstall, + getGitSha, };