Skip to content

Commit

Permalink
Merge pull request #36 from tago-io/feat/cli-upload-plugin
Browse files Browse the repository at this point in the history
Plugin publish cli
  • Loading branch information
RicardoStoklosa authored Nov 18, 2022
2 parents 510cb6a + 563ad83 commit 7014f45
Show file tree
Hide file tree
Showing 17 changed files with 1,347 additions and 249 deletions.
431 changes: 422 additions & 9 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/tcore-console/src/Components/Icon/Icon.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Types of icons available in the system.
* Each one must match the corresponding filename in the /assets/icons folder.
*/
enum EIcon {
enum EIcon {
"alpine" = "alpine",
"apple" = "apple",
"arrow-alt-circle-down" = "arrow-alt-circle-down",
Expand Down
2 changes: 1 addition & 1 deletion packages/tcore-sdk/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module.exports = {
},
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint", "import"],
ignorePatterns: ['.eslintrc.js', 'types.js', 'types.d.ts'],
ignorePatterns: ['.eslintrc.js', 'types.js', 'types.d.ts', "__mocks__/*"],
extends: [
"plugin:prettier/recommended",
"plugin:@typescript-eslint/eslint-recommended",
Expand Down
34 changes: 21 additions & 13 deletions packages/tcore-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,42 +40,50 @@
],
"scripts": {
"build": "rm -rf ./build; tsc",
"dev": "ts-node src/Bin/Bin.ts",
"test": "TZ=UTC jest",
"test:watch": "TZ=UTC jest --watch",
"tsc:watch": "npm run watch",
"watch": "tsc --watch --preserveWatchOutput"
},
"dependencies": {
"cron-parser": "4.4.0",
"@tago-io/sdk": "10.9.4",
"chalk": "4.1.2",
"commander": "9.1.0",
"cron-parser": "4.4.0",
"glob": "7.2.0",
"image-size": "1.0.1",
"ini": "3.0.0",
"jszip": "3.10.1",
"luxon": "2.3.0",
"ora": "5.4.1",
"nanoid": "^3.1.32",
"uuid": "^8.3.2",
"zod": "3.13.4",
"tar": "6.1.11",
"glob": "7.2.0",
"chalk": "4.1.2",
"ora": "5.4.1",
"prompts": "2.4.2",
"semver": "7.3.5",
"image-size": "1.0.1"
"tar": "6.1.11",
"uuid": "^8.3.2",
"zod": "3.13.4"
},
"devDependencies": {
"@types/semver": "7.3.9",
"@types/commander": "2.12.2",
"@types/glob": "7.2.0",
"@types/tar": "6.1.1",
"@types/luxon": "2.0.9",
"@types/ini": "1.3.31",
"@types/jest": "27.0.1",
"@types/luxon": "2.0.9",
"@types/node": "16.7.1",
"@types/prompts": "2.4.1",
"@types/semver": "7.3.9",
"@types/tar": "6.1.1",
"@types/uuid": "^8.3.1",
"@typescript-eslint/eslint-plugin": "5.14.0",
"@typescript-eslint/parser": "5.14.0",
"@typescript-eslint/parser": "5.14.0",
"eslint": "7.32.0",
"eslint-config-prettier": "8.3.0",
"eslint-import-resolver-typescript": "2.4.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-prettier": "3.4.1",
"jest": "27.0.6",
"memfs": "3.4.11",
"prettier": "2.3.2",
"ts-jest": "27.0.5",
"typescript": "4.3.5"
Expand Down
251 changes: 29 additions & 222 deletions packages/tcore-sdk/src/Bin/Bin.ts
Original file line number Diff line number Diff line change
@@ -1,233 +1,40 @@
#!/usr/bin/env node

import fs from "fs";
import path from "path";
import crypto from "crypto";
import chalk from "chalk";
import ora from "ora";
import tar from "tar";
import glob from "glob";
import semver from "semver";
import { program } from "commander";
import getImageData from "image-size";
import { z } from "zod";
import { zPluginPermission, zPluginType } from "../Types";
import { login } from "./Commands/Login";
import { logout } from "./Commands/Logout";
import { pack } from "./Commands/Pack";
import { publish } from "./Commands/Publish";
import { whoAmI } from "./Commands/WhoAmI";

const cwd = process.cwd();
const pkg = require(path.join(cwd, "package.json"));
const pkgName = pkg.name.replace("@", "").replace(/\//g, "-");
program.command("whoami").description("display account name").action(whoAmI);

/**
* Glob of patterns to ignore. Acquired from the .tcoreignore file.
*/
let tcoreIgnore: string[] = [];
program.command("logout").description("logs out of the plugin store").action(logout);

/**
* The name of the output file.
*/
let outputFile = `${pkgName}-${pkg.version}.tcore`;

/**
* Formats the bytes into a more readable format.
*/
function formatBytes(bytes: number) {
if (bytes === 0) {
return "0 B";
}

const k = 1024;
const dm = 2;
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
}

/**
*/
function getSha256OutputFile() {
return new Promise((resolve) => {
const stream = crypto.createHash("sha1").setEncoding("hex");
fs.createReadStream(path.join(cwd, outputFile))
.pipe(stream)
.on("finish", () => resolve(stream.read()));
});
}

/**
*/
async function printDetails(spinner: ora.Ora, amountOfFiles: number) {
const filePath = path.join(cwd, outputFile);
const stat = await fs.promises.stat(filePath).catch(() => null);
const size = stat?.size || 0;

console.log(`${chalk.magentaBright("[TCore SDK]")} ${chalk.magenta(`====== Details ======`)}`);

const shasum = await getSha256OutputFile();

spinner.succeed(`name: ${pkg.name}`);
spinner.succeed(`version: ${pkg.version}`);
spinner.succeed(`filename: ${outputFile}`);
spinner.succeed(`size: ${formatBytes(size)}`);
spinner.succeed(`shasum: ${shasum}`);
spinner.succeed(`files: ${amountOfFiles}`);
}

/**
*/
async function pack(spinner: ora.Ora): Promise<number> {
const files: string[] = [];

if (pkg.files) {
for (const item of pkg.files) {
const ignore = [outputFile, ".DS_Store", ...tcoreIgnore];
const globFiles = glob.sync(item, { cwd, ignore, dot: true });
files.push(...globFiles);
}
} else {
const ignore = [outputFile, ".gitignore", "node_modules", "node_modules/**", ".git", ".git/**/*", ...tcoreIgnore];
const globFiles = glob.sync("**/*", { cwd, ignore, dot: true });
files.push(...globFiles);
}

if (!files.includes("package.json")) {
// package.json is necessary, cannot remove it
files.push("package.json");
}

const fd = pkg.tcore?.full_description || "";
if (fd) {
// full description is necessary, cannot remove it
files.push(fd);
}

let amount = 0;

for (const file of files) {
const stat = fs.statSync(path.join(cwd, file));
const size = stat.size;
if (!stat.isDirectory()) {
spinner.succeed(`Added ${chalk.cyan(file)} (${formatBytes(size)})`);
amount++;
}
}

spinner.start("Generating .tcore file");

return await new Promise<number>((resolve, reject) => {
const opts = {
cwd,
gzip: true,
file: path.join(cwd, outputFile),
};

tar.c(opts, files, (err) => {
if (err) {
reject(err);
} else {
spinner.succeed("Generated .tcore file");
resolve(amount);
}
});
});
}

/**
*/
function validate(spinner: ora.Ora): boolean {
let error = false;

const setMessage = (m: string) => {
spinner.fail(chalk.redBright(m));
error = true;
};

// ------------------------------
// engine, not required but must be a valid semver if present
const engine = pkg.engines?.tcore;
if (engine && !semver.validRange(engine)) {
setMessage(`'package.engines.tcore' has an invalid TCore range (${engine})`);
}

// ------------------------------
// types must exist and have valid values
const types = pkg.tcore?.types || [];
if (types.length === 0) {
setMessage("'package.tcore.types' should contain at least one module type");
}
if (!z.array(zPluginType).safeParse(types).success) {
setMessage("'package.tcore.types' contains one or more invalid values");
}

// ------------------------------
// permissions must have valid values if it exists
const permissions = pkg.tcore?.permissions || [];
if (!z.array(zPluginPermission).safeParse(permissions).success) {
setMessage("'package.tcore.permissions' contains one or more invalid values");
}

// ------------------------------
// icon validation, required
const icon = pkg.tcore?.icon || "";
const iconPath = path.join(cwd, icon);
const iconExists = fs.existsSync(iconPath);
if (icon && iconExists) {
const data = getImageData(iconPath);
const ratio = ((data.width || 0) / (data.height || 1)).toFixed(2);
const valid = data.type === "png" && ratio === "1.35";
if (!valid) {
setMessage(
"'package.tcore.icon' should be a PNG image with aspect ratio of 50:37 (width 1.35x larger then height)"
);
}
} else {
setMessage("'package.tcore.icon' file not found");
}

// ------------------------------
// full description, not required
const fd = pkg.tcore?.full_description || "";
const fdPath = path.join(cwd, fd);
const fdExists = fs.existsSync(fdPath);
if (fd && !fdExists) {
setMessage("'package.tcore.full_description' file not found");
}

return error;
}

/**
*/
async function generate(opts: any) {
tcoreIgnore = await fs.promises
.readFile(path.join(cwd, ".tcoreignore"), "utf-8")
.then((r) => r.split("\n").filter((x) => x))
.catch(() => []);

const spinner = ora("Validating package.json");
spinner.prefixText = chalk.magentaBright("[TCore SDK]");

try {
const error = validate(spinner);
if (error && !opts.force) {
spinner.fail("Process aborted due to errors");
return;
}
if (opts?.output) {
outputFile = opts.output;
}

const amountOfFiles = await pack(spinner);
await printDetails(spinner, amountOfFiles);
} catch (ex: any) {
spinner.fail(`${spinner.text} - ${chalk.redBright(ex?.message || ex)}`);
}
}
program
.command("login")
.option("-u, --email <value>", "Email of your TagoIO Account")
.option("-p, --password <value>", "Password of your TagoIO Account")
.description("logs into the plugin store")
.action(login);

program
.command("pack")
.option("-f, --force", "Forces creation even with errors")
.option("-o, --output <value>", "Set the output name of the file")
.description("Creates a .tcore file from a package")
.action(generate);
.option("--filename <value>", "Name of the packaged file")
.option("-f, --force", "Forces pack even with errors")
.option("-t, --target <id...>", "Target to pack for")
.option("-o, --out <path>", "Output folder")
.description("creates a .tcore file from a package")
.action(pack);

program
.command("publish")
.option("-f, --force", "Forces pack even with errors")
.option("-o, --out <path>", "Output folder")
.option("-p, --publisher <profile_id>", "ID of the TagoIO Profile to be used as the publisher")
.option("-v, --visible <boolean>", "Boolean to indicate if the plugin should be publicly visible or not")
.option("--only-publish", "Don't pack, only publish existing .tcore files")
.description("publishes a package to the Plugin Store")
.action(publish);

program.parse();
Loading

0 comments on commit 7014f45

Please sign in to comment.