fusox
Version:
Command line wrapper for fuse-box
280 lines (225 loc) • 11 kB
JavaScript
const minimist = require('minimist')
const util = require('util')
const dotenv = require('dotenv')
const process = require('process')
const path = require('path')
const os = require('os')
const _ = require('lodash')
const cors = require('cors-anywhere')
const {execSync} = require('child_process')
const {existsSync} = require('fs')
const {parseBoolean, parseNumber, printHeader, getFreePort} = require('../helpers')
module.exports = {parseBuildArgs, parseBuildFlags, buildCommand}
function loadEnvConfig (paths) {
let configs = paths.map((path) => (dotenv.config({path: path}).parsed || {}))
return _.merge({}, ...configs)
}
function parseBuildArgs (args) {
let outerArgs = {...args}
// convert arg1 arg2 to arg1:arg2
if (args._[0] && args._[0].indexOf(':') === -1) {
args._ = [args._.join(':')]
}
return args._.map((entry) => _.merge({}, outerArgs, minimist(entry.split(':'))))
}
function parseBuildFlags (args) {
return args.map((args) => {
let source = args._.shift()
let destination = args._.shift()
if (!source) {
throw new Error('Missing source files directory argument')
}
if (!destination) {
throw new Error('Missing destination directory argument')
}
let mode = args.mode || parseBoolean(args.production) ? 'production' : 'development'
let generateSourceMaps = args.sourcemaps === undefined ? mode === 'development' : parseBoolean(args.sourcemaps)
let useCache = args.cache === undefined ? mode === 'development' : parseBoolean(args.cache)
let runApplication = args.run === undefined ? false : parseBoolean(args.run)
let defaultDevServerPort = args.port === undefined ? 9999 : parseNumber(args.port)
let cleanArtifacts = args.clean === undefined ? false : parseBoolean(args.clean)
let uglifyCode = args.uglify === undefined ? mode === 'production' : parseBoolean(args.uglify)
let runCorsProxy = args.cors === undefined ? false : parseBoolean(args.cors) && mode === 'development'
let defaultCorsProxyPort = 8888
let sourcePath = path.resolve(args.cwd || process.cwd(), (source || '').replace('~', os.homedir()))
let resolveSourcePath = (...segments) => path.resolve(sourcePath, ...segments)
let resolveSourcePathRelative = (path) => (path || '').replace(sourcePath + '/', '')
let cwdPath = args.cwd ? path.resolve(args.cwd) : process.cwd()
let resolveCwdPath = (...segments) => path.resolve(cwdPath, ...segments)
let resolveSourceParentPath = (...segments) => path.resolve(sourcePath, '..', ...segments)
let findExistingPath = (...paths) => paths.find((p) => existsSync(p))
let destinationPath = resolveCwdPath((destination || '').replace('~', os.homedir()))
let resolveDestinationPath = (...segments) => path.resolve(destinationPath, ...segments)
let nodeModulesPath = resolveCwdPath('node_modules')
let customTsconfigPath = resolveCwdPath('tsconfig.json')
let customTsconfigPath2 = resolveSourceParentPath('tsconfig.json')
let defaultTsconfigPath = path.resolve(__dirname, '../assets/tsconfig.json')
let tsconfigPath = findExistingPath(customTsconfigPath, customTsconfigPath2, defaultTsconfigPath)
let customTailwindCssConfigPath = resolveCwdPath('tailwind.js')
let customTailwindCssConfigPath2 = resolveSourceParentPath('tailwind.js')
let defaultTailwindCssConfigPath = null
let tailwindCssConfigPath = findExistingPath(customTailwindCssConfigPath, customTailwindCssConfigPath2, defaultTailwindCssConfigPath)
if (tailwindCssConfigPath) {
process.env.TAILWINDCSS_CONFIG_PATH = tailwindCssConfigPath
}
let customPostCssConfigPath = resolveCwdPath('postcss.js')
let customPostCssConfigPath2 = resolveSourceParentPath('postcss.js')
let defaultPostCssConfigPath = path.resolve(__dirname, '../assets/postcss.js')
let postCssConfigPath = findExistingPath(customPostCssConfigPath, customPostCssConfigPath2, defaultPostCssConfigPath)
let postCssConfig = require(postCssConfigPath)
let watchForChanges = args.watch === undefined ? false : parseBoolean(args.watch)
let parseEnvFiles = args.env === undefined ? true : parseBoolean(args.env)
let envConfigPaths = [
resolveCwdPath('.env.dist'),
resolveCwdPath('.env'),
resolveCwdPath('.env.' + mode + '.dist'),
resolveCwdPath('.env.' + mode),
resolveSourceParentPath('.env.dist'),
resolveSourceParentPath('.env'),
resolveSourceParentPath('.env.' + mode + '.dist'),
resolveSourceParentPath('.env.' + mode),
resolveSourcePath('.env.dist'),
resolveSourcePath('.env'),
resolveSourcePath('.env.' + mode + '.dist'),
resolveSourcePath('.env.' + mode),
]
let envConfig = parseEnvFiles ? loadEnvConfig(envConfigPaths) : {}
process.env.NODE_ENV = envConfig.NODE_ENV = mode
let processEnvConfig = _.mapKeys(
_.mapValues(envConfig, (v) => JSON.stringify(v)),
(v, k) => 'process.env.' + k
)
let packageJsonPath = resolveCwdPath('package.json')
let packageJson = existsSync(packageJsonPath) && require(packageJsonPath) || null
let globalName = (args.name && args.name.toString()) || null
let appName = null
let defaultAssetFilesExtensions = 'jpg,png,gif,svg,txt,html'
let appendAssetFilesExtensions = (args.assets === undefined ? false : args.assets.toString().substr(0, 1) === '+')
args.assets = appendAssetFilesExtensions === false ? args.assets : defaultAssetFilesExtensions + ',' + args.assets.toString().substr(1)
let assetFilesExtensions = (args.assets === undefined ? defaultAssetFilesExtensions : args.assets).toString()
.split(',').map((i) => util.format('*.%s', i.trim()))
let defaultDynamicFilesExtensions = 'json,yml,txt'
let appendDynamicFilesExtensions = (args.dynamic === undefined ? false : args.dynamic.toString().substr(0, 1) === '+')
args.dynamic = appendDynamicFilesExtensions === false ? args.dynamic : defaultDynamicFilesExtensions + ',' + args.dynamic.toString().substr(1)
let dynamicFilesInstructions = (args.dynamic === undefined ? defaultDynamicFilesExtensions : args.dynamic).toString()
.split(',').map((i) => util.format('+**/*.%s', i.trim())).join(' ')
let fuseboxCachePath = resolveCwdPath('.fusebox')
process.env.FUSEBOX_TEMP_FOLDER = fuseboxCachePath
if (mode !== 'development' && mode !== 'production') {
throw new Error(util.format('Mode "%s" is not supported, supported modes are "development" and "production"', mode))
}
if (!existsSync(sourcePath)) {
throw new Error(util.format('Source directory "%s" does not exist', sourcePath))
}
let browser = args.browser === undefined ? false : parseBoolean(args.browser)
let server = args.server === undefined ? false : parseBoolean(args.server)
let electron = args.electron === undefined ? false : parseBoolean(args.electron)
let library = args.library === undefined ? false : parseBoolean(args.library)
let target = args.target || browser && 'browser' || server && 'server' || electron && 'electron' || library && 'library'
if (!target) {
throw new Error('You must specify a build target using either the "--target=browser|server|electron|library", "--browser", "--server", "--electron" or "--library" flag')
}
if (target !== 'browser' && target !== 'server' && target !== 'electron' && target !== 'library') {
throw new Error(util.format('Invalid target provided "%s", valid targets are "browser", "server", "electron" or "library"'))
}
return parseBuildTargetArgs(args, {
cwdPath,
resolveCwdPath,
resolveSourcePath,
resolveSourcePathRelative,
resolveDestinationPath,
mode,
target,
uglifyCode,
runApplication,
defaultDevServerPort,
cleanArtifacts,
sourcePath,
runCorsProxy,
defaultCorsProxyPort,
destinationPath,
nodeModulesPath,
tsconfigPath,
watchForChanges,
parseEnvFiles,
packageJson,
appName,
globalName,
envConfig,
processEnvConfig,
generateSourceMaps,
useCache,
assetFilesExtensions,
dynamicFilesInstructions,
fuseboxCachePath,
tailwindCssConfigPath,
postCssConfigPath,
postCssConfig,
})
})
}
function parseBuildTargetArgs (args, flags) {
if (flags.target === 'browser') {
// dynamic import for fusebox cache override
let {parseBuildBrowserFlags} = require('./buildBrowser')
return parseBuildBrowserFlags(args, flags)
} else if (flags.target === 'server') {
let {parseBuildServerFlags} = require('./buildServer')
return parseBuildServerFlags(args, flags)
} else if (flags.target === 'electron') {
let {parseBuildElectronFlags} = require('./buildElectron')
return parseBuildElectronFlags(args, flags)
} else if (flags.target === 'library') {
let {parseBuildLibraryFlags} = require('./buildLibrary')
return parseBuildLibraryFlags(args, flags)
}
return flags
}
function buildCommand (flags) {
let defaultCorsProxyPort = flags.map((f) => f.defaultCorsProxyPort).shift() || null
return getFreePort(defaultCorsProxyPort)
.then((corsProxyPort) => {
let corsProxyHost = 'localhost'
let runCorsProxy = flags.find((f) => f.runCorsProxy)
if (runCorsProxy) {
if (defaultCorsProxyPort !== corsProxyPort) {
printHeader('CORS proxy port "%s" already in use, fallback to "%s"', defaultCorsProxyPort, corsProxyPort)
}
cors.createServer()
.listen(corsProxyPort, corsProxyHost, () => {
printHeader('CORS proxy server running on http://%s:%s', corsProxyHost, corsProxyPort)
})
}
if (flags.find((f) => f.cleanArtifacts)) {
execSync(util.format('rm -rf %s', process.env.FUSEBOX_TEMP_FOLDER))
}
let builds = flags.map((f) => {
if (f.cleanArtifacts) {
execSync(util.format('rm -rf %s', f.destinationPath))
}
if (runCorsProxy) {
f.envConfig = _.merge(f.envConfig, {
CORS_PROXY_HOST: corsProxyHost,
CORS_PROXY_PORT: corsProxyPort
})
}
printHeader('Loaded env config')
console.dir(f.envConfig, {depth: 10})
if (f.target === 'browser') {
// dynamic import, in order to override fuse box cache folder, see parseBrowserArgs
let {buildBrowserCommand} = require('./buildBrowser')
return buildBrowserCommand(f)
} else if (f.target === 'server') {
let {buildServerCommand} = require('./buildServer')
return buildServerCommand(f)
} else if (f.target === 'electron') {
let {buildElectronCommand} = require('./buildElectron')
return buildElectronCommand(f)
} else if (f.target === 'library') {
let {buildLibraryCommand} = require('./buildLibrary')
return buildLibraryCommand(f)
}
})
return Promise.all(builds)
})
}