@factor/cli
Version:
Learn how to use Factor's CLI tool at [https://factor.dev]
308 lines (260 loc) • 7.8 kB
text/typescript
/* eslint-disable unicorn/no-process-exit */
import { buildProduction } from "@factor/build/webpack-config"
import { generateLoaders } from "@factor/cli/extension-loader"
import * as tools from "@factor/api"
import commander from "commander"
import log from "@factor/api/logger"
import { emitEvent } from "@factor/api/events"
import execa from "execa"
import { createServer } from "@factor/server"
import { showInstallRoutine } from "@factor/loader"
import { getRemoteConfig } from "@factor/api/remote"
import { serverInfo, getCliExecutor } from "./util"
import { factorize, setEnvironment } from "./factorize"
import { CommandOptions } from "./types"
import pkg from "./package.json"
import LoadingBar from "./loading"
import { logSetupNeeded } from "./setup"
interface CommanderArguments {
options: object[]
parent: Record<string, any>
[key: string]: any
}
/**
* Opens the node inspector port
* https://nodejs.org/api/inspector.html
*/
const initializeNodeInspector = async (): Promise<void> => {
const inspector = require("inspector")
inspector.close()
await inspector.open()
}
/**
* Serve Factor
* @param setup - server options
*/
export const runServer = async (setup: CommandOptions): Promise<void> => {
await tools.runCallbacks("create-server", setup)
}
/**
* Runs a command entered in the CLI
* @param options - command options
*/
export const runCommand = async (options: CommandOptions): Promise<void> => {
const setup = {
install: true,
clean: true,
NODE_ENV: options.command == "dev" ? "development" : "production",
...options,
}
const {
install,
filter,
command,
inspect,
PORT,
NODE_ENV,
skipVerifyDeps,
debug,
} = setup
/**
* Set environment again based on any changes made via CLI arguments
*/
setEnvironment({ NODE_ENV, PORT, debug, command })
/**
* Gets remote configuration info
*/
getRemoteConfig()
try {
await createServer({ port: PORT, openOnReady: true })
} catch (error) {
log.error(error)
process.exit(1)
}
/**
* Show installation and setup routine
*/
if (command == "dev") {
await showInstallRoutine()
}
/**
* Open node inspector port if 'inspect' flag is set
*/
if (command && inspect) {
await initializeNodeInspector()
}
try {
const bar = new LoadingBar()
await bar.update({ percent: 12, msg: "setting up" })
/**
* Make sure all package dependencies are installed and updated
*/
if (install && !skipVerifyDeps) {
await bar.update({
percent: 25,
msg: `checking dependencies with ${getCliExecutor()}`,
})
const verifyDepProcess = execa(getCliExecutor(), ["install"])
await verifyDepProcess
await bar.update({ percent: 55, msg: "generating files" })
try {
generateLoaders(setup)
} catch (error) {
bar.stop()
log.error(error)
}
}
/**
* Extend and setup Node server environment
*/
await bar.update({ percent: 80, msg: "adding server extensions" })
await factorize(setup)
await bar.update({ percent: 99, msg: "loaded" })
bar.stop()
} catch (error) {
emitEvent("buildError", error)
throw error
}
await tools.runCallbacks(`environment-created`, setup)
/**
* Log initial server info
*/
await serverInfo({ NODE_ENV, command })
logSetupNeeded(command)
try {
if (command && ["build", "start"].includes(command)) {
await buildProduction(setup)
} else if (command == "setup") {
await tools.runCallbacks(`cli-setup`, setup)
} else if (command == "run") {
if (!filter) throw new Error("Filter argument is required.")
await tools.runCallbacks(`cli-run-${filter}`, setup)
log.success(`Successfully ran "${filter}"\n\n`)
}
if (command && ["start", "dev", "serve"].includes(command)) {
await runServer(setup) // Long running process
} else {
if (command) log.success(`Successfully ran [${command}]`)
// eslint-disable-next-line unicorn/no-process-exit
process.exit(0)
}
} catch (error) {
log.error(error)
}
return
}
/**
* Clean up commanders provided arguments to just return needed CLI arguments
* @library commander
* @param commanderArguments - arguments provided by commander lib
*/
const cleanArguments = (commanderArguments: CommanderArguments): Record<string, any> => {
const out: { [index: string]: any } = {}
const { parent = {}, program = {}, ...rest } = commanderArguments
const flat = { ...program, ...parent, ...rest }
// Remove all keys starting with Capital letters or underscore
Object.keys(flat).forEach((k) => {
if (!k.startsWith("_")) {
out[k] = flat[k]
}
})
return out
}
/**
* Handle the CLI using Commander
* Set up initial Node environment
*/
export const execute = (): void => {
process.noDeprecation = true
process.maxOldSpaceSize = 8192
setEnvironment()
// options added by filters, plugins or if not wanted in '--help'
commander.allowUnknownOption(true)
commander.noHelp = true
commander
.version(pkg.version)
.description("CLI for managing Factor data, builds and deployments")
.option("--PORT <PORT>", "set server port. default: 3000")
.option("--ENV <ENV>", "set FACTOR_ENV. default: NODE_ENV")
.option("--restart", "restart server process flag")
.option("--debug", "log debugging info")
.option("--offline", "run in offline mode")
.option("--inspect", "run node debug-mode inspector")
commander
.command("test")
.description("Used for testing")
.action(() => {})
commander
.command("dev")
.description("Start development server")
.option("--static-files", "use static file system for builds instead of memory")
.option(
"--watch-server",
"server development mode - restart the server on file changes"
)
.option("--skip-verify-deps", "Skip dependency check")
.action((_arguments) => {
runCommand({
command: "dev",
...cleanArguments(_arguments),
NODE_ENV: "development",
})
})
commander
.command("start")
.description("Build and then serve production app.")
.action((_arguments) =>
runCommand({ command: "start", ...cleanArguments(_arguments) })
)
commander
.command("serve [NODE_ENV]")
.description("Serve app in selected environment.")
.action((NODE_ENV, _arguments) =>
runCommand({
command: "serve",
...cleanArguments(_arguments),
install: false,
NODE_ENV,
})
)
commander
.command("build")
.option("--analyze", "Analyze package size")
.option("--speed", "Output build speed data")
.description("Build production app")
.action((_arguments) =>
runCommand({ command: "build", ...cleanArguments(_arguments) })
)
commander
.command("setup [filter]")
.description("Setup and verify your Factor app")
.action((filter, _arguments) =>
runCommand({
command: "setup",
filter,
clean: false,
...cleanArguments(_arguments),
})
)
commander
.command("run <filter>")
.description("Run CLI utilities based on filter name (see documentation)")
.action((filter, _arguments) =>
runCommand({
command: "run",
filter,
install: false,
...cleanArguments(_arguments),
})
)
commander
.command("create-loaders")
.option("--clean", "clean generated directories before creation")
.description("Generate extension loaders")
.action((_arguments) => generateLoaders(cleanArguments(_arguments)))
try {
commander.parse(process.argv)
} catch (error) {
throw new Error(error)
}
}