@arc-fusion/cli
Version:
CLI for running Arc Fusion on your local machine
424 lines (396 loc) • 14.5 kB
JavaScript
/* eslint-disable no-template-curly-in-string */
const os = require('os')
const path = require('path')
const semver = require('semver')
const webpackDockerfile = require('./webpack.Dockerfile')
const getLinkedModules = require('./links')
const {
getEnvVariables,
getBlockVariables,
getLinkedThemeBlocks,
getLinkedEngineSdkBlock,
getLinkedCssFrameworkBlock,
getLinkedThemeComponentsBlock,
getLinkedCacheProxyRepo,
getLinkedEngineRepo,
getLinkedOriginRepo,
getLinkedResolverRepo,
getLinkedFusionVersion,
getLinkedFusionEngineRepo
} = require('../bin/utils/local')
const { mkdirp, writeFile } = require('../bin/utils/promises')
const { MONGO_VERSION } = require('../bin/environment')
const isWindows = /^win/i.test(os.platform())
module.exports = async ({ ADMIN_RELEASE, PROJECT_ROOT, REPO_NAME, CACHE_VOLUME_NAME, admin = true, rebuild, links, production }) => {
const environment = {
DB_NAME: `\${DB_NAME:-${REPO_NAME}}`,
MONGO_URL: `mongodb://data:27017/\${DB_NAME:-${REPO_NAME}}`,
PB_MONGODB_URI: 'mongodb://data:27017',
NODE_ENV: 'development',
ENVIRONMENT: 'localhost',
CONTEXT_PATH: '${CONTEXT_PATH:-pf}',
DEBUG: '${DEBUG:-arc.fusion.render.error}',
HTTP_ENGINE: 'http://engine:8080',
HTTP_RESOLVER: 'http://resolver:8080'
}
const networks = [
'fusion'
]
// get all modules linked via npm link
const linkedModules = await getLinkedModules(PROJECT_ROOT) || {}
const moduleLinks = Object.keys(linkedModules)
.map((linkedModule) => `${linkedModules[linkedModule]}:/opt/engine/bundle/linked_modules/${linkedModule}:ro,cached`)
// check for local repos we need to link
const env = await getEnvVariables(PROJECT_ROOT)
const blocksEnv = await getBlockVariables(PROJECT_ROOT)
if (blocksEnv) {
const linkedBlocks = await getLinkedThemeBlocks(PROJECT_ROOT, env.THEMES_BLOCKS_REPO, blocksEnv, links, production) || []
if (linkedBlocks.length) moduleLinks.push(...linkedBlocks)
const themeComponents = getLinkedThemeComponentsBlock(env.THEME_COMPONENTS_REPO, blocksEnv)
if (themeComponents) moduleLinks.push(themeComponents)
if (blocksEnv.engineSDK) {
const engineSdk = getLinkedEngineSdkBlock(env.ENGINE_SDK_REPO, blocksEnv)
if (engineSdk) moduleLinks.push(engineSdk)
}
if (blocksEnv.cssFramework) {
const cssFramework = getLinkedCssFrameworkBlock(env.CSS_FRAMEWORK_REPO, blocksEnv)
if (cssFramework) moduleLinks.push(cssFramework)
}
}
// These functions will get the path to the linked Pagebuilder repos.
// `fusionLinks` will remain as we separate out more of rest of fusion
const cacheProxyLinks = getLinkedCacheProxyRepo(env.CACHEPROXY_SOURCE_DIR) || {}
if (env.FUSION_REPO && env.ENGINE_SOURCE_DIR) {
console.error('Linking the fusion and engine repos at the same time is not supported. Please unlink one of them.')
}
const engineLinks = {
...getLinkedFusionEngineRepo(env.FUSION_REPO),
...getLinkedEngineRepo(env.ENGINE_SOURCE_DIR)
}
const originLinks = getLinkedOriginRepo(env.ORIGIN_SOURCE_DIR) || {}
const resolverLinks = getLinkedResolverRepo(env.RESOLVER_SOURCE_DIR) || {}
// filter out duplicates from links
const uniqueLinks = [...new Set(moduleLinks)]
// If the bundle has a fusion version tagged, check whether it is 3.2.7 or higher, since
// Redis is only supported there. Error out and notify the user to use higher Fusion version
if (env.FUSION_RELEASE && semver.satisfies(semver.coerce(env.FUSION_RELEASE), '< 3.2.7')) {
console.log('Checking Fusion Release Version')
return console.error('This version of the CLI only supports Fusion engine versions 3.2.7 and higher. Please use later images')
} else if (engineLinks && engineLinks.engine) {
console.log('Checking Linked Fusion Version')
const fusionVersion = getLinkedFusionVersion(env.FUSION_REPO)
if (fusionVersion !== 'latest' && semver.satisfies(semver.coerce(fusionVersion), '< 3.2.7')) { return console.error('This version of the CLI only supports Fusion engine versions 3.2.7 and higher. Please use later images') }
}
// Otherwise, no tags exist, meaning it's using the latest image. All latest images will support redis
// the node_modules volume trick to hide host files from the container causes an error if the directory does not exist
await mkdirp(path.join(PROJECT_ROOT, 'node_modules'))
await writeFile(
path.join(PROJECT_ROOT, '.fusion', 'webpack.Dockerfile'),
await webpackDockerfile(
{
PROJECT_ROOT,
rebuild,
localEngine: !!Object.keys(engineLinks).length,
themesVersion: env.BLOCK_DIST_TAG,
cssVersion: env.CSS_DIST_TAG,
sdkVersion: env.ENGINE_SDK_DIST_TAG,
componentsVersion: env.THEME_COMPONENTS_DIST_TAG,
npmLegacyPeerDeps: env.NPM_LEGACY_PEER_DEPS
})
)
return {
networks: {
fusion: {
driver: 'bridge',
external: false,
internal: false
}
},
secrets: {
github_token: {
environment: 'GITHUB_TOKEN'
}
},
services: {
data: {
...(env.MONGO_SOURCE_DIR && {
build: {
context: path.resolve(env.MONGO_SOURCE_DIR)
}
}),
image: `washpost/mongo-vandelay:${env.MONGO_VERSION || MONGO_VERSION}`,
container_name: 'fusion-data',
environment: {
...environment,
RESTORE: 'true',
SYNC: (isWindows) ? 'true' : ''
},
networks: {
fusion: {
aliases: [
'db',
'database'
]
}
},
ports: [
'27017:27017'
],
volumes: [
'../data/db:/data/db:rw',
'../data/dumps:/data/dumps:rw',
'../data/restore:/data/restore:rw'
]
},
'content-cache': {
// memcached service runs from an image, so there is no Dockerfile
image: 'memcached',
// max size is 10MB
command: 'memcached -I 10m',
container_name: 'fusion-content-cache',
networks
},
'cache-proxy': {
...(cacheProxyLinks.cacheProxy && {
build: {
context: path.resolve(env.CACHEPROXY_SOURCE_DIR),
secrets: ['github_token']
}
}),
image: cacheProxyLinks.cacheProxy ? 'local-washpost/fusion-cache-proxy:latest' : 'washpost/fusion-cache-proxy:${CACHE_PROXY_RELEASE:-latest}',
container_name: 'fusion-cache-proxy',
depends_on: [
'content-cache'
],
networks,
ports: cacheProxyLinks.cacheProxy ? ['9030:8080'] : [],
environment: {
CACHE_PROXY_CREDENTIALS: 'localhost:password',
CACHE_NODES: 'content-cache:11211',
WATCH: cacheProxyLinks.cacheProxy ? 'true' : null
},
...(cacheProxyLinks.cacheProxy && { volumes: cacheProxyLinks.cacheProxy })
},
webpack: {
build: {
context: '..',
dockerfile: './.fusion/webpack.Dockerfile',
args: {
FUSION_RELEASE: '${FUSION_RELEASE:-latest}',
NODE_VERSION: env.ENGINE_NODE_VERSION
},
secrets: ['github_token']
},
restart: 'unless-stopped',
command: 'watch',
container_name: 'fusion-webpack',
env_file: [
'../.env'
],
environment: {
...environment,
NODE_OPTIONS: `--max-old-space-size=${env.WEBPACK_HEAP_SIZE || '4096'}`,
WATCH: engineLinks.engine ? 'true' : null,
POLL: null,
// because we import ALL variables from .env, ignore PORT
PORT: '8080'
},
network_mode: 'none',
volumes: [
'..:/opt/engine/bundle/src:ro,cached',
// hide the bundle's node_modules from docker
'/opt/engine/bundle/src/node_modules',
// because this volume is created on docker-compose up, it will copy the existing data from the image
'shared:/opt/engine/bundle/node_modules:rw,delegated',
// this is external so make sure we override the existing data in the image with the data we have
`${CACHE_VOLUME_NAME}:/opt/engine/node_modules/.cache:rw,nocopy,delegated`,
'../.fusion/babel:/opt/engine/bundle/babel:rw,cached',
'../.fusion/build:/opt/engine/bundle/build:rw,cached',
'../.fusion/dist:/opt/engine/bundle/dist:rw,cached',
'../.fusion/generated:/opt/engine/bundle/generated:rw,cached',
...(engineLinks.engine || []),
...uniqueLinks
]
},
engine: {
...(engineLinks.engine && {
build: {
context: env.FUSION_REPO ? path.resolve(env.FUSION_REPO, 'engine') : path.resolve(env.ENGINE_SOURCE_DIR),
args: {
NODE_VERSION: env.ENGINE_NODE_VERSION
},
secrets: ['github_token']
}
}),
image: 'washpost/fusion-engine:${FUSION_RELEASE:-latest}',
restart: 'unless-stopped',
container_name: engineLinks.engine ? 'fusion-engine-local' : 'fusion-engine',
depends_on: [
'cache-proxy',
'data'
],
env_file: [
'../.env'
],
environment: {
...environment,
NODE_OPTIONS: `--max-old-space-size=${env.ENGINE_HEAP_SIZE || '2048'}`,
CACHE_PROXY_URL: 'http://localhost:password@cache-proxy:8080/cache',
CACHE_PREFIX: '0803',
// because we import ALL variables from .env, ignore PORT
PORT: '8080',
SLS_RELOAD_HANDLER: `${env.SLS_RELOAD_HANDLER || 'true'}`
},
networks,
ports: engineLinks.engine ? ['9010:8080', '9229:9229'] : [],
volumes: [
'..:/opt/engine/bundle/src:ro,cached',
// hide the bundle's node_modules from docker
'/opt/engine/bundle/src/node_modules',
// read-only and nocopy ensures the data will be shared from the webpack container
'shared:/opt/engine/bundle/node_modules:ro,nocopy,delegated',
'../.fusion/babel:/opt/engine/bundle/babel:ro,cached',
'../.fusion/build:/opt/engine/bundle/build:ro,cached',
// need to be able to write skeleton scripts
'../.fusion/dist:/opt/engine/bundle/dist:rw,cached',
...(engineLinks.engine || []),
...uniqueLinks
]
},
resolver: {
...(resolverLinks.resolver && {
build: {
context: path.resolve(env.RESOLVER_SOURCE_DIR),
secrets: ['github_token'],
args: {
NODE_VERSION: env.RESOLVER_NODE_VERSION
}
}
}),
image: 'washpost/fusion-resolver:${RESOLVER_RELEASE:-latest}',
container_name: resolverLinks.resolver ? 'fusion-resolver-local' : 'fusion-resolver',
depends_on: [
'data',
'engine'
],
environment: {
...environment,
RESOLVE_FROM_DB: 'true', // should only be 'true' for local dev environments
TRAILING_SLASH_RULE: null, // Options are FORCE, DROP, or NOOP
WATCH: resolverLinks.resolver ? 'true' : null
},
networks,
ports: resolverLinks.resolver ? ['9020:8080', '9021:9229'] : [],
...(resolverLinks.resolver && { volumes: resolverLinks.resolver })
},
...(
(admin)
? {
admin: {
image: `washpost/pb-editor-api:\${PB_RELEASE:-${ADMIN_RELEASE}}`,
container_name: 'fusion-admin',
depends_on: [
'data'
],
env_file: [
'../.env'
],
environment: {
...environment,
KAFKA_HOSTS: '',
PORT: 8888
},
networks,
ports: []
}
}
: {}
),
'fusion-cli-api': {
...(env.FUSION_CLI_API_REPO && {
build: path.resolve(env.FUSION_CLI_API_REPO),
volumes: [
`${path.resolve(env.FUSION_CLI_API_REPO)}:/app`
]
}),
image: 'washpost/fusion-cli-api:${CLI_API_RELEASE:-production}',
container_name: 'fusion-cli-api',
env_file: ['../.env'],
environment: {
...environment
},
networks,
ports: [
'${PORT:-80}:8080'
]
},
origin: {
...(originLinks.origin && {
build: {
context: path.resolve(env.ORIGIN_SOURCE_DIR),
secrets: ['github_token'],
args: {
NODE_VERSION: env.ORIGIN_NODE_VERSION
}
}
}),
image: 'washpost/fusion-origin:${ORIGIN_RELEASE:-latest}',
container_name: 'fusion-origin',
depends_on: [
'engine',
'resolver'
],
environment: {
...environment,
...(
(admin)
? {
PB_ADMIN: 'http://admin:8888',
PB_EDITOR: 'http://${PB_EDITOR:-beta}.pb-admin.aws.arc.pub'
}
: {}
),
...(originLinks.origin && {
NODE_INSPECT: 'true',
WATCH: 'true'
}),
DEFAULT_ARC_SITE: null,
IS_ADMIN: 'true'
},
networks,
ports: originLinks.origin ? ['8081:8081', '9001:9229'] : [],
volumes: [
'../mocks:/opt/origin/mocks:ro',
'../resources:/opt/origin/resources:ro',
'../.fusion/dist:/opt/origin/dist:ro,cached',
...(originLinks.origin || [])
]
},
themes: {
image: 'pagebuilderteam/arc-themes-stylebuilder:latest',
env_file: [
'../.env'
],
environment: {
...environment,
STYLES_PATH: '/opt/engine/bundle/node_modules',
STYLES_WRITE_PATH: '/opt/origin',
FEATURE_PACK_PATH: '/opt/engine/bundle/src'
},
volumes: [
'shared:/opt/engine/bundle/node_modules:ro,nocopy,delegated',
'..:/opt/engine/bundle/src:ro,cached',
'../site-styles:/opt/origin/:rw',
...uniqueLinks
]
}
},
volumes: {
shared: null,
[CACHE_VOLUME_NAME]: {
external: true
}
}
}
}