@webda/shell
Version:
Deploy a Webda app or configure it
1,237 lines (1,229 loc) • 45.6 kB
JavaScript
import { CancelablePromise, CryptoService, FileUtils, getCommonJS, JSONUtils, Logger } from "@webda/core";
import { ConsoleLogger, LogFilter, WorkerLogLevelEnum, WorkerOutput } from "@webda/workout";
import chalk from "chalk";
import { spawn } from "child_process";
import { createHash } from "crypto";
import * as fs from "fs";
import { glob } from "glob";
import { createRequire } from "module";
import * as path from "path";
import semver from "semver";
import { Transform } from "stream";
import yargs from "yargs";
import { createGzip } from "zlib";
import { DiagramTypes } from "../code/documentation/diagrams.js";
import { BuildSourceApplication, SourceApplication } from "../code/sourceapplication.js";
import { DeploymentManager } from "../handlers/deploymentmanager.js";
import { WebdaServer } from "../handlers/http.js";
import { WebdaTerminal } from "./terminal.js";
const { __dirname } = getCommonJS(import.meta.url);
export var DebuggerStatus;
(function (DebuggerStatus) {
DebuggerStatus["Stopped"] = "STOPPED";
DebuggerStatus["Stopping"] = "STOPPING";
DebuggerStatus["Compiling"] = "COMPILING";
DebuggerStatus["Launching"] = "LAUNCHING";
DebuggerStatus["Serving"] = "SERVING";
})(DebuggerStatus || (DebuggerStatus = {}));
class WebdaConsole {
static async parser(_args) {
let y = yargs()
// @ts-ignore
.exitProcess(false)
.version(false) // Use our custom display of version
.help(false) // Use our custom display of help
.alias("d", "deployment")
.alias("v", "version")
.alias("h", "help")
.option("log-level", {}) // No default to fallback on env or default of workout
.option("log-format", {
default: ConsoleLogger.defaultFormat
})
.option("no-compile", {
type: "boolean"
})
.option("version", {
type: "boolean"
})
.option("help", {
type: "boolean"
})
.option("notty", {
type: "boolean",
default: false
})
.option("app-path", { default: process.cwd() });
let cmds = WebdaConsole.builtinCommands();
Object.keys(cmds).forEach(key => {
let cmd = cmds[key];
// Remove the first element as it is the handler
y = y.command(cmd.command || key, cmd.description, cmd.module);
});
return y;
}
static serve(argv) {
return new CancelablePromise(async () => {
if (argv.deployment) {
// Loading first the configuration
this.output("Serve as deployment: " + argv.deployment);
}
else {
this.output("Serve as development");
}
/* c8 ignore next 3 - not trying to test message of deprecation */
if (argv.websockets) {
this.output("Deprecated usage of --websockets");
}
WebdaConsole.webda = new WebdaServer(this.app);
this.webda.setDevMode(argv.devMode);
await this.webda.init();
await this.webda.serve(argv.port);
}, async () => {
// Close server
await this.webda.stop();
});
}
/**
* Get a service configuration
*
* @param argv
*/
static async serviceConfig(argv) {
WebdaConsole.webda = new WebdaServer(this.app);
WebdaConsole.webda.initStatics();
let serviceName = argv.name;
let service = this.webda.getService(serviceName);
if (!service) {
let error = "The service " + serviceName + " is missing";
this.output(chalk.red(error));
return -1;
}
this.output(JSON.stringify(service.getParameters(), null, " "));
}
static getApplicationExport() {
const packageInfo = this.app.getPackageDescription();
return {
application: {
name: packageInfo.name,
version: packageInfo.version,
author: packageInfo.author
}
};
}
/**
*
* @param argv
*/
static async models(argv) {
this.webda = new WebdaServer(this.app);
this.webda.initStatics();
const models = this.webda.getModels();
const exportFile = argv.exportFile;
// Generate config
const modelsExport = {
...WebdaConsole.getApplicationExport(),
models: {}
};
// Will be generalized
let shortIds = {};
Object.keys(models)
.filter(id => id.startsWith(this.app.getNamespace()))
.forEach(ope => {
shortIds[models[ope].name] = ope;
});
Object.keys(models)
.filter(id => !id.startsWith(this.app.getNamespace()))
.forEach(ope => {
if (shortIds[models[ope].name]) {
shortIds[ope] = ope;
}
shortIds[models[ope].name] = ope;
});
// Copy all schemas
Object.keys(models)
.filter(id => this.app.hasSchema(id) !== undefined)
.forEach(ope => {
const name = (shortIds[ope] ? ope.substring(0, ope.indexOf("/") + 1) : "") + models[ope].name;
modelsExport.models[name] = this.app.getSchema(ope);
});
if (exportFile.match(/\.(json|ya?ml)$/)) {
FileUtils.save(modelsExport, exportFile, true);
}
else if (exportFile.endsWith(".ts")) {
// Generate code for operation
let code = `/**
* Auto-generated by webda models exporter
*
* To use do not forget to add json-schema-to-ts module
*
* yarn add --dev json-schema-to-ts
*
* OR
*
* npm install --dev json-schema-to-ts
*/
import { FromSchema } from "json-schema-to-ts";
/**
* Models from ${modelsExport.application.name} ${modelsExport.application.version}
*/
export type Models = ${Object.keys(modelsExport.models)
.map(k => `"${k}"`)
.join(" | ")};
// Schema definitions
`;
code += Object.keys(modelsExport.models)
.filter(k => modelsExport.models[k] !== undefined)
.map(k => {
const name = k.replace(/\//, "_");
return `const ${name}Schema = ${JSON.stringify(modelsExport.models[k], undefined, 2)} as const;
export type ${name} = FromSchema<typeof ${name}Schema>;`;
})
.join("\n\n");
fs.writeFileSync(exportFile, code);
}
}
/**
*
* @param argv
*/
static async operations(argv) {
this.webda = new WebdaServer(this.app);
this.webda.initStatics();
const operations = this.webda.listOperations();
const exportFile = argv.exportFile;
// Generate config
const operationsExport = {
...WebdaConsole.getApplicationExport(),
operations,
schemas: {}
};
// Copy all schemas
Object.values(operations).forEach(ope => {
var _a, _b, _c, _d;
if (ope.input) {
(_a = operationsExport.schemas)[_b = ope.input] ?? (_a[_b] = this.app.getSchema(ope.input));
}
if (ope.output) {
(_c = operationsExport.schemas)[_d = ope.output] ?? (_c[_d] = this.app.getSchema(ope.output));
}
});
if (exportFile.match(/\.(json|ya?ml)$/)) {
FileUtils.save(operationsExport, exportFile, true);
}
else if (exportFile.endsWith(".ts")) {
// Generate code for operation
let code = `/**
* Auto-generated by webda operations exporter
*
* To use do not forget to add json-schema-to-ts module
*
* yarn add --dev json-schema-to-ts
*
* OR
*
* npm install --dev json-schema-to-ts
*/
import { FromSchema } from "json-schema-to-ts";
/**
* Operations from ${operationsExport.application.name} ${operationsExport.application.version}
*/
export type Operations = ${Object.keys(operationsExport.operations)
.map(k => `"${k}"`)
.join(" | ")};
// Schema definitions
`;
code += Object.keys(operationsExport.schemas)
.map(k => {
const name = k.replace(/\//, "_");
return `const ${name}Schema = ${JSON.stringify(operationsExport.schemas[k], undefined, 2)} as const;
export type ${name} = FromSchema<typeof ${name}Schema>;`;
})
.join("\n\n");
code += `\n\n// Client connection
export interface Transporter {
call: async (operation: string, input: any) => Promise<any>;
}
export class OperationClient {
constructor(protected transporter: Transporter) {}
${Object.keys(operationsExport.operations)
.map(k => {
return ` public async call(operation: "${k}"${operationsExport.operations[k].input ? ", input: " + operationsExport.operations[k].input.replace(/\//, "_") : ""}) : Promise<${(operationsExport.operations[k].output || "void").replace(/\//, "_")}>;\n`;
})
.join("")}
public async call(operation: Operations, input: any) : Promise<any> {
return this.transporter.call(operation, input);
}
}`;
fs.writeFileSync(exportFile, code);
}
}
/**
* Run a method of a service
*
* @param argv
*/
static async worker(argv) {
const args = [...argv.methodArguments];
const launcher = this.app.getPackageWebda().launcher;
let serviceName = argv.serviceName;
WebdaConsole.webda = new WebdaServer(this.app);
await this.webda.init();
let service = this.webda.getService(serviceName);
let method = argv.methodName || "work";
if (launcher) {
this.log("INFO", `Using launcher: ${launcher.service}.${launcher.method}`);
args.unshift(serviceName, method);
service = this.webda.getService(launcher.service);
serviceName = launcher.service;
method = launcher.method;
}
if (!service) {
this.log("ERROR", `The service ${serviceName} is missing`);
return -1;
}
if (!service[method]) {
this.log("ERROR", `The method ${method} is missing in service ${serviceName}`);
return -1;
}
// Launch the worker with arguments
let timestamp = new Date().getTime();
return Promise.resolve(service[method](...args))
.catch(err => {
this.log("ERROR", "An error occured", err);
})
.then(res => {
if (res) {
this.log("INFO", typeof res === "string" ? res : JSON.stringify(res, undefined, 2));
}
else {
this.log("DEBUG", "Result: void");
}
this.log("TRACE", "Took", Math.ceil((Date.now() - timestamp) / 1000) + "s");
});
}
/**
* Launch debug on application
*
* Compiling application as it is modified
* Relaunching the serve command on any new modification
*
* @param argv
*/
static async debug(argv) {
const curArgs = argv._.slice(1);
if (curArgs.length === 0) {
curArgs.push("serve");
}
let launchServe = (diagnostic) => {
if (typeof diagnostic !== "string" && (diagnostic.code === 6032 || diagnostic.code === 6031)) {
this.setDebuggerStatus(DebuggerStatus.Compiling);
return;
}
// Compilation succeed
if (diagnostic !== "MODULE_GENERATED") {
return;
}
this.setDebuggerStatus(DebuggerStatus.Launching);
if (this.serverProcess) {
this.logger.logTitle("Refreshing Webda");
this.serverProcess.removeAllListeners();
this.serverProcess.kill();
}
else {
this.logger.logTitle("Launching Webda");
this.output("Launch webda in debug mode");
}
let args = ["--noCompile"];
if (argv.deployment) {
args.push("-d");
args.push(argv.deployment);
}
args.push("--appPath");
args.push(this.app.getAppPath());
args.push(...curArgs);
if (argv.port) {
args.push("--port");
args.push(argv.port);
}
if (argv.bind) {
args.push("--bind");
args.push(argv.bind);
}
if (argv.logLevels) {
args.push("--logLevels");
args.push(argv.logLevels);
}
args.push("--logLevel");
args.push("TRACE");
args.push("--logFormat");
args.push("#W# %(l)s|%(m)s");
args.push("--notty");
if (!process.env["NO_DEV_MODE"]) {
args.push("--devMode");
}
let webdaConsole = this;
let lastLineLogLevel;
let addTime = new Transform({
transform(chunk, _encoding, callback) {
chunk
.toString()
.split("\n")
.forEach(line => {
// Stip tags
line = line.replace(/\x1B\[(;?\d{1,3})+[mGK]/g, "");
if (line.indexOf("Server running at") >= 0) {
webdaConsole.setDebuggerStatus(DebuggerStatus.Serving);
webdaConsole.logger.logTitle("Webda " + line.substr(10));
return;
}
let lvl;
if (line.startsWith("#W# ")) {
lastLineLogLevel = lvl = line.substr(4, 5).trim();
line = line.substr(10);
}
else {
lvl = lastLineLogLevel;
}
if (line === "")
return;
if (argv.logLevel) {
// Should compare the loglevel
if (!LogFilter(lvl, argv.logLevel)) {
return;
}
}
webdaConsole.log(lvl, line);
});
callback();
}
});
this.serverProcess = spawn("webda", args, { cwd: this.app.getAppPath() });
this.serverProcess.stdout.pipe(addTime);
this.serverProcess.stderr.pipe(addTime);
this.serverProcess.on("exit", err => {
this.logger.logTitle("Webda Server stopped");
// Might want to auto restart
this.output("Webda Server process exit", err);
});
};
this.app.getCompiler().watch(launchServe, this.logger);
WebdaConsole.configurationWatch(() => {
// Might want to validate against schemas before relaunch
launchServe("MODULE_GENERATED");
}, this.app.getCurrentDeployment());
return new CancelablePromise(() => {
// Never return
});
}
/**
* Watch for configuration changes
*
* @param callback
* @param deployment
*/
static configurationWatch(callback, deployment) {
try {
// Typescript mode -> launch compiler and update after compile is finished
fs.watch(this.app.configurationFile, callback);
if (deployment) {
fs.watch(this.app.deploymentFile, callback);
}
}
catch (err) {
// Cannot fake fs.watch error unless modifying code to allow test
/* c8 ignore next 2 */
this.log("WARN", "Auto-reload for configuration cannot be setup", err);
}
}
/**
* Get shell package version
*/
static getVersion() {
return JSON.parse(fs.readFileSync(__dirname + "/../../package.json").toString()).version;
}
/**
* Add a system to recompile if needed
* @returns
*/
static requireCompilation() {
const f = this.app.getAppPath(".webda");
let webdaCache = {};
if (fs.existsSync(f)) {
webdaCache = JSONUtils.loadFile(f);
}
const digest = webdaCache.digest;
// This is a cache key not cryptographic need
const current = createHash("md5");
const tsCfg = fs.readFileSync(this.app.getAppPath("tsconfig.json"));
current.update(tsCfg);
const ts = JSON.parse(tsCfg.toString());
glob
.sync(ts.include || ["**/*"], {
ignore: ts.exclude,
nodir: true
})
.forEach(f => {
current.update(fs.readFileSync(f));
});
webdaCache.digest = current.digest("hex");
if (webdaCache.digest == digest) {
this.log("DEBUG", "Skipping compilation as nothing changed");
return false;
}
JSONUtils.saveFile(webdaCache, f);
return true;
}
/**
* If deployment in argument: display or export the configuration
* Otherwise launch the configuration UI
*
* @param argv
*/
static async config(argv) {
if (argv.deployment) {
const config = this.app.getConfiguration(argv.deployment);
// Remove deployers as they should be used on deployed app
config.cachedModules.deployers = {};
let json = JSON.stringify(config, null, " ");
if (argv.exportFile) {
fs.writeFileSync(argv.exportFile, json);
}
else {
this.output(json);
}
}
return 0;
}
/**
* If deployment in argument: display or export the configuration
* Otherwise launch the configuration UI
*
* @param argv
*/
static async configEncrypt(argv) {
const filename = argv.file;
if (!filename.match(/\.jsonc?$/)) {
this.log("ERROR", `Only json/jsonc format are handled for now: '${filename}'`);
return -1;
}
this.log("INFO", "Encrypting values in configuration file", filename);
WebdaConsole.webda = new WebdaServer(this.app);
await WebdaConsole.webda.init();
await JSONUtils.updateFile(filename, async (value) => {
if (typeof value === "string") {
// We want to migrate all encrypted string to another type of encryption
if (argv.migrate) {
let newValue = await CryptoService.decryptConfiguration(value);
if (value !== newValue) {
value = `encrypt:${argv.migrate}:${newValue.getValue()}`;
}
}
return await CryptoService.encryptConfiguration(value);
}
else {
return value;
}
});
return 0;
}
/**
* Rotate crypto keys
*/
static async rotateKeys() {
WebdaConsole.webda = new WebdaServer(this.app);
await WebdaConsole.webda.init();
await WebdaConsole.webda.getCrypto().rotate();
return 0;
}
/**
* Deploy the new code
* @param argv
*/
static async deploy(argv) {
let manager = new DeploymentManager(this.app, argv.deployment);
argv._ = argv._.slice(1);
return manager.commandLine(argv);
}
/***
* Get yeoman
*/
static async getYeoman() {
return (await import("yeoman-environment")).default;
}
/**
* Generate a new Webda Application based on yeoman
*
* @param argv
* @param generatorName
*/
static async init(argv, generatorName = "webda") {
if (argv.generator !== undefined) {
generatorName = argv.generator;
}
let generatorAction = "app";
// Cannot start with :
if (generatorName.indexOf(":") > 0) {
[generatorName, generatorAction] = generatorName.split(":");
}
const yeoman = await this.getYeoman();
const env = yeoman.createEnv();
const require = createRequire(import.meta.url);
env.register(require.resolve(`generator-${generatorName}/generators/${generatorAction}/index.js`), generatorName);
return env.run(generatorName);
}
/**
* Init loggers
* @param argv
*/
static async initLogger(argv) {
if (argv["logLevel"]) {
process.env["LOG_LEVEL"] = argv["logLevel"].toUpperCase();
}
if (process.env["LOG_LEVEL"]) {
process.env["LOG_LEVEL"] = process.env["LOG_LEVEL"].toUpperCase();
}
}
/**
* Main command switch
*
* Parse arguments
* Init logger
* Create Webda Application
* Run the command or display help
*
* @param args
*/
static async handleCommand(args, versions, output = undefined) {
let res = await this.handleCommandInternal(args, versions, output);
if (res !== 0 && this.terminal) {
this.terminal.close();
}
return res;
}
static loadExtensions(appPath) {
let getAppPath = function (re) {
return path.join(appPath, re);
};
const paths = [getAppPath("node_modules")];
// Search for workspace
let parent = path.join(appPath, "..");
do {
let packageJson = path.join(parent, "package.json");
if (fs.existsSync(packageJson)) {
let currentInfo = FileUtils.load(packageJson);
if (currentInfo.workspaces) {
this.log("DEBUG", "Application is running within a workspace");
// Replace any relative path by absolute one
paths.push(path.join(parent, "node_modules"));
break;
}
}
parent = path.resolve(path.join(parent, ".."));
} while (parent !== path.resolve(path.join(parent, "..")));
// Replace with find, when a max depth is added
let files = [];
for (let nodeModules of paths) {
if (!fs.existsSync(nodeModules)) {
continue;
}
// Search for shell override
files = files.concat(FileUtils.find(nodeModules, { filterPattern: /webda\.shell\.json/, followSymlinks: true }));
}
let appCustom = getAppPath("webda.shell.json");
if (fs.existsSync(appCustom)) {
files.push(appCustom);
}
// Load each files
for (let i in files) {
try {
let info = JSON.parse(fs.readFileSync(files[i]).toString());
for (let j in info.commands) {
WebdaConsole.extensions[j] = info.commands[j];
WebdaConsole.extensions[j].relPath = path.dirname(files[i]);
}
}
catch (err) {
this.log("ERROR", err);
return -1;
}
}
}
/**
* Generate a JSON Schema for a symbol
*/
static async schema(argv) {
argv._.shift();
let symbol = argv.type;
let filename = argv.exportFile;
let schema = this.app.getCompiler().getSchema(symbol);
if (filename) {
FileUtils.save(schema, filename);
}
else {
this.log("INFO", JSON.stringify(schema, undefined, 2));
}
}
/**
* Print a Fake Terminal to play with @webda/workout
*
* This is a non-supported method therefore no specific unit test
* as there is no value in it
*/
/* c8 ignore start */
static async fakeTerm() {
let res;
let i = 1;
this.app.getWorkerOutput().startProgress("fake", 100, "Fake Progress");
setInterval(() => {
if (++i <= 100) {
this.app.getWorkerOutput().updateProgress(i, "fake");
if (i === 50) {
this.app.getWorkerOutput().startProgress("fake2", 100, "Fake SubProgress");
}
if (i > 50) {
this.app.getWorkerOutput().updateProgress((i - 50) * 2, "fake2");
}
}
else if (i >= 200) {
this.app.getWorkerOutput().startProgress("fake", 100, "Fake Progress");
i = 0;
}
this.log(WorkerLogLevelEnum[Math.floor(Math.random() * 5)], "Random level message".repeat(Math.floor(Math.random() * 10) + 1));
}, 100);
while ((res = await this.app.getWorkerOutput().requestInput("Give me your number input", 0, ["\\d+"]))) {
this.log("INFO", res);
}
}
/* c8 ignore stop */
/**
* Generate the webda.module.json
*/
static async build(argv) {
if (argv.watch) {
this.app.getCompiler().watch(() => {
// Empty callback
}, this.logger);
return new CancelablePromise();
}
if (!(await this.app.generateModule())) {
return -1;
}
try {
if (fs.existsSync(this.app.configurationFile)) {
// Generate config schema as well
this.app.getCompiler().generateConfigurationSchemas();
}
/* c8 ignore next 3 */
}
catch (err) {
this.log("ERROR", "Cannot generate configuration schema", err);
}
}
/**
* Generate a diagram for the application
* @param argv
* @returns
*/
static async diagram(argv) {
this.webda = new WebdaServer(this.app);
await this.webda.init();
let type = argv.diagramType;
// If diagram exists, use it
if (!DiagramTypes[type]) {
this.log("ERROR", `Diagram type '${type}' not supported: supported types are ${Object.keys(DiagramTypes)}`);
return -1;
}
let diagram = new DiagramTypes[type]();
if (argv.exportFile) {
diagram.update(argv.exportFile, this.webda);
this.log("INFO", `Diagram '${type}' exported to ${argv.exportFile}`);
}
else {
this.log("INFO", diagram.generate(this.webda));
}
return 0;
}
/**
* Return the default builin command map
*/
static builtinCommands() {
return {
serve: {
handler: WebdaConsole.serve,
description: "Serve the application",
module: {
devMode: {
alias: "x"
},
port: {
alias: "p",
default: 18080
},
bind: {
alias: "b",
default: "127.0.0.1"
},
websockets: {
alias: "w",
deprecated: "websockets can be enable by adding specific modules now"
}
}
},
deploy: {
handler: WebdaConsole.deploy,
description: "Deploy the application"
},
"new-deployment": {
command: "new-deployment [name]",
handler: DeploymentManager.newDeployment,
description: "Create a new deployment for the application",
module: y => {
return y.command("name", "Deployment name to create");
}
},
stores: {
command: "stores",
handler: WebdaConsole.stores,
description: "Display current stores"
},
store: {
command: "store <storeName> <action>",
handler: WebdaConsole.store,
description: "Store actions",
module: y => {
return y.command("storeName", "Store name to use", y2 =>
/* c8 ignore next 2 */
y2.command("export <filepath>", "Export the store to a file").option("batchSize"));
}
},
"service-configuration": {
command: "service-configuration <name>",
handler: WebdaConsole.serviceConfig,
description: "Display the configuration of a service",
module: y => {
return y.command("name", "Service name to display configuration for");
}
},
launch: {
command: "launch <serviceName> [methodName] [methodArguments...]",
handler: WebdaConsole.worker,
description: "Launch a method of a service",
module: y => {
return y.command("serviceName", "Service name to launch");
}
},
diagram: {
handler: WebdaConsole.diagram,
description: "Generate a diagram of the application",
command: "diagram <diagramType> [exportFile]"
},
debug: {
handler: WebdaConsole.debug,
description: "Debug current application",
module: {
port: {
alias: "p",
default: 18080
},
bind: {
alias: "b",
default: "127.0.0.1"
},
websockets: {
alias: "w"
}
}
},
config: {
handler: WebdaConsole.config,
command: "config [exportFile]",
description: "Generate the configuration of the application",
module: y => {
return y.command("exportFile", "File to export configuration to");
}
},
"config-encrypt": {
handler: WebdaConsole.configEncrypt,
command: "config-encrypt [file]",
description: "Encrypt all fields due for encryption in the file",
module: {
migrate: {
type: "string"
}
}
},
init: {
command: "init [generator]",
handler: WebdaConsole.init,
description: "Initiate a new webda project using yeoman generator"
},
build: {
handler: WebdaConsole.build,
description: "Generate the module for the application",
module: {
watch: {
alias: "w"
}
}
},
openapi: {
command: "openapi [exportFile]",
handler: WebdaConsole.generateOpenAPI,
description: "Generate the OpenAPI definition for the app",
module: {
"include-hidden": {
type: "boolean",
default: false
}
}
},
schema: {
command: "schema <type> [exportFile]",
handler: WebdaConsole.schema,
description: "Generate a schema for a type"
},
types: {
handler: WebdaConsole.types,
description: "List all available types for this project"
},
faketerm: {
handler: WebdaConsole.fakeTerm,
description: "Launch a fake interactive terminal"
},
"rotate-keys": {
handler: WebdaConsole.rotateKeys,
description: "Rotate encryption keys or create them"
},
operations: {
command: "operations <exportFile>",
handler: WebdaConsole.operations,
description: "Export application operations to a definition or code",
module: y => {
return y.command("exportFile", "File to export configuration to");
}
},
models: {
command: "models <exportFile>",
handler: WebdaConsole.models,
description: "Export application models to a definition or code",
module: y => {
return y.command("exportFile", "File to export configuration to");
}
}
};
}
/**
* Output all types of Deployers, Services and Models
*/
static async types() {
const webda = new WebdaServer(this.app);
await webda.init();
this.log("INFO", "Deployers:", Object.keys(this.app.getDeployers()).join(", "));
this.log("INFO", "Moddas:", Object.keys(this.app.getModdas()).join(", "));
this.log("INFO", "Models:", Object.keys(this.app.getModels())
.map(model => `${model} [${webda.getModelStore(webda.getModel(model)).getName()}]`)
.join(", "));
}
/**
* Return if a package is within minor version of each others
* @param package1
* @param package2
*/
static withinMinorVersion(package1, package2) {
return (semver.satisfies(package1.replace(/-.*/, ""), "^" + package2.replace(/-.*/, "")) ||
semver.satisfies(package2.replace(/-.*/, ""), "^" + package1.replace(/-.*/, "")));
}
static async handleCommandInternal(args, versions, output = undefined) {
// Arguments parsing
let parser = await this.parser(args);
let argv = parser.parse(args);
// Output version
if (argv.version) {
for (let v in versions) {
console.log(WebdaTerminal.webdaize(`${v}: ${versions[v].version}`));
}
return 0;
}
let extension;
await this.initLogger(argv);
// Init WorkerOutput
output = output || new WorkerOutput();
WebdaConsole.logger = new Logger(output, "console/webda");
// Only load extension if the command is unknown
if (!WebdaConsole.builtinCommands()[argv._[0]] || argv.help) {
WebdaConsole.loadExtensions(argv.appPath || process.cwd());
for (let cmd of Object.keys(this.extensions)) {
let ext = this.extensions[cmd];
// Dynamic we load from the extension as it is more complex
if (ext.yargs === "dynamic") {
parser = parser.command(ext.command || cmd, ext.description, (await import(path.join(ext.relPath, ext.require)))["yargs"]);
// Hybrid with builder
}
else if (ext.yargs && ext.yargs.command) {
parser = parser.command(ext.yargs);
}
else {
// Simple case
parser = parser.command(ext.command || cmd, ext.description, this.extensions[cmd].yargs);
}
}
argv = parser.parse(args);
extension = this.extensions[argv._[0]];
}
if (argv.help || argv._[0] === "help") {
this.displayHelp(parser);
return 0;
}
if (["deploy", "install", "uninstall"].indexOf(argv._[0]) >= 0) {
if (argv.deployment === undefined) {
this.output("Need to specify an environment");
return -1;
}
}
let logger;
if (argv.notty ||
process.env.NO_TTY ||
!process.stdout.isTTY ||
["init", "build", "openapi", "models", "operations", "diagram"].includes(argv._[0])) {
logger = new ConsoleLogger(output, argv.logLevel, argv.logFormat);
}
else {
if (extension && extension.terminal) {
// Allow override of terminal
this.terminal = new (await import(path.join(extension.relPath, extension.terminal))).default(output, versions, argv.logLevel, argv.logFormat);
}
else {
this.terminal = new WebdaTerminal(output, versions, undefined, argv.logLevel, argv.logFormat);
}
}
// Add SIGINT listener
if (WebdaConsole.onSIGINT) {
process.removeListener("SIGINT", WebdaConsole.onSIGINT);
}
WebdaConsole.onSIGINT = () => {
output.log("INFO", "Exiting on SIGINT");
WebdaConsole.stopDebugger();
if (this.webda) {
this.webda.stop();
}
if (this.terminal) {
this.terminal.close();
}
process.exit(0);
};
process.on("SIGINT", WebdaConsole.onSIGINT);
try {
// Display warning for versions mismatch
if (!this.withinMinorVersion(versions["@webda/core"].version, versions["@webda/shell"].version)) {
output.log("WARN", `Versions mismatch: @webda/core (${versions["@webda/core"].version}) and @webda/shell (${versions["@webda/shell"].version}) are not within minor versions`);
}
// Load Application
try {
if (argv._[0] === "build") {
// Avoid loading the local module as source might not exist yet
this.app = new BuildSourceApplication(argv.appPath, output);
}
else {
this.app = new SourceApplication(argv.appPath, output);
if (argv.noCompile || !this.requireCompilation()) {
this.app.preventCompilation(true);
}
else {
this.app.compile();
}
}
}
catch (err) {
output.log("WARN", err.message);
}
// Load deployment
if (argv.deployment) {
if (!this.app.hasDeployment(argv.deployment)) {
this.output(`Unknown deployment: ${argv.deployment}`);
return -1;
}
try {
this.app.setCurrentDeployment(argv.deployment);
// Try to load it already
this.app.getDeployment();
}
catch (err) {
this.log("ERROR", err.message);
return -1;
}
}
// Load webda module
if (this.app) {
await this.app.load();
}
// Update logo
if (this.app && this.app.getPackageWebda().logo && this.terminal) {
let logo = this.app.getPackageWebda().logo;
this.log("TRACE", "Updating logo", logo);
if (Array.isArray(logo)) {
this.terminal.setLogo(logo);
}
else if (typeof logo === "string") {
if (fs.existsSync(this.app.getAppPath(logo))) {
this.terminal.setLogo(fs.readFileSync(this.app.getAppPath(logo)).toString().split("\n"));
}
else {
this.log("WARN", "Cannot find logo", this.app.getAppPath(logo));
}
}
}
if (this.terminal && this.terminal.getLogo().length === 0) {
this.terminal.setDefaultLogo();
}
let result;
// Launch builtin commands
if (WebdaConsole.builtinCommands()[argv._[0]]) {
result = (await WebdaConsole.builtinCommands()[argv._[0]].handler.bind(this)(argv)) ?? 0;
}
else if (extension) {
this.log("DEBUG", "Launching extension " + argv._[0], extension);
// Load lib
argv._.shift();
result = await this.executeShellExtension(extension, extension.relPath, argv);
}
else {
// Display help if nothing is found
this.displayHelp(parser);
}
this.webda?.stop();
return result;
// Would need to create a fake app with a throw exception in a module to generate this
/* c8 ignore next 4 */
}
catch (err) {
this.log("ERROR", err);
throw err;
}
finally {
if (this.terminal) {
this.log("TRACE", "Closing terminal");
this.terminal.close();
}
if (logger) {
logger.close();
}
}
}
/**
* Display help for parser
*
* Separated into a method to allow override
* @param parser
*/
static displayHelp(parser) {
parser.showHelp(s => process.stdout.write(WebdaTerminal.webdaize(s)));
}
/**
*
* @param ext extension to execute
* @param relPath relative path of the extension
* @param argv arguments passed to the shell
*/
static async executeShellExtension(ext, relPath, argv) {
ext.export ?? (ext.export = "default");
return (await import(path.join(relPath, ext.require)))[ext.export](this, argv);
}
/**
* Display stores and their managed models
*/
static async stores() {
this.webda = new WebdaServer(this.app);
await this.webda.init();
const models = this.webda.getModels();
const stores = {};
for (let model in models) {
const name = this.webda.getModelStore(models[model]).getName();
stores[name] ?? (stores[name] = []);
stores[name].push(model);
}
Object.values(this.webda.getStores()).forEach(store => {
this.log("INFO", `Store ${store.getName()}: ${(stores[store.getName()] || [""]).join(", ")} (default:${store.getParameters().model})`);
});
return 0;
}
/**
* Manage store
* @param argv
*/
static async store(argv) {
this.webda = new WebdaServer(this.app);
await this.webda.init();
let store = this.webda.getService(argv.storeName);
if (!store) {
this.log("ERROR", `Store not found '${argv.storeName}'`);
return -1;
}
switch (argv.action) {
case "export":
let filepath = argv._[1] || "./export.ndjson.gz";
const writer = createGzip();
const fsWriter = fs.createWriteStream(filepath);
writer.pipe(fsWriter);
const batchSize = argv.batchSize || 100;
let continuationToken;
let count = 0;
do {
const result = await store.query(continuationToken ? `LIMIT ${batchSize} OFFSET '` + continuationToken + "'" : `LIMIT ${batchSize}`);
count += result.results.length;
this.log("INFO", "Exported", count, "models");
continuationToken = result.continuationToken;
for (const model of result.results) {
writer.write(JSON.stringify(model.toStoredJSON(), (_, value) => typeof value === "bigint" ? value.toString() : value) + "\n");
}
} while (continuationToken);
let p = new Promise(resolve => fsWriter.on("finish", resolve));
writer.end();
await p;
}
}
/**
* Generate the OpenAPI definition in a file
*
* If filename can end with .yml or .json to select the format
* @param argv
*/
static async generateOpenAPI(argv) {
this.webda = new WebdaServer(this.app);
this.webda.initStatics();
let openapi = this.webda.exportOpenAPI(!argv.includeHidden);
let name = argv.exportFile || "./openapi.json";
FileUtils.save(openapi, name);
}
/**
* Stop the debugger and wait for its complete stop
*/
static async stopDebugger() {
if (this.serverProcess) {
this.serverProcess.kill();
}
this.setDebuggerStatus(DebuggerStatus.Stopping);
this.app?.getCompiler()?.stopWatch();
this.setDebuggerStatus(DebuggerStatus.Stopped);
}
/**
* Get debugger current status
*/
static getDebuggerStatus() {
return this.debuggerStatus;
}
static setDebuggerStatus(status) {
this.debuggerStatus = status;
}
static output(...args) {
this.log("INFO", ...args);
}
static log(level, ...args) {
WebdaConsole.logger.log(level, ...args);
}
}
WebdaConsole.debuggerStatus = DebuggerStatus.Stopped;
WebdaConsole.onSIGINT = undefined;
WebdaConsole.extensions = {};
export default WebdaConsole;
export { WebdaConsole };
//# sourceMappingURL=webda.js.map