Skip to content
This repository has been archived by the owner on May 3, 2024. It is now read-only.

Commit

Permalink
feat(prod-sample): deploy module util (#308) (#323)
Browse files Browse the repository at this point in the history
  • Loading branch information
James Singleton authored Sep 29, 2020
1 parent 1e63b32 commit becfca5
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 84 deletions.
58 changes: 9 additions & 49 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
21 changes: 21 additions & 0 deletions prod-sample/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | |

50 changes: 16 additions & 34 deletions scripts/build-sample-modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');
Expand All @@ -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}`);
Expand Down
125 changes: 125 additions & 0 deletions scripts/deploy-prod-sample-module.js
Original file line number Diff line number Diff line change
@@ -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();
40 changes: 39 additions & 1 deletion scripts/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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,
};

0 comments on commit becfca5

Please sign in to comment.