Skip to content
This repository has been archived by the owner on Nov 7, 2023. It is now read-only.

PSA: state of Temir #19

Open
tcardlab opened this issue Oct 25, 2023 · 2 comments
Open

PSA: state of Temir #19

tcardlab opened this issue Oct 25, 2023 · 2 comments

Comments

@tcardlab
Copy link

tcardlab commented Oct 25, 2023

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 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 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:

Or wait for the maintainer to addresses this issue.
(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. 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 as a reference. Or, maybe leverage more vanilla libs like Inquirer.

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 if you need help.

For command-based tools, you will want to use the render function more synchronously as well as pass data:

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 => <Selector items={items} onSubmit={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 => <Selector items={items} onSubmit={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:

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...

{
  "scripts": {
    "start": "temir entryFile.jsx"
  }
}

To alleviate this, you may want to create a serve file that hardcodes the entry point:

// 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
Copy link
Author

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
Copy link
Owner

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. ♥️

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants