Skip to content

Commit

Permalink
Merge pull request #2 from UnderKoen/feature/interactive
Browse files Browse the repository at this point in the history
  • Loading branch information
UnderKoen authored Jan 16, 2024
2 parents 03916bd + 56a81c6 commit 3757353
Show file tree
Hide file tree
Showing 21 changed files with 2,761 additions and 2,801 deletions.
7 changes: 7 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ module.exports = {
],
rules: {
"@typescript-eslint/no-throw-literal": "off",
"@typescript-eslint/no-extraneous-class": "off",
"@typescript-eslint/no-unused-vars": [
"error", // or "error"
{
argsIgnorePattern: "^_",
},
],
},
},
],
Expand Down
14 changes: 14 additions & 0 deletions esbuild.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const prod = process.env.PROD === "TRUE";

require("esbuild").build({
entryPoints: ["./src/index.ts"],
bundle: true,
platform: "node",
target: "node16",
outfile: "dist/index.js",
define: {
"process.env.NODE_ENV": '"production"',
},
minify: prod,
sourcemap: !prod,
});
4,412 changes: 1,867 additions & 2,545 deletions package-lock.json

Large diffs are not rendered by default.

35 changes: 18 additions & 17 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,29 +31,30 @@
"test:cov": "bsm test.cov"
},
"devDependencies": {
"@inquirer/core": "^5.1.1",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/exec": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@semantic-release/github": "^8.0.7",
"@types/minimist": "^1.2.2",
"@types/node": "^20.5.9",
"@types/sinon": "^10.0.16",
"@typescript-eslint/eslint-plugin": "^6.6.0",
"@typescript-eslint/parser": "^6.6.0",
"@under_koen/bsm": "^1.1.1",
"c8": "^8.0.1",
"ci-info": "^3.8.0",
"defu": "^6.1.2",
"esbuild": "^0.19.2",
"eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0",
"@semantic-release/github": "^9.2.6",
"@types/minimist": "^1.2.5",
"@types/node": "^20.11.4",
"@types/sinon": "^17.0.3",
"@typescript-eslint/eslint-plugin": "^6.19.0",
"@typescript-eslint/parser": "^6.19.0",
"@under_koen/bsm": "^1.2.5",
"c8": "^9.1.0",
"ci-info": "^4.0.0",
"defu": "^6.1.4",
"esbuild": "^0.19.11",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"minimist": "^1.2.8",
"prettier": "3.0.3",
"prettier": "^3.2.2",
"prettier-package-json": "^2.8.0",
"semantic-release": "^21.0.1",
"sinon": "^15.2.0",
"semantic-release": "^23.0.0",
"sinon": "^17.0.1",
"tsm": "^2.3.0",
"typescript": "^5.2.2",
"typescript": "^5.3.3",
"uvu": "^0.5.6"
},
"keywords": [
Expand Down
28 changes: 21 additions & 7 deletions package.scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@ module.exports = {
$description: "Build the project, has options for prod, dev, and watch",
_default: {
_pre: "rimraf ./dist",
_default:
"esbuild src/index.ts --bundle --platform=node --target=node16 --outfile=dist/index.js",
_default: "node esbuild.config.js",
},
prod: "bsm ~ -- --minify",
dev: "bsm ~ -- --sourcemap",
watch: "bsm ~.dev -- --watch",
prod: {
//TODO single line $env
$env: {
PROD: "TRUE",
},
_default: "bsm build",
},
//TODO
watch: "bsm ~ -- --watch",
},
prettier: {
$description: "Run all formatters",
Expand All @@ -26,15 +31,24 @@ module.exports = {
lint: {
$description: "Run all linters",
_default: "bsm ~.*",
typescript: "tsc --noEmit",
eslint: "eslint --ext .ts,.js .",
prettier: "prettier --check .",
},
test: {
_default: "uvu -r tsm test",
cov: "c8 bsm ~",
$env: {
TEST: "TRUE",
NODE_ENV: "test",
},
_pre: "bsm build",
_default: "uvu -r tsm test -i fixtures",
cov: "c8 bsm ~ --",
},
env: () => {
console.log(process.env);
},
},
config: {
defaultHelpBehavior: "interactive",
},
};
109 changes: 109 additions & 0 deletions src/ConfigLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { ExtendConfig, TConfig } from "./types";
import minimist from "minimist";
import { defu } from "defu";

type AdvanceConfig = (...args: unknown[]) => TConfig;

export const DEFAULT_CONFIG: Readonly<TConfig> = Object.freeze({
scripts: {},
config: {
defaultHelpBehavior: "help",
},
} as TConfig);

export class ConfigLoader {
static getSource(config: ExtendConfig): string {
if (Array.isArray(config)) return config[0];
return config;
}

static loadExtensions(config: TConfig): TConfig[] {
if (!Object.hasOwn(config, "extends") || !config.extends) return [];

return [...config.extends]
.map((c) => this.loadFile(c, false))
.filter((c): c is TConfig => c !== undefined);
}

static loadFile(
file: ExtendConfig | undefined,
noBail = true,
): TConfig | undefined {
if (!file) return undefined;
let source = this.getSource(file);

try {
source = require.resolve(source, {
paths: [process.cwd()],
});

// eslint-disable-next-line @typescript-eslint/no-var-requires
let config = require(source) as TConfig | AdvanceConfig;
if (typeof config === "function") {
config = config(...(Array.isArray(file) ? file.slice(1) : []));
}

const configs = this.loadExtensions(config);

return defu(config, ...configs, DEFAULT_CONFIG);
} catch (e) {
const error = e as NodeJS.ErrnoException & { requireStack?: string[] };

const path = error.requireStack?.[0]?.replaceAll("\\", "/");

if (
(error?.code === "MODULE_NOT_FOUND" &&
path?.includes("bsm/dist/index.js")) ||
// For when running tests or with ts-node
(process.env.NODE_ENV !== "production" &&
path?.includes("bsm/src/ConfigLoader.ts"))
) {
if (noBail) return undefined;

console.error(
`\x1b[31mCannot find config '${source}' to extend\x1b[0m`,
);
process.exit(1);
} else {
console.error(e);

if (Array.isArray(file)) file = file[0];
console.error(`\x1b[31mError loading config '${file}'\x1b[0m`);
process.exit(1);
}
}
}

static load(argv: minimist.ParsedArgs): TConfig {
const possibleConfigFiles: string[] = [
argv.config,
process.env.BSM_CONFIG,
"./package.scripts.js",
"./package.scripts.json",
].filter((s): s is string => s !== undefined);

for (const file of possibleConfigFiles) {
const config = this.loadFile(file);
if (!config) continue;

process.env.BSM_CONFIG = file;
this._config = config;
return config;
}

console.error(
`\x1b[31mCannot find config ${possibleConfigFiles
.filter((s): s is string => s !== undefined)
.map((s) => `'${s}'`)
.join(" or ")}\x1b[0m`,
);
process.exit(1);
}

private static _config: TConfig | undefined;

static get config(): TConfig {
if (this._config) return this._config;
throw new Error("Config not loaded");
}
}
76 changes: 58 additions & 18 deletions src/executor.ts → src/Executor.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import { TError, TFunction, TScript, TScripts } from "./types";
import child_process from "node:child_process";
import { isCI } from "ci-info";
import { Help } from "./help";
import { Help } from "./Help";
import path from "path";
import fs from "fs";
import { Interactive } from "./Interactive";
import { ConfigLoader } from "./ConfigLoader";

type Options = {
excludeArgs?: true;
ignoreNotFound?: true;
env?: Record<string, string>;
};

// eslint-disable-next-line @typescript-eslint/no-extraneous-class
class Executor {
static async run(script: string): Promise<void> {
const config = ConfigLoader.config;
await Executor.runScript(config.scripts, script.split("."), [], {});
}

static async runScript(
context: TScript,
script: string[],
Expand All @@ -30,39 +36,52 @@ class Executor {
await Executor.executeObject(context, script, path, options);
}
} else {
Executor.notFound(path, options);
// TODO support for other types
// TODO improve not found message
await Executor.notFound(path, options);
}
}

static notFound(
static async notFound(
path: string[],
options: Options,
context?: TScripts,
): never | void {
context?: TScripts | TScript[],
): Promise<void> {
if (options.ignoreNotFound) return;

if (context) {
const sub = path[path.length - 1];
const rest = path.slice(0, -1);

if (rest.length === 0) {
console.error(`\x1b[31mScript '${sub}' does not exist\x1b[0m\n`);
console.error(`\x1b[31mScript '${sub}' does not exist\x1b[0m`);
} else {
console.error(
`\x1b[31mScript '${rest.join(
".",
)}' does not have a '${sub}' script\x1b[0m\n`,
)}' does not have a '${sub}' script\x1b[0m`,
);
}

if (Object.hasOwn(context, "$description")) {
Help.printCommand(context, rest);
if (ConfigLoader.config.config?.defaultHelpBehavior === "interactive") {
const scripts = await Interactive.selectScript(ConfigLoader.config, {
_: [path.join(".")],
});

//TODO: support for multiple scripts (currently Interactive only supports one)
await this.run(scripts[0]);
process.exit(0);
} else {
console.log();
}
if (Object.hasOwn(context, "$description")) {
Help.printCommand(context, rest);
console.log();
}

console.log(`\x1b[1mTry one of the following:\x1b[0m`);
console.log(`\x1b[1mTry one of the following:\x1b[0m`);

Help.printCommands(context, rest, false);
Help.printCommands(context, rest, false);
}
} else {
console.error(`\x1b[31mScript '${path.join(".")}' not found\x1b[0m`);
}
Expand Down Expand Up @@ -105,7 +124,9 @@ class Executor {
}

if (result == undefined) {
if (script.length > 0) Executor.notFound([...path, ...script], options);
//TODO improve not found message
if (script.length > 0)
await Executor.notFound([...path, ...script], options);
return;
}

Expand All @@ -121,8 +142,9 @@ class Executor {
path: string[],
options: Options,
): Promise<void> {
//TODO improve not found message
if (script.length > 0)
return Executor.notFound([...path, ...script], options);
return await Executor.notFound([...path, ...script], options);

if (!options.excludeArgs && process.argv?.length) {
context += " " + process.argv.join(" ");
Expand Down Expand Up @@ -184,7 +206,7 @@ class Executor {
const element = context[parseInt(sub)];

if (element === undefined)
return Executor.notFound([...path, sub], options);
return await Executor.notFound([...path, sub], options, context);

await Executor.runScript(
element,
Expand Down Expand Up @@ -240,7 +262,7 @@ class Executor {
options,
);
} else {
return Executor.notFound([...path, sub], options, context);
return await Executor.notFound([...path, sub], options, context);
}
}

Expand Down Expand Up @@ -374,7 +396,25 @@ class Executor {
options,
);
} else {
Executor.notFound([...path, "_default"], options, context);
await Executor.notFound([...path, "_default"], options, context);
}
}

static isExecutable(context: TScript): boolean {
if (typeof context === "function") return true;
if (typeof context === "string") return true;
// Unknown type
if (typeof context !== "object") return false;
if (Array.isArray(context)) return true;

const platform = process.platform;

if (Executor._isCI && Object.hasOwn(context, "_ci")) {
return true;
} else if (Object.hasOwn(context, platform)) {
return true;
} else {
return Object.hasOwn(context, "_default");
}
}
}
Expand Down
Loading

0 comments on commit 3757353

Please sign in to comment.