From 168652a6b73c1bb25cf14bf33e51e54b039a9ec8 Mon Sep 17 00:00:00 2001 From: Jan Andrle Date: Tue, 22 Oct 2024 14:01:10 +0200 Subject: [PATCH] v0.8 (#4) --- .jaaENV | 1 + README.md | 82 ++++++++------- bs.js | 226 ++++++++++++++++++++++++++++++++--------- bs/.build.toml | 1 - bs/README.md | 7 ++ bs/dev | 5 + example/bs/.build.toml | 5 - example/bs/README.md | 13 +++ package-lock.json | 124 ++++++++++------------ package.json | 6 +- src/completion.js | 59 ++++++----- src/config.js | 69 ------------- src/utils.js | 1 + 13 files changed, 340 insertions(+), 259 deletions(-) create mode 100644 .jaaENV delete mode 100644 bs/.build.toml create mode 100644 bs/README.md create mode 100755 bs/dev delete mode 100644 example/bs/.build.toml create mode 100644 example/bs/README.md delete mode 100644 src/config.js diff --git a/.jaaENV b/.jaaENV new file mode 100644 index 0000000..6acbe2c --- /dev/null +++ b/.jaaENV @@ -0,0 +1 @@ +nodejs=18 diff --git a/README.md b/README.md index ac253f6..53b43b6 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,50 @@ Your working directory should contain `bs` directory with building scrips/executables. You can use [`bs`](#bs) utility with auto-find feature. +### Executing scripts Now you can run and lists your build options like: - raw: - Run command: `bs/build.js some-argument` - - Lists commands: `find bs -type f -executable` - (list help texts `grep -H help bs/.*.toml bs/*/.*.toml`, see below) + - Lists commands: `find bs/** -executable`, `ls bs`, `find bs -type f -executable`, … + - create folder: `mkdir bs`, `mkdir -p bs`, `touch README.md`, … + - (optional, [see below](#configinfo-files-using-readmemd)) list commands with commnets: `cat bs/README.md`, `bat bs/README.md`, … - using `bs`: - Run command: `bs build some-argument` - Lists commands: `bs .ls` + - create folder: `bs .mkdir` + - Cat README: `bs .cat` + +### Organizing scripts +There are no rules, *it is all up to you*. But definitely +we can put together some *suggestions* to work with bs more +**happily**. + +1. prefers **short names** without unnecessary + special characters (spaces, brackets, …) +1. *provide* `--help` options for your scripts +1. *use subdirectories* for subtasks +1. *use dots* in names for non-scripts (like `.config.js`, + `.common.js`, `.utils.js`, …) +1. provide `README.md` to comment your build scripts +``` +bs/ +├───build.js +├───build/ +│ ├───html.js +│ └───sass.js +├───run.js +├───publish.js +├───.config.js +└───README.md +``` + +PS: You can create alias for task with: +```bash +ln -rfs bs/target bs/alias +``` + +### Build flows Now focus on creating building flows. For parallel tasts, you can use this pattern: ```bash @@ -68,13 +103,6 @@ set -eou pipefail cat src/*.js | manipulate > index.js ``` -You can create alias for task with: -```bash -ln -rfs bs/target bs/alias -# optionaly -ln -rfs bs/.target.toml bs/.alias.toml -``` - ## `bs` This is just a simple helper providing nice outputs and make some operations easier. @@ -86,31 +114,15 @@ You can find binaries on [Release](https://github.com/jaandrle/bs/releases/lates Or use: `npm install https://github.com/jaandrle/bs --location=global` -### Config/Info files -You can create `.command.toml` file to describe `command` -and add additional configuration. Example: -``` -buld.sh -.build.toml -``` -```toml -#.build.toml -info= "Description of command" -default= true - -[completions] -__all= [ "--help", "--version" ] -cmd= [] -``` -…all is optional. But: -- `info`: this text is listed aside of command name (e.g. `bs .ls`) -- `default`: this changes behavior of plain `bs`. By default it runs `.ls`, now it runs marked command -- `completions`: provide options for completions `bs .run build …`/`bs build …` - - `__all`: these options are listed for all sub-commands - - `cmd`: registers sub-command and its possible arguments (`bs .run build cmd …`) +### ~Config/Info files~ Using README.md +[This feature](https://github.com/jaandrle/bs/blob/adfbe3dc419b3189a1f9661d308c293b1e3b0514/README.md#configinfo-files) has been removed in version 0.8. +It seems to be better to use `bs/README.md` for comment your build scripts. +See example for current project [`bs/README.md`](./bs/README.md). + +You can than use `cat bs/README.md` to get quick overview of available commands. ### `bs` synopsis -See [bs.js (line ≥24)](./bs.js#L24). +See [bs.js (line ≥31)](./bs.js#L31). ### `bs` completions To allow completions just add `eval "$(bs .completion bash)"` to your `.bashrc`. @@ -119,12 +131,8 @@ To allow completions just add `eval "$(bs .completion bash)"` to your `.bashrc`. You can use [Git - Submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules) to share your build scrips across your projects. ## WIP -- [x] provide `bs` binary -- [ ] some missing? commands in `bs` (maybe `.init`) -- [x] docs for `.command.toml` (`bs` completion) -- [x] docs for git submodules to share build scripts +- [ ] provide `bs` via npm - [ ] docs for coexistence with others (such as `npm run`) -- [x] examples how to use bash for parallel/serial execution ## Acknowledgments - [labaneilers/bs: The simplest possible build system using bash](https://github.com/labaneilers/bs) diff --git a/bs.js b/bs.js index 4022e84..655ea71 100755 --- a/bs.js +++ b/bs.js @@ -2,29 +2,38 @@ "use strict"; const // global functions/utils { log, format }= require("css-in-console"), - { readFileSync, existsSync, statSync }= require("node:fs"), + { readFileSync, existsSync, mkdirSync, writeFileSync, statSync }= require("node:fs"), + { join }= require("node:path"), { pipe, linesToMaxLength, passBuildArgs, - listExecutables, isExecutable }= require("./src/utils.js"); + listExecutables, isExecutable }= require("./src/utils.js"), + { cwd: pwd }= require("node:process"); /** @type {()=> void} */ let error; const // global consts + headline= "bs: Build system based on executables", css= log.css` .error { color: lightred; } .code { color: blue; } .code::before, .code::after { content: "\`"; } .tab::before { content: " "; } - .info { color: magenta; } + .info {} + .headline { color: magenta; } + .highlight { color: lightyellow; } + .script::before { content: "λ "; } + .cwd::before { content: "⌂ "; } `, - { version, name }= pipe( readFileSync, JSON.parse )(__dirname+"/package.json"), - folder_root= findBS(), - config= require("./src/config.js")(folder_root); + { version, name, homepage, description }= pipe( readFileSync, JSON.parse )(__dirname+"/package.json"); +let // bs folder + folder_root; const fc= (code, ...rest)=> format("%c"+( !Array.isArray(code) ? code : String.raw(code, ...rest) ), css.code); //format as code const catchError= a=> { if(!error) return a; error(a); }; const api= require("sade")(name) .version(version) .describe([ + headline, + "", "This script allows you to create build scripts using simple executables¹:", "", `1. Create a ${fc(name)} directory in your repository root`, @@ -36,79 +45,183 @@ const api= require("sade")(name) "", "So, this script is not neccessary, but it provides some helpers:", `1. You can call executables without extensions (for example ${fc`${name}/test.py`} ⇔ ${fc`${name} test`})`, - `2. You can define default executable`, - `3. You can use completion, see ${fc`.completion`} command`, - `4. This utility can find current or any parent folder containing ${fc(name)} directory`, + `2. You can use completion, see ${fc`.completion`} command`, + `3. This utility can find current or any parent folder containing ${fc(name)} directory`, "", "To point out:", `1. To prevent colision all ${fc(name)} commands starts with ${fc`.`}c (e.g. ${fc`.ls`})`, - `2. Similar logic is used for special files/folder (e.g. ${fc`.command.toml`}?)`, + `2. It is a good practice to distinc non-commands from commands (eg. with preposition ${fc`.`}, ${fc`_`}, …)`, + "", + "Known pitfalls:", + `1. The ${fc(name)} tries to find root folder and uses it as cwd, so ${fc`${name} command ./file`} can works unexpectedly!`, "", "Notes:", `[1] use ${fc`chmod +x`} and shebang² like ${fc`#!/usr/bin/env node`} (similarly for bash, …)`, "[2] https://en.wikipedia.org/wiki/Shebang_(Unix)" ].map(linesToMaxLength(65))) -.command(".run [script]", "Run the given build executable", { default: true }) + + .command( + ".run [script]", + "Run the given build executable "+format("%c (default when `bs [script]`)", css.highlight), + { default: true } + ) .action(run) -.command(".ls", "Lists all available executables") - .action(()=> ls().forEach(lsPrintNth)) -.command(".completion ", [ "Register a completions for the given shell", - `This provides completions for ${fc`bs`} itself and available executables`, - "and argumnets for executables if specify in corresponding config file.", - "", - "To allow completions:", - `Just add ${fc`eval "$(${name} .completion bash)"`} to your ${fc`.bashrc`}` ]) + + .command( + ".ls", + "Lists all available executables"+format("%c (default when only `bs`)", css.highlight), + ) + .action(()=> { + const list= ls({ is_out: true }); + if(!list.length){ + if(!folder_root){ + log("%cNo `bs` for current directory: %c%s", css.error, css.unset, pwd()); + log("You may want to run %cbs .mkdir%c", css.code, css.unset); + } else + log(`No executables found in '${folder_root}'.`); + log(`Run %c${name} --help%c for more info.`, css.code, css.unset); + return process.exit(1); + } + list.forEach(lsPrintNth); + log("\nFor more info use %cbs .cat%c", css.code, css.unset); + return process.exit(0); + }) + + .command(".mkdir [root]", [ "This initializes the projects bs directory", + `With ${fc`root`} folder defaults to ${fc`.`}.` ]) + .action(init) + + .command(".readme", "This is primarly used for update current bs/README.md content.") + .action(init) + + .command(".cat", "This prints bs/README.md content") + .action(cat) + + .command(".completion ", [ "Register a completions for the given shell", + `This provides completions for ${fc`bs`} itself and available executables`, + "and argumnets for executables if specify in corresponding config file.", + "", + "To allow completions:", + `Just add ${fc`eval "$(${name} .completion bash)"`} to your ${fc`.bashrc`}` ]) .action(completion); api.parse(passBuildArgs()); -function ls(){ +function readReadme(path_bs){ + const path= join(path_bs, "README.md"); + const content= existsSync(path) ? readFileSync(path, "utf8").split("\n") : [ + "# "+headline, + `This project uses [${homepage.slice("https://github.com/".length)}: ${description}](${homepage}).`, + "", + "## Available executables", + "", + ]; + let h_level= 3; // defaults to ### + const found= content + .flatMap((line, i)=> line.match(/^#+ `?(.\/)?bs\/(.*)/) ? [ [ content[i], i ] ] : []) //` + .map(function([ l, i ]){ + h_level= l.indexOf(" "); + const script_start= l.indexOf("bs/")+3; + // index of space or ` or line end + const script_end= /( |`|$)/g.exec(l.slice(script_start)); + const end_adjust= script_start + ( script_end[0]!=="" ? 0 : 1 ); + const key= l.slice(script_start, script_end.index+end_adjust); + return [ key, i ]; + }); + + return { path, content, found: Object.fromEntries(found), h_level }; +} + +function init(root= pwd()){ + const is_init= process.argv.slice(2)[0]===".mkdir"; + const folder_root_local= is_init ? join(root, name) : ( loadBS(), folder_root ); + if(!existsSync(folder_root_local)) mkdirSync(folder_root_local); + console.log("Folder: "+folder_root_local); + const readme= readReadme(folder_root_local); + const execs= listExecutables(folder_root_local, 0).map(e=> e.replace(folder_root_local+"/", "")); + const execs_known= Object.keys(readme.found); + console.log("Executables: "+execs.join(", ")); + const execs_add= execs + .filter(e=> !execs_known.includes(e)) + .map(e=> "#".repeat(readme.h_level)+" bs/"+e); + writeFileSync(readme.path, readme.content.join("\n")); + if(execs_add.length) + console.log("Missing in README: \n```markdown\n"+execs_add.join("\n")+"\n```"); + console.log("Readme: "+readme.path); + process.exit(0); +} +function cat(){ + loadBS(); + if(!folder_root){ + log("%cNo `bs` directory found", css.error); + return process.exit(1); + } + log("%c"+folder_root, css.cwd); + const readme= join(folder_root, "README.md"); + const readme_content= existsSync(readme) ? readFileSync(readme, "utf8") : ""; + if(!readme_content){ + log("%cNo `README.md` (content) found in `bs` directory", css.error); + return process.exit(1); + } + readme_content.split("\n") + .forEach(function echoLine(line){ + if(line.trim().startsWith("#")) + return log("%c"+line, css.headline); + log(line); + }); + process.exit(0); +} +function ls({ is_out= false }= {}){ + loadBS(); if(!folder_root) return []; + if(is_out) log("%c"+folder_root, css.cwd); + const { content, found }= readReadme(folder_root); return listExecutables(folder_root, 0) .map(pipe( f=> f.slice(folder_root.length+1), - path=> path.replace(/\.[^/.]+$/, "") + function(script){ + const name= script.replace(/\.[^/.]+$/, ""); + const readme= Reflect.has(found, script) ? found[script] : -2; + return { script, name, docs: content[readme+1] }; + } )) - .sort(function(a, b){ + .sort(function({ script: a }, { script: b }){ const deep= [ a, b ].reduce((acc, curr, i)=> acc + (-1)**i * (curr.match(new RegExp("/", "g")) || []).length, 0); if(deep) return deep; return a.localeCompare(b); }); } -function lsPrintNth(file){ - const c= config.executables[file]; - let out= "> "+fc(file); - if(c && c.info) - out+= "\t"+format("%c"+c.info, css.info+css.tab); - log(out); +function lsPrintNth({ script, docs }){ + let out= fc(script); + if(docs) out+= ": "+docs+"…"; + log("%c"+out, css.script); } function run(script){ + loadBS(); const args= process.argv.slice(2); if(args[0]===".run") args.shift(); - if(!args.length){ - const is_default= Object.entries(config.executables).find(([_, c])=> c.default); - if(!is_default) return runFallback(); - script= is_default[0]; - } + if(!args.length) return api.tree[".ls"].handler(); + else args.shift(); catchError(); - const head= lsPrintNth.bind(null, script); + // TODO: what about `./` in args when cwd≠folder_root/.. process.chdir(folder_root.replace(/\/bs$/, "")); - script= "bs"+"/"+script; + script= "bs/"+script; if(!existsSync(script) || !statSync(script).isFile()){ const candidate= listExecutables(script.slice(0, script.lastIndexOf("/")), 0) .find(f=> f.startsWith(script) && f[script.length]==="."); if(candidate) script= candidate; } + log("%c%s", css.cwd, folder_root); if(!isExecutable(script)){ log(`%c'${script}' doesn't exist or is not executable`, css.error); return process.exit(1); } - head(); + lsPrintNth({ script }); const { spawn }= require("node:child_process"); return spawn(script, args, { stdio: "inherit" }) .on("exit", function onexit(exit_code, signal){ @@ -119,26 +232,45 @@ function run(script){ process.kill(process.pid, signal) }); } -function runFallback(){ - const list= ls(); - if(list.length) - return list.forEach(lsPrintNth); - - log(`%c${name}@v${version}`, css.info); - log(`Run %c$ ${name} --help%c for more info.`, css.code, css.unset); - return process.exit(0); -} function completion(shell){ const { completionBash, completionRegisterBash }= require("./src/completion.js"); if("bash"===shell) return completionRegisterBash(name); if("bash--complete"===shell) - return completionBash({ api, ls, config }, process.argv.slice(4)); + return completionBash( + { api, completionScript, ls: ()=> ls().map(r=> r.name) }, + process.argv.slice(4) + ); log("Unknown shell: "+shell); process.exit(1); } +function completionScript(name){ + let bsrc; + if(!folder_root) loadBS(); + const end_empty= { "subcommands": [] }; + if(!folder_root) return end_empty; + + const { readdirSync }= require("node:fs"); + for(const file of readdirSync(folder_root)){ + if(!file.startsWith(".bsrc")) continue; + bsrc= join(folder_root, file); + break; + } + if(!bsrc || !isExecutable(bsrc)) return end_empty; + const { script }= ls().find(f=> f.name===name) || {}; + if(!script) return end_empty; + const { spawnSync }= require("node:child_process"); + try { + return JSON.parse(spawnSync(bsrc, [ "completion", script ]).stdout); + } catch(e){ + return end_empty; + } +} -function findBS(cwd= process.cwd()){ +function loadBS(){ + if(!folder_root) folder_root= findBS(); +} +function findBS(cwd= pwd()){ const folder_root= "/bs"; // allow change? let candidate= cwd.replace(/\/$/, ""); while(!existsSync(candidate+folder_root)){ diff --git a/bs/.build.toml b/bs/.build.toml deleted file mode 100644 index 32dac81..0000000 --- a/bs/.build.toml +++ /dev/null @@ -1 +0,0 @@ -info= "Package nodejs into `dist`" diff --git a/bs/README.md b/bs/README.md new file mode 100644 index 0000000..7680c3f --- /dev/null +++ b/bs/README.md @@ -0,0 +1,7 @@ +# bs: Build system based on executables +This project uses [jaandrle/bs: The simplest possible build system using executable/bash scripts](https://github.com/jaandrle/bs). + +## Available executables + +### bs/build +Package nodejs into `dist` diff --git a/bs/dev b/bs/dev new file mode 100755 index 0000000..846fd66 --- /dev/null +++ b/bs/dev @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -eou pipefail +csd="./${BASH_SOURCE[0]%/*}" +. "$csd/build" +cp -v "$csd/../dist/bs-linux" ~/bin/bs diff --git a/example/bs/.build.toml b/example/bs/.build.toml deleted file mode 100644 index 07b46c1..0000000 --- a/example/bs/.build.toml +++ /dev/null @@ -1,5 +0,0 @@ -info= "Test Build #1" - -[completions] -__all= [ "--help", "--version" ] -production= [ "--target" ] diff --git a/example/bs/README.md b/example/bs/README.md new file mode 100644 index 0000000..f7ae745 --- /dev/null +++ b/example/bs/README.md @@ -0,0 +1,13 @@ +# bs: Build system based on executables +This project uses [jaandrle/bs: The simplest possible build system using executable/bash scripts](https://github.com/jaandrle/bs). + +## Available executables + +### bs/build.sh +Build script. + +### bs/sleep +Sleep. + +### bs/sleep2 +More sleep. diff --git a/package-lock.json b/package-lock.json index 5cc6b58..d0b9bdf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "bs", - "version": "0.7.4", + "version": "0.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bs", - "version": "0.7.4", + "version": "0.8.0", "license": "MIT", "dependencies": { - "css-in-console": "~2.0", + "css-in-console": "~1.1", "sade": "~1.8" }, "bin": { @@ -19,7 +19,7 @@ "pkg": "~5.8" }, "engines": { - "node": ">= 18.0.0" + "node": "~18.19" } }, "node_modules/@babel/generator": { @@ -37,18 +37,18 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", - "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true, "engines": { "node": ">=6.9.0" @@ -113,9 +113,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true }, "node_modules/@jridgewell/trace-mapping": { @@ -263,12 +263,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -356,20 +356,20 @@ "dev": true }, "node_modules/css-in-console": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/css-in-console/-/css-in-console-2.0.0.tgz", - "integrity": "sha512-Xbphn3qbe/XO67D8dr7Q39qSqi3ddnSsL72nixhKLiUEr8K3oTlk9aCOKRyCVNgPtgWwhynM19jj5mw1n/K2Tg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/css-in-console/-/css-in-console-1.1.1.tgz", + "integrity": "sha512-y1a9V0Pb4Q7pyWSA+nLTwjD0NdvgLPg9DOsxAw57HNtQBHt+UOoh7FRUsKSut/hoei+cwjhl/0hNnE8Qm1LFCw==", "engines": { "node": ">=18" } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -441,9 +441,9 @@ } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "engines": { "node": ">=6" @@ -484,9 +484,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -652,9 +652,9 @@ ] }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "engines": { "node": ">= 4" @@ -769,18 +769,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -791,12 +779,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -839,9 +827,9 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, "node_modules/multistream": { @@ -889,9 +877,9 @@ "dev": true }, "node_modules/node-abi": { - "version": "3.56.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.56.0.tgz", - "integrity": "sha512-fZjdhDOeRcaS+rcpve7XuwHBmktS1nS1gzgghwKUQQ8nTy2FdSDr6ZT8k6YhvlJeHmmQMYiT/IH9hfco5zeW2Q==", + "version": "3.67.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.67.0.tgz", + "integrity": "sha512-bLn/fU/ALVBE9wj+p4Y21ZJWYFjUXLXPi/IewyLZkx3ApxKDNBWCKdReeKOtD8dWpOdDCeMyLh6ZewzcLsG2Nw==", "dev": true, "dependencies": { "semver": "^7.3.5" @@ -1145,12 +1133,15 @@ } }, "node_modules/resolve/node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1207,13 +1198,10 @@ "dev": true }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -1496,12 +1484,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", diff --git a/package.json b/package.json index 95119b1..20ecf22 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bs", - "version": "0.7.4", + "version": "0.8.0", "description": "The simplest possible build system using executable/bash scripts", "author": "Jan Andrle ", "license": "MIT", @@ -32,11 +32,11 @@ "pkg": "~5.8" }, "dependencies": { - "css-in-console": "~2.0", + "css-in-console": "~1.1", "sade": "~1.8" }, "engines": { - "node": "~18.19" + "node": ">=18.19" }, "pkg": { "assets": "./package.json" diff --git a/src/completion.js b/src/completion.js index ba8da83..cebf2ab 100644 --- a/src/completion.js +++ b/src/completion.js @@ -1,22 +1,23 @@ -const { pipe }= require("./utils.js"); const log= console.log.bind(console); -function completionBash({ api, ls, config }, [ level, now, prev, first, second, third ]){ +function completionBash({ api, completionScript, ls }, [ level, now= "", prev, first, second, third ]){ level-= 2; const matches= arr=> { - let out= arr.filter(item=> item.indexOf(now)===0); + let out= arr.filter(item=> item.toLowerCase().indexOf(now.toLowerCase())!==-1).sort(); if(!now.includes("/")) - out= out.map(f=> f.includes("/") ? f.slice(0, f.indexOf("/")) : f); + out= out.map(f=> f.includes("/") ? f.slice(0, f.indexOf("/")+1) : f); if(out.length) return out.join(" "); return ""; } const resolve= arr=> { log(matches(arr)); return process.exit(0); }; const options_global= api.tree.__all__.options.map(r=> r[0]).concat("--help", "--version"); - if(!level) return pipe( - ()=> Object.keys(api.tree).filter(c=> !c.startsWith("__")), - arr=> arr.concat(...options_global), - arr=> arr.concat(...ls()), - resolve - )(); + if(!level){ + if(now.startsWith(".")) + return resolve(Object.keys(api.tree).filter(c=> !c.startsWith("__"))); + if(now.startsWith("-")) + return resolve(options_global); + else + return resolve(ls()); + } if(first===".run"){ if(level===1) return resolve(ls().concat(...options_global)); @@ -26,31 +27,37 @@ function completionBash({ api, ls, config }, [ level, now, prev, first, second, } if(first.startsWith(".")) return resolve(options_global); - if(!Object.hasOwn(config.executables, first)) - process.exit(0); + if(!ls().includes(first)) + return process.exit(0); - const { commands= {}, __all= [] }= config.executables[first]; - if(level===1) - return resolve([ ...Object.keys(commands), ...__all ]); - if(commands[second]) - return resolve([ ...commands[second], ...__all ]); - process.exit(0); + // TODO: https://github.com/fvictorio/completely + level-= 1; // indexing from 0 + const { subcommands }= completionScript(first); + try{ + if(!subcommands.length) return process.exit(0); + if(!level && subcommands.length > 1) + return resolve(subcommands.flatMap(r=> r.command)); + + const subcommand= subcommands.length === 1 ? subcommands[0] : subcommands.find(r=> r.command===second); + return resolve(subcommand.flags.map(r=> r.name)); + } catch (_){ + log(".bsrc has probably invalid json scheme"); + return process.exit(1); + } } function completionRegisterBash(name){ log([ `__${name}_opts()`, "{", - ` COMPREPLY=( $(${name} .completion bash--complete "\${#COMP_WORDS[@]}" "\${COMP_WORDS[COMP_CWORD]}" "\${COMP_WORDS[COMP_CWORD-1]}" "\${COMP_WORDS[1]}" "\${COMP_WORDS[2]}" "\${COMP_WORDS[3]}") )`, - ' local l="2"', - ' if [[ "${COMP_WORDS[1]}" == \\.* ]]; then', - ' local l="3"', - " fi", - ' if [[ "${#COMP_WORDS[@]}" -gt "$l" ]]; then', - ' COMPREPLY+=( $(compgen -A file -- "${COMP_WORDS[COMP_CWORD]}") )', + "local current=${COMP_WORDS[COMP_CWORD]}", + ` COMPREPLY=( $(${name} .completion bash--complete "\${#COMP_WORDS[@]}" "\$current" "\${COMP_WORDS[COMP_CWORD-1]}" "\${COMP_WORDS[1]}" "\${COMP_WORDS[2]}" "\${COMP_WORDS[3]}") )`, + // current word starts with ./ or ../ + ' if [[ "$current" == \\.\\/* ]] || [[ "$current" == \\.\\.\\/* ]]; then', + ' COMPREPLY+=( $(compgen -A file -- "$current") )', " fi", " return 0", "}", - `complete -F __${name}_opts ${name}`, + `complete -o nospace -F __${name}_opts ${name}`, ].join("\n")); process.exit(0); } diff --git a/src/config.js b/src/config.js deleted file mode 100644 index ed8b28f..0000000 --- a/src/config.js +++ /dev/null @@ -1,69 +0,0 @@ -const { readFileSync, readdirSync, existsSync, statSync }= require("node:fs"); -/** - * @typedef Command - * @type {{ - * info?: string, - * options: string[], - * commands: Record - * }} - * */ -/** - * @param {...string} candidates - * @returns {{ - * executables: Record - * }} - * */ -module.exports= function(folder){ - if(!folder || !existsSync(folder)) - return { executables: {} }; - - const executables= listTOML(folder).map(readTOML) - .reduce(function(out, [ cmd, { completions= {}, ...curr } ]){ - const { __all, ...commands }= completions; - curr.__all= __all ? __all : []; - curr.commands= commands ? commands : {}; - out[cmd]= curr; - return out; - }, {}); - return { executables }; -}; - -function readTOML(path){ - const file_name= path.slice(path.lastIndexOf("/")+2, -".toml".length); - const pre= readFileSync(path).toString().split(/\n(?=[A-Za-z_]+=|\[)/g) - .reduce(function(out, line){ - line= line.replace(/#[^"'\r\n]*[\n\r]/g, "").trim().split(/\n/g).filter(line=> !line.trim().startsWith("#")).join("\n"); - if(!line) return out; - if(line.startsWith('[')){ - const end_bracket= line.indexOf("]"); - const end_line= line.indexOf("\n") - 2; - out.push([ line.slice(1, end_line < 0 ? end_bracket : end_line), {} ]); - return out; - } - const eq= line.indexOf("="); - if(eq===-1) throw new Error("Wrong TOML format (`rule= value`): " + line); - const key= line.slice(0, eq); - const value= line.slice(eq+1).trim(); - out.at(-1)[1][key]= JSON.parse(value); - return out; - }, [ [ file_name, {} ] ]); - const out= pre.shift(); - return [ out[0], pre.reduce((out, [key, o])=> (out[key]= o, out), out[1]) ]; -} -function listTOML(dir, level= 0){ - const out= []; - for(const file of readdirSync(dir)){ - const file_path= dir + '/' + file; - const stats= statSync(file_path); - if(stats.isDirectory() && level < 3){ - out.push(...listTOML(file_path, level + 1)); - } - if(stats.isFile() && isTOML(file_path)) - out.push(file_path); - } - return out; -} -function isTOML(file_path){ - const file_name= file_path.slice(file_path.lastIndexOf("/")+1); - return file_name.startsWith('.') && file_name.toLowerCase().endsWith(".toml"); -} diff --git a/src/utils.js b/src/utils.js index 401b58a..072225e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -53,6 +53,7 @@ function listExecutables(dir, level){ if(!existsSync(dir)) return out; for(const file of readdirSync(dir)){ + if(file.startsWith('.')) continue; const file_path= dir + '/' + file; const stats= statSync(file_path); if(stats.isDirectory() && level < 3){