@leanix/reporting-cli
Version:
Command line interface to develop custom reports for LeanIX EAM
621 lines (596 loc) • 23.1 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __async = (__this, __arguments, generator) => {
return new Promise((resolve3, reject) => {
var fulfilled = (value) => {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
};
var rejected = (value) => {
try {
step(generator.throw(value));
} catch (e) {
reject(e);
}
};
var step = (x) => x.done ? resolve3(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
step((generator = generator.apply(__this, __arguments)).next());
});
};
// src/app.ts
var import_axios3 = require("axios");
var import_chalk6 = __toESM(require("chalk"));
var import_commander = require("commander");
// src/builder.ts
var import_chalk = __toESM(require("chalk"));
// src/async.helpers.ts
var import_node_child_process = require("child_process");
var import_node_fs = require("fs");
var import_node_util = require("util");
var import_rimraf = require("rimraf");
var execAsync = (0, import_node_util.promisify)(import_node_child_process.exec);
var writeFileAsync = (0, import_node_util.promisify)(import_node_fs.writeFile);
var rimrafAsync = import_rimraf.rimraf;
// src/builder.ts
var Builder = class {
constructor(logger) {
this.logger = logger;
}
build(distPath, buildCommand) {
return __async(this, null, function* () {
this.logger.log(import_chalk.default.yellow(import_chalk.default.italic("Building...")));
try {
yield rimrafAsync(distPath);
const { stdout } = yield execAsync(buildCommand);
this.logger.log(stdout);
this.logger.log(import_chalk.default.green("\u2713 Project successfully build!"));
} catch (error) {
this.logger.error(error);
}
});
}
};
// src/bundler.ts
var fs = __toESM(require("fs"));
var import_node_path3 = require("path");
var tar = __toESM(require("tar"));
// src/file.helpers.ts
var import_node_fs2 = require("fs");
var import_node_path2 = require("path");
// src/path.helpers.ts
var import_node_path = require("path");
function getProjectDirectoryPath(path = "") {
return (0, import_node_path.resolve)(process.cwd(), path);
}
function getTemplateDirectoryPath() {
return (0, import_node_path.join)(__dirname, "../template");
}
// src/file.helpers.ts
function readJsonFile(path) {
const buffer = (0, import_node_fs2.readFileSync)(path);
return JSON.parse(buffer.toString("utf-8"));
}
function loadLxrConfig() {
const lxrConfigPath = getProjectDirectoryPath("lxr.json");
return readJsonFile(lxrConfigPath);
}
function loadPackageJson() {
const packageJsonPath = getProjectDirectoryPath("package.json");
return readJsonFile(packageJsonPath);
}
var defaultBuildCmd = (0, import_node_path2.join)(...[".", "node_modules", ".bin", "webpack"]);
var defaultDistPath = "dist";
function loadCliConfig(packageJson2 = loadPackageJson()) {
var _a, _b;
const leanixReportingCli = packageJson2.leanixReportingCli || {};
return {
distPath: (_a = leanixReportingCli.distPath) != null ? _a : defaultDistPath,
buildCommand: (_b = leanixReportingCli.buildCommand) != null ? _b : defaultBuildCmd
};
}
// src/bundler.ts
var Bundler = class {
bundle(distPath) {
return __async(this, null, function* () {
yield this.writeMetadataFile(distPath);
yield this.createTarFromDistFolder(distPath);
});
}
writeMetadataFile(distPath) {
const packageJson2 = loadPackageJson();
const metadataFile = (0, import_node_path3.join)(distPath, "lxreport.json");
const metadata = Object.assign(
{},
{
name: packageJson2.name,
version: packageJson2.version,
author: this.getReportAuthor(packageJson2.author),
description: packageJson2.description,
documentationLink: packageJson2.documentationLink
},
packageJson2.leanixReport
);
return writeFileAsync(metadataFile, JSON.stringify(metadata));
}
getReportAuthor(author) {
if (typeof author === "string") {
return author.replace("<", "(").replace(">", ")");
} else {
return `${author.name} (${author.email})`;
}
}
createTarFromDistFolder(distPath) {
const files = fs.readdirSync(distPath);
return tar.c({ gzip: true, cwd: distPath, file: "bundle.tgz" }, files);
}
};
// src/dev-starter.ts
var import_node_path4 = require("path");
var import_chalk2 = __toESM(require("chalk"));
var import_cross_spawn = require("cross-spawn");
var import_opn = __toESM(require("opn"));
// src/api-token-resolver.ts
var import_axios = __toESM(require("axios"));
// src/helpers.ts
var import_https_proxy_agent = require("https-proxy-agent");
var import_jwt_decode = require("jwt-decode");
var import_zod = __toESM(require("zod"));
var jwtClaimsPrincipalSchema = import_zod.default.object({
id: import_zod.default.string(),
username: import_zod.default.string(),
role: import_zod.default.string(),
status: import_zod.default.string(),
account: import_zod.default.object({ id: import_zod.default.string(), name: import_zod.default.string() }),
permission: import_zod.default.object({
id: import_zod.default.string(),
workspaceId: import_zod.default.string(),
workspaceName: import_zod.default.string(),
role: import_zod.default.string(),
status: import_zod.default.string()
})
});
var jwtClaimsSchema = import_zod.default.object({
sub: import_zod.default.string(),
principal: jwtClaimsPrincipalSchema,
iss: import_zod.default.url(),
jti: import_zod.default.string(),
exp: import_zod.default.number(),
instanceUrl: import_zod.default.url(),
region: import_zod.default.string()
});
var getJwtClaims = (bearerToken) => {
let claims;
let claimsUnchecked;
try {
claimsUnchecked = (0, import_jwt_decode.jwtDecode)(bearerToken);
claims = jwtClaimsSchema.parse(claimsUnchecked);
} catch (err) {
if (err instanceof import_jwt_decode.InvalidTokenError) {
console.error("could not decode jwt token", err);
}
if (err instanceof import_zod.ZodError) {
console.error("unexpected jwt claims schema", err.message);
}
throw err;
}
return claims;
};
var getAxiosProxyConfiguration = (proxyConfig) => {
const httpsAgent = new import_https_proxy_agent.HttpsProxyAgent(proxyConfig);
return httpsAgent;
};
// src/api-token-resolver.ts
var ApiTokenResolver = class {
static getAccessToken(host, apiToken, proxy) {
return __async(this, null, function* () {
const config = {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": `Basic ${Buffer.from(`apitoken:${apiToken}`).toString("base64")}`
}
};
if (proxy) {
config.httpsAgent = getAxiosProxyConfiguration(proxy);
}
const url = `${host}/services/mtm/v1/oauth2/token?grant_type=client_credentials`;
const accessToken = yield import_axios.default.post(url, {}, config).then((response) => response.data.access_token);
return accessToken;
});
}
};
// src/dev-starter.ts
var DevStarter = class {
start() {
return __async(this, null, function* () {
const config = loadLxrConfig();
const accessToken = yield this.getAccessToken(config);
const result = yield this.startLocalServer(config, accessToken);
this.openUrlInBrowser(result.launchUrl);
console.log(`${import_chalk2.default.green(`Open the following url to test your report:
${result.launchUrl}`)}
`);
console.log(
import_chalk2.default.yellow(
`If your report is not being loaded, please check if it opens outside of LeanIX via this url:
${result.localhostUrl}`
)
);
});
}
startLocalServer(config, accessToken) {
return __async(this, null, function* () {
const port = config.localPort || 8080;
const localhostUrl = `https://localhost:${port}`;
const urlEncoded = encodeURIComponent(localhostUrl);
const claims = getJwtClaims(accessToken);
const instanceUrl = claims.instanceUrl;
const workspace = claims.principal.permission.workspaceName;
console.log(import_chalk2.default.green(`Your workspace is ${workspace}`));
const launchUrl = new URL(`${instanceUrl}/${workspace}/reports/dev?url=${urlEncoded}`);
launchUrl.hash = `access_token=${accessToken}`;
console.log(import_chalk2.default.green(`Starting development server and launching with url: ${launchUrl}`));
const wpMajorVersion = yield this.getCurrentWebpackMajorVersion();
const args = ["--port", `${port}`];
if (wpMajorVersion < 5) {
args.push("--https");
}
if (config.ssl && config.ssl.cert && config.ssl.key) {
args.push(`--cert=${config.ssl.cert}`);
args.push(`--key=${config.ssl.key}`);
}
console.log(`${args.join(" ")}`);
let projectRunning = false;
const webpackCmd = (0, import_node_path4.join)(
...wpMajorVersion === 5 ? ["node_modules", ".bin", "webpack"] : ["node_modules", ".bin", "webpack-dev-server"]
);
const serverProcess = wpMajorVersion === 5 ? (0, import_cross_spawn.spawn)(webpackCmd, ["serve", ...args]) : (0, import_cross_spawn.spawn)(webpackCmd, args);
serverProcess.stdout.on("data", (data) => {
console.log(data.toString());
});
serverProcess.stderr.on("data", (data) => {
const output = data.toString();
if (output.includes("Project is running")) {
projectRunning = true;
}
console.error(import_chalk2.default.red(data.toString()));
});
return new Promise((resolve3) => {
serverProcess.on("error", (err) => {
console.error(err);
});
serverProcess.stdout.on("data", (data) => {
const output = data.toString();
if (output.includes("Project is running")) {
projectRunning = true;
}
if (projectRunning && output.toLowerCase().includes("compiled successfully")) {
resolve3({ launchUrl: launchUrl.toString(), localhostUrl });
}
});
});
});
}
getCurrentWebpackMajorVersion() {
return new Promise((resolve3) => {
const webpackVersion = (0, import_cross_spawn.spawn)((0, import_node_path4.join)(...["node_modules", ".bin", "webpack"]), ["-v"]);
webpackVersion.stdout.on("data", (data) => {
const output = data.toString();
const matches = output.match(/(\d+)\.\d+\.\d+/);
const majorVersion = Number.parseInt(matches[0]);
resolve3(majorVersion);
});
});
}
getAccessToken(config) {
return __async(this, null, function* () {
if (!config.apitoken) {
throw new Error("no api token provided, please include it in the lxr.json file");
}
const token = yield ApiTokenResolver.getAccessToken(`https://${config.host}`, config.apitoken, config.proxyURL);
return token;
});
}
openUrlInBrowser(url) {
try {
(0, import_opn.default)(url);
} catch (err) {
console.error(`Unable to open your browser: ${err}`);
}
}
};
// src/initializer.ts
var import_node_process = require("process");
var import_chalk4 = __toESM(require("chalk"));
var inquirer = __toESM(require("inquirer"));
// src/template-extractor.ts
var fs2 = __toESM(require("fs"));
var import_node_path5 = require("path");
var import_chalk3 = __toESM(require("chalk"));
var import_ejs = require("ejs");
var import_mkdirp = require("mkdirp");
var TemplateExtractor = class {
extractTemplateFiles(baseTemplateDir, answers) {
console.log(import_chalk3.default.green("Extracting template files..."));
this.extractTemplateDir(baseTemplateDir, baseTemplateDir, answers);
}
extractTemplateDir(templateDir, baseTemplateDir, answers) {
fs2.readdirSync(templateDir).forEach((file) => {
const filePath = (0, import_node_path5.resolve)(templateDir, file);
const isDir = fs2.lstatSync(filePath).isDirectory();
if (isDir) {
this.extractTemplateDir(filePath, baseTemplateDir, answers);
} else {
this.extractTemplateFile(filePath, baseTemplateDir, answers);
}
});
}
extractTemplateFile(sourcePath, baseTemplateDir, answers) {
const destPath = sourcePath.replace(baseTemplateDir, getProjectDirectoryPath()).replace(/\.ejs$/, "");
console.log(sourcePath, destPath);
const template = fs2.readFileSync(sourcePath).toString("utf-8");
const result = (0, import_ejs.render)(template, answers);
(0, import_mkdirp.sync)((0, import_node_path5.dirname)(destPath));
fs2.writeFileSync(destPath, result);
}
};
// src/initializer.ts
var Initializer = class {
constructor() {
this.extractor = new TemplateExtractor();
}
init() {
console.log(import_chalk4.default.green("Initializing new project..."));
return inquirer.prompt(this.getInquirerQuestions()).then((answers) => {
answers.nodeVersion = import_node_process.versions.node;
this.extractor.extractTemplateFiles(getTemplateDirectoryPath(), answers);
console.log(import_chalk4.default.green("\u2713 Your project is ready!"));
console.log(import_chalk4.default.green("Please run `npm install` to install dependencies and then run `npm start` to start developing!"));
});
}
getInquirerQuestions() {
return [
{
type: "input",
name: "name",
message: "Name of your project for package.json"
},
{
type: "input",
name: "id",
message: "Unique id for this report in Java package notation (e.g. net.leanix.barcharts)"
},
{
type: "input",
name: "author",
message: "Who is the author of this report (e.g. LeanIX GmbH)"
},
{
type: "input",
name: "title",
message: "A title to be shown in LeanIX when report is installed"
},
{
type: "input",
name: "description",
message: "Description of your project"
},
{
type: "input",
name: "licence",
default: "UNLICENSED",
message: "Which licence do you want to use for this project?"
},
{
type: "input",
name: "host",
default: "app.leanix.net",
message: "Which host do you want to work with?"
},
{
type: "input",
name: "apitoken",
message: "API-Token for Authentication (see: https://dev.leanix.net/docs/authentication#section-generate-api-tokens)"
},
{
type: "confirm",
name: "behindProxy",
message: "Are you behind a proxy?",
default: false
},
{
when: (answers) => answers.behindProxy,
type: "input",
name: "proxyURL",
message: "Proxy URL?"
}
];
}
};
// src/uploader.ts
var import_node_fs3 = __toESM(require("fs"));
var import_axios2 = __toESM(require("axios"));
var import_chalk5 = __toESM(require("chalk"));
var import_form_data = __toESM(require("form-data"));
var Uploader = class {
upload(params) {
return __async(this, null, function* () {
const { tokenhost, apitoken, proxyURL, store } = params;
const accessToken = yield ApiTokenResolver.getAccessToken(`https://${tokenhost}`, apitoken, proxyURL);
const claims = getJwtClaims(accessToken);
let url;
if (store) {
const { host = "store.leanix.net", assetVersionId } = store;
url = `https://${host}/services/torg/v1/assetversions/${assetVersionId}/payload`;
} else {
url = `${claims.instanceUrl}/services/pathfinder/v1/reports/upload`;
}
console.log(import_chalk5.default.yellow(import_chalk5.default.italic(`Uploading to ${url} ${proxyURL ? `through a proxy` : ``}...`)));
const formData = new import_form_data.default();
formData.append("file", import_node_fs3.default.createReadStream(getProjectDirectoryPath("bundle.tgz")));
const options = {
headers: __spreadValues({
Authorization: `Bearer ${accessToken}`
}, formData.getHeaders())
};
if (proxyURL) {
options.httpsAgent = getAxiosProxyConfiguration(proxyURL);
}
try {
const response = yield import_axios2.default.post(url.toString(), formData, options);
if (response.data.status === "OK") {
console.log(import_chalk5.default.green("\u2713 Project successfully uploaded!"));
return true;
} else if (response.data.status === "ERROR") {
console.log(import_chalk5.default.red(`ERROR: ${response.data.errorMessage}`));
return false;
}
} catch (err) {
if (err.response && err.response.data && err.response.data.errorMessage) {
console.log(import_chalk5.default.red(`ERROR: ${err.response.data.errorMessage}`));
} else {
console.log(import_chalk5.default.red(`ERROR: ${err.message}`));
}
return false;
}
});
}
};
// src/version.ts
var import_node_path6 = require("path");
var packageJson = readJsonFile((0, import_node_path6.join)(__dirname, "..", "package.json"));
var version = packageJson.version;
// src/app.ts
var program = new import_commander.Command();
program.version(version);
program.command("init").description("Initializes a new project").action(() => {
new Initializer().init().catch(handleError);
});
program.command("start").description("Start developing and testing your report").action(() => {
new DevStarter().start().catch(handleError);
});
program.command("build").description(`Builds the report`).on("--help", () => {
console.log(`
By default, the report will be built by running "node_modules/.bin/webpack".
Before the build, the dist folder ("dist" by default) will be deleted to
ensure a clean build.
These defaults can be changed by setting "distPath" and "buildCommand" in the
"leanixReportingCli" section of package.json:
{
"leanixReportingCli": {
"distPath": "output",
"buildCommand": "make"
}
}
Please note that the value provided for "distPath" needs to be aligned with
configuration of the given build command. E.g., in the example above, "make"
would need to configured in a way that it writes the report artefacts to the
"output" folder.`);
}).action(() => __async(null, null, function* () {
const cliConfig = loadCliConfig();
const builder = new Builder(console);
try {
yield builder.build(cliConfig.distPath, cliConfig.buildCommand);
} catch (error) {
console.error(import_chalk6.default.red(error));
}
}));
program.command("upload").description("Uploads the report to the configured workspace").on("--help", () => {
console.log(`
Before uploading, the report will be built \u2013 see "lxr help build" for details.
The report will be uploaded to the workspace associated with the "apitoken" on
the "host" given in lxr.json.`);
}).action(() => __async(null, null, function* () {
const cliConfig = loadCliConfig();
const lxrConfig = loadLxrConfig();
const { host: tokenhost, apitoken, proxyURL } = lxrConfig;
const builder = new Builder(console);
const bundler = new Bundler();
const uploader = new Uploader();
console.log(import_chalk6.default.yellow(import_chalk6.default.italic("Bundling and uploading your project...")));
try {
yield builder.build(cliConfig.distPath, cliConfig.buildCommand);
yield bundler.bundle(cliConfig.distPath);
yield uploader.upload({ tokenhost, apitoken, proxyURL });
} catch (error) {
handleError(error);
}
}));
program.command("store-upload <id> <apitoken>").description("Uploads the report to the LeanIX Store").requiredOption("--tokenhost <tokenhost>", "Where to resolve the apitoken (default: app.leanix.net)").option("--host <host>", "Which store to use (default: store.leanix.net)").action((assetVersionId, apitoken, options) => __async(null, null, function* () {
const cliConfig = loadCliConfig();
const host = options.host || "store.leanix.net";
const builder = new Builder(console);
const bundler = new Bundler();
const uploader = new Uploader();
console.log(import_chalk6.default.yellow(`Bundling and uploading your project to the LeanIX Store (${host})...`));
try {
yield builder.build(cliConfig.distPath, cliConfig.buildCommand);
yield bundler.bundle(cliConfig.distPath);
yield uploader.upload({ apitoken, tokenhost: options.tokenhost, store: { assetVersionId, host } });
} catch (error) {
handleError(error);
}
}));
program.parse(process.argv);
if (process.argv.length === 2) {
console.log(import_chalk6.default.cyan(" LeanIX Reporting CLI"));
console.log(import_chalk6.default.cyan(" ===================="));
console.log("");
console.log(import_chalk6.default.cyan(` version: ${version}`));
console.log(import_chalk6.default.cyan(" github: https://github.com/leanix/leanix-reporting-cli"));
console.log("");
program.outputHelp();
}
function handleError(err) {
if (err instanceof import_axios3.AxiosError) {
if (err.status) {
if (err.status === 401) {
console.error("Invalid API token");
} else {
console.error(`${err.status}: ${JSON.stringify(err.response.data)}`);
}
} else {
console.error(`${err.cause}: ${err.code}`);
}
} else if (err instanceof Error) {
console.error(import_chalk6.default.red(err.message));
} else {
console.error(import_chalk6.default.red(err));
}
}