webfansplz / temir

Vue for interactive command-line apps
MIT License
1k stars 36 forks source link

PSA: state of Temir #19

Open tcardlab opened 1 year ago

tcardlab commented 1 year ago

Temir is a very cool package but has some rough edges. I just want to briefly go over some of the issues I have run into, some of the solutions, and remaining issues. Hopefully you can walk away with a better idea as to whether this library fits your use-case.

I think many of the Pros speak for themselves, this is one of the few (maybe only?) reactive cli tools with automatic dep tracking (courtesy of Vue). I had no issues with the reactivity, that all functions just as you'd expect.

Issues:

### Small Community, Few Refences, Mostly Unmaintained: There is no discussion tab on this repo, no discord, and issues seem to be unaddressed. If you are having trouble, the best I can do is point you to [these few references](https://www.npmjs.com/browse/depended/@temir/core) and maybe you can compare your work or strip their repos and use them as a template. ### Undocumented Features: If you plan to use this repo, it may be beneficial to review [Ink's docs](https://github.com/vadimdemedes/ink) as this repo is based on it. It will also be beneficial to look over the Temir's source code. In both cases you'll find things that exist here but are undocumented, like `waitUntilExit()` and other render/instance methods. ### TS Dependency Issues Preventing Build: The functionality of this repo's examples are currently dependent upon the lock file. If you delete that and rebuild, you will run into issues. Likewise, if you are starting a project from scratch without knowing this, you will likely run into these issues as well. One option is to just not use TS I suppose... Others have clearly gotten TS to work, so let them be your guide: - [live-parser](https://github.com/liou666/live-parser) - [flora-cmd](https://github.com/LittleSource/flora-cmd) - [npms](https://github.com/alexzhang1030/npms) Or wait for the maintainer to addresses [this issue](https://github.com/webfansplz/temir/issues/17). (I'll mention another solution later - `tips: building` below) ### Select input broken I mainly just wanted to use this repo for a convenient "item selection list", but was disappointed to find it was broken. If you arrow down then back up, things break. I stripped the original and [rolled my own here](https://github.com/tcardlab/STDB-VM/blob/main/components/Selector.vue). Hopefully someone may find it of use or otherwise find the bug in the original. ### Double console.log I was able to avoid it, most probably can as well. It is a little annoying tho. Depending ones use case, `render(app, { patchConsole: false })` may help. ### No Text Input Component If this is something you require, you will have to roll your own. You may wish to use [this ink version](https://github.com/vadimdemedes/ink-text-input/blob/master/source/index.tsx) as a reference. Or, maybe leverage more vanilla libs like [Inquirer](https://github.com/SBoudrias/Inquirer.js).

Tips:

### App vs Commands Going into you're project, you should have an idea as to whether you want CLI tool to be an App or Command based. I don't have, much to say about making a pure app but you can reference this [snake game](https://github.com/webfansplz/temir-snake-game) if you need help. For command-based tools, you will want to use the render function more synchronously as well as pass data: ```js function ender(renderer) { renderer.clear() renderer.unmount() } let renderWait = async (component, cb) => { let r, argsTmp; let resolvableCB = (...args)=>{ argsTmp=args; ender(r) } r = render(component(resolvableCB)) await r.waitUntilExit() // wait for resolvableCB() to execute ender() // We forward args to avoid race condition and double console.log if(!Array.isArray(argsTmp)) process.exit(1) // quit without selecting return await cb(...argsTmp) } program.command('someCommand') .action(async (options, cmd) => { let outPut = await renderWait( cb => , selected => { console.log('Selected:', selected.value) return selected.value }) }) // this could probably be better let renderPromise = async (component, cb) => { return new Promise(async (resolve, reject) => { let r; let resolvableCB = (...args)=>{ ender(r); // exit first to prevent double log issue resolve(cb(...args)) } r = render(component(resolvableCB)) }) } program.command('someCommand') .action(async (options, cmd) => { let outPut = await renderPromise( cb => , selected => { console.log('Selected:', selected.value) return selected.value }) }) ``` ### Building: Because I ran into TS issues with the unbuild version and also prefer to have a transparent build process, I chose Vite to build: ```js import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue' import vueJsx from '@vitejs/plugin-vue-jsx' import { dependencies } from './package.json' // Node modules will have to be registered manually here let node_libs = [ 'util', 'os', 'child_process', 'stream', 'path', 'fs' // can prob replace with `import { builtinModules } from "module"` ].flatMap(m=>[m, `node:${m}`]) let pkg_deps = Object.keys(dependencies) let pkg_dep_map = Object.fromEntries(pkg_deps.map(m => [m, m.replaceAll(/[\@\/\:]/g, '_')])) export default defineConfig({ plugins: [ vue(), vueJsx(), shebang('./dist/index.js'), del('./dist/index.umd.cjs') ], build: { minify: false, lib: { entry: './index.jsx', fileName: 'index', name: 'default', }, rollupOptions: { external: [ ...pkg_deps, ...node_libs ], output: { globals: { ...pkg_dep_map, ...Object.fromEntries(node_libs.map(m=>[m, m])) } } } } }) import fs from 'fs' function shebang (pathToBang) { return { name: 'remove-main-script', enforce: 'post', apply: 'build', closeBundle: async () => { if (!fs.existsSync(pathToBang)) return let stringToPrepend = `#! /usr/bin/env node\n\n` try { const data = fs.readFileSync(pathToBang, 'utf8') fs.writeFileSync(pathToBang, stringToPrepend + data) } catch (err) { console.error(err); } } } } function del(pathToRm) { return { name: 'del', enforce: 'post', apply: 'build', closeBundle: async () => { if (!fs.existsSync(pathToRm)) return fs.rmSync(pathToRm, { recursive: true } ) } } } ``` I am no Vite expert, so there are probably improvements to be made. ### Don't pass entry script to args If you are taking a command-based approach, you will only want to pass the given args to the program. This is a problem with the temir cli, where the given file to be served is passed as an arg... ```json { "scripts": { "start": "temir entryFile.jsx" } } ``` To alleviate this, you may want to create a serve file that hardcodes the entry point: ```js // serve.js // Allows us to start the dev server without passing index as an cmd-line arg import { runDevServer } from '@temir/cli' runDevServer('./entryFile.jsx') ``` `node serve.js` > NOTE: `runDevServer()` defaults to 'src/main.ts' rather than checking your entry in package.json... ### HMR Again, when dealing with a command-based approach, there are times when HMR gets in the way. An example might be starting a server on a given port or something that doesn't use the renderer at all: - You'll probably want to use `process.exit()` for renderless commands. - For things like a server, it would be nice to use something similar to `import.meta?.hot.on('vite:beforeUpdate', ()=>{})` to clean up long running/persistent processes. However, I haven't found a similar function in `vite-node` as of yet...  

Closing Statement:

I love how powerful and versatile Vue is and preferer to use it when possible. Despite the issues I ran into, I believe there is still great potential for this repo. However, in its current state I would not use it for something I need done quickly or professionally. Hopefully others may share their tips and trick to build up more of a community around this tool, I would love for it to become an obvious choice over Ink someday.

tcardlab commented 1 year ago

I almost wonder if building components in mitosis might appeal to a larger audience, then people can take them back to their framework specific renderers ( assuming they all use an Ink based api ).

webfansplz commented 1 year ago

Hi @tcardlab. Thank you for your suggestions and such detailed feedback. We plan to migrate temir to vue-termui. (I‘ll address this point in the README.) For several reasons, our work at vue-termui is currently not able to provide very active maintenance. If you're interested in it, we'd love to have you contribute and join us. Thanks again for enjoying it and providing such detailed feedback. ♥️