Create Custom CLIs - Live Workshop 2019-11-18 Part 3

Shawn Wang
InstructorShawn Wang
Share this video with your friends

Social Share Links

Send Tweet
Published 4 years ago
Updated 3 years ago

Create Custom CLIs with Shawn Wang

Instructor: [00:00] OK. I don't seem to have gotten [00:03] any questions over the break. I [00:05] hope people are up to speed, and [00:12] maybe you played around a little [00:13] bit with scaffolding and at [00:17] least some of the other prompts [00:19] in Inquirer. Definitely, if [00:23] you're working on something, [00:27] throw it up. I'm happy to take a [00:29] look and offer suggestions. [00:31] This is a very safe space, as to [00:35] we're all learning here, and I'm [00:36] no stranger to showing my crappy [00:38] code to everyone else as well. [00:42] The better part of our workshop [00:44] is being able to share, and get [00:46] some live feedback, and [00:50] collaborate, because a lot of [00:52] times, we're alone on this. [00:54] In [00:55] fact, over the break, I actually [00:57] saw Ben Awad. Famous is his name. [01:01] He released his own CLI as well. [01:05] That's not him. '87? '97. Holy [01:11] shit, he's that young. He's so [01:14] young. I forgot about the lower. [01:19] Let's do that. He created [01:24] GraphQL API. It incorporates [01:26] TypeScript, Apollo, Type GraphQL, [01:28] and Type ORM. [01:33] The way I look [01:34] into this is I go to MPM first [01:37] because he didn't publish any [01:39] GitHub. There it is. He did [01:45] publish in GitHub. I'm sure the [01:48] documentation is minimal. Yep. [01:50] [laughs] Classic. [inaudible] [01:56] GraphQL API by running one [01:58] command. Looks like he's using a [02:00] monorepo setup. Yep. [02:05] Let's head [02:05] to packages, and creat GraphQL [02:07] API, package JSON. It's going to [02:11] have the bin folder that we [02:12] always know and love now. It's [02:14] going to source index.js and [02:16] uses chalk commander FS extra. [02:18] That's about it. [02:20] Let's look at [02:21] his CLI. It's got to be like 10 [02:24] lines. It's ridiculous. No, it's [02:27] 60 lines. Using commander, he's [02:31] creating a new command using [02:33] package json.name. Adding [02:36] versioning arguments product [02:38] directory, using Chalk -- we're [02:40] going to use Chalk in this part [02:42] of the workshop -- and then [02:46] doing some prompting if stuff is [02:47] not supplied, and he is copying [02:52] a template. [02:54] This is using fs. [02:56] copySync. There may be some [02:59] cross-platform issues, or [03:01] recursive directory issues. I [03:03] use copy-template-dir, because [03:05] if I ever need to swap out [03:07] anything, I can also swap out [03:10] the variable names. That's very [03:12] helpful as well. This is a very [03:15] common utility function as well, [03:18] like, "Should I use Yarn or npm?" [03:20] There are Yarn detection [03:21] libraries out there that you can [03:24] use as well. [03:28] SQLite? Why is he [03:30] using SQLite? He's git ignoring [03:33] it, and then he's doing npm [03:40] install execSync. Very cool. [03:45] We're going to use some of this. [03:48] We have implemented quite a few [03:52] pieces of this. We have [03:57] understood how to parse flags [04:00] and arguments, and stuff like [04:01] that. [04:02] We've copied stuff from a [04:04] source template folder into a [04:07] destination folder. We need to [04:10] execute installation. That's the [04:14] missing step, that's something [04:16] that I need to do. Let's get [04:18] right into it. This is [04:20] definitely a nice little...it's [04:21] one of those CLI projects that... [04:23] It's just for him. He does a lot [04:24] of projects, and it's super [04:26] helpful. [04:27] It didn't take that [04:27] long, like we're not taking that [04:31] long. I'm taking my sweet time [04:32] explaining everything, because I [04:33] have the luxury of time. You can [04:36] tell that, once you're used to [04:37] this, that you copy on this [04:39] template folder. Ben Awad, he [04:42] does really good work with [04:44] TypeScript and GraphQL. Follow [04:46] him. [04:49] We're on to executing and [04:51] piping child processes with [04:53] Execa, and then we're going to [04:57] go onto the Polish and react-ink [04:59] section. These two will be much [05:02] faster, because we're not even [05:04] going to mess around with code [05:05] in some of these. I wanted to [05:06] tell you that they exist and [05:07] they're very easy. [laughs] [05:12] Let's go to the guide, and go to [05:16] child process. What we're doing [05:19] here...What is a child process? [05:21] A lot of times, your CLI wants [05:23] to call other CLIs or execute [05:24] code in parallel. Your [05:27] JavaScript is single-threaded, [05:29] but not really because you can [05:30] do threading or,run multiple [05:35] concurrent child processes. [05:41] The [05:41] communication between the back [05:43] and forth is sufficiently [05:44] advanced that I've seen some [05:46] really cool stuff done in [05:48] parallel and taking advantage of [05:51] the multicores of your machine. [05:53] This is really awesome. [05:54] The [05:55] node itself actually has a [05:57] simple child process API, [05:59] actually is very usable. You [06:01] just require execSync like you [06:03] saw Ben Awad did, or you can use [06:05] spawn center, like the stream [06:09] version of this. You spawn and [06:12] then you call some base command [06:14] and then you pass in some flags [06:16] similar to what you already saw. [06:18] Then LS represents three [06:21] different streams. As some of [06:23] you may or may not know, a lot [06:24] of CLIs depend on piping in and [06:28] out three different streams. [06:29] There's one standard in, you [06:31] represent a user input or the [06:33] previous CLI's input, standard [06:36] in coming in, it says CLI. [06:37] Coming out is two streams, [06:38] standard out and standard error. [06:40] Standard out is for successful [06:42] logs. Standard error is for [06:44] error logs. It's merely a [06:49] setting that you, the developer, [06:50] set like as to like, "Hey, this [06:52] is an error. Pipe it to standard [06:53] error. Hey, this is a log. pipe [06:55] it to standard up." [06:57] You can [06:58] accept data piped in from other [06:59] CLIs. That's why you have a pipe [07:01] command. One of the ones I use a [07:02] lot is lsof. I'm taking lsof, [07:10] and then I'm piping it into grip, [07:12] and then I'm piping into grip [07:13] again. That's a very common type [07:16] of unique C approach that you [07:19] frankly don't really use a lot [07:20] in node CLIs for most of your [07:22] use cases. [07:24] If that's a design [07:25] goal of yours, absolutely take [07:26] advantage of that. Most of the [07:29] time, you actually care about [07:30] piping out so that people can [07:32] grip your input. Something like [07:37] myCLI generate or stream, and [07:43] then and then you grip, like [07:45] grip errors only, something like [07:50] that, whatever. [07:53] You care about [07:54] the exhaust more than the [07:55] ingestion, because you're [07:56] typically the primary source of [07:59] CLI operation. There's some [08:05] explanation about oclif and why [08:06] we use this.log instead of [08:08] console.log. Not super important. [08:13] We are going to deal with child [08:16] processes. The main reason we [08:18] don't use the raw node child [08:21] process is because of cross [08:22] platform nuances, which nobody [08:24] has time for. It's documented in [08:27] the execa docs. [08:29] We're just [08:29] going to use execa. I's from [08:30] Sindre Sorhus who is the most [08:33] prolific node author on the [08:35] planet. We could do a lot worse [08:37] than relying on his celebrities [08:39] because everyone else does. [08:42] We're going to incorporate... [08:45] Let's just stick it in here. [08:50] Just CD out of this. zcli- [08:54] workshop. I'm going to use Z [08:56] again. I think I'm back in [09:01] business. I have my dev command. [09:08] I could use as my generate [09:09] command. I can generate. I can [09:13] name my folder. I need a cheap [09:14] command. I'm Just going to [09:15] create a new test command test. [09:18] ts, check it in there. [09:22] I don't [09:22] want this to copy out [09:23] directories. I just want it to [09:24] run stuff so I can show you [09:28] things. Let's dump out inquirer. [09:37] I'm going to install execa. [09:39] Execa is a nice crossplatform. [09:46] They are over the default node [09:50] process. [09:52] Literally, you're just [09:54] running commands like you would [09:58] in a terminal except you break [09:59] it up into the base command and [10:01] then an array featuring all your [10:03] arguments and flags. Obviously, [10:05] there's no native conception of [10:07] the difference between arguments [10:08] and flags, so they're all [10:09] arguments. Bear that in mind. [10:20] Let's see what this says, when [10:23] we pipe it out. Here, we're [10:25] piping out the stdout of the sub- [10:27] process. We're taking stdout of [10:30] this sub-process and then we're [10:31] piping it to the parent process [10:34] stdout. It's going to echo [10:36] whatever we have over there. cli- [10:40] workshop-six generate test. [10:48] Again, the reason it takes so [10:49] long to compile, A is, because [10:50] I'm streaming, B is, because [10:53] it's compiling TypeScript every [10:55] time. "Cannot find the name [10:56] execa." Of course, I didn't [10:58] install it. Is it a top-level [11:04] import? Yeah. I hope that works. [11:07] It should work. Sometimes it's [11:09] usually very fastidious about [11:10] the default export. [11:24] [pause] Instructor: [11:24] I need to add the [11:24] esModuleInterop flag. Did I not [11:26] already add it? I added it. Nope. [11:31] This is a flag that lets you [11:33] interrupt with ES modules, like [11:35] it says inside of TypeScript. [11:38] It's a good idea to always have [11:39] it. It's too bad that oclif [11:44] doesn't scaffold it for you. If [11:47] you are familiar enough with [11:48] TypeScript, you should be able [11:49] to run that. [11:53] It does stdout [11:54] pipe, and then echoes, and then [11:59] it crushes it. If I wanted to [12:02] run foo but then not say [12:06] anything, I could say, for [12:09] example, standard...I could [12:12] silence this and not pipe [12:13] anything, and it would just say [12:16] foo, but then it won't pipe out [12:20] anything. [12:21] As a parent, the [12:23] child is saying foo, but no [12:24] one's listening, so I don't have [12:29] any place to print it. We can [12:31] obviously pipe it through the [12:33] file. Here we can create a write [12:34] stream. This is one of my [12:38] favorite ways to log out stuff [12:40] inside of a CLI, especially if [12:41] it's a super long thing and you [12:43] just want a sessions log, you [12:46] can definitely pipe it into a [12:48] stdout. I need to import fs. [13:07] [sings] [13:07] Here's the stdout, [13:08] saying foo. That's something [13:09] that we piped out over time. [13:13] That's great. Let's make it do [13:18] something more interesting. [13:21] Let's switch back to our [13:23] generate function. Over here, [13:28] what we're going to do is, we're [13:31] copying over the directory. [13:38] Remember, there's no Node [13:39] modules here, so you want to run [13:42] npm install, or Yarn add, or [13:44] Yarn install for these users. [13:50] The task would be to run npm [13:54] install. This Zoom bar is [14:06] disturbing me, even though you [14:08] can't see it. [sings] [14:17] I'm [14:17] cheating here and assuming Yarn. [14:20] Obviously, you should run some [14:21] Yarn test. For example, the one [14:25] that we saw with Yarn. You can [14:35] also use Yarn or npm, that's a [14:37] npm package, and then do the [14:39] appropriate npm or Yarn [14:41] installing. Not super [14:43] interesting, but whatever. Yarn [14:49] install...Yeah, that's it. [14:59] You [15:00] have to be inside of a directory. [15:08] This is something I'm personally [15:10] not super familiar with. Let's [15:18] look at Execa, and see how we [15:20] can set a directory inside of [15:22] Execa. I forgot how to do this. [15:27] Give me one second. [sings] [15:59] Local dir? OK. I want local dir, [16:17] I'm not 100 percent sure. I [16:20] thought I had this. Clearly, I [16:24] forgot already. Let's look at [16:28] options, local dir, and then [16:31] destination dir. There's a high [16:38] chance this is wrong, but we'll [16:40] try to figure it out and hope [16:47] for the best. [16:52] Just like before, [16:53] we're going to step out of the [16:56] workshop repo for this one and [16:57] head to a new project. Now we're [17:02] going to say, cli-workshop [17:05] generate myApp, get us name [17:09] myApp. Did I use a flag? I'm [17:14] going to prompt for it anyway. [17:17] Generally, you want to tie your [17:18] flag to your prompt. There's a [17:21] better developer experience [17:23] there to use. I still haven't [17:24] [laughs] resolved this one. I [17:25] forgot to dive into that. I'm [17:27] sure it's a very small issue. [17:31] MyApp, I'm going to call that [17:33] myApp. It's supposed to run Yarn [17:35] install for me. I doubt that it [17:37] did, because it executed so [17:38] quickly. Let's look at my new [17:43] project...No. Where's myApp? [17:56] It's supposed to run Yarn [17:57] install for me and inside of [18:00] that new folder, which it did [18:03] not. [18:09] I'm not really sure where [18:11] to configure this. I'm happy to [18:14] take suggestions right now [18:15] because I'm a little bit...I [18:17] can't believe I forgot. Let me [18:22] see the chat if there is a chat. [18:24] Await execa. Sorry. All right. [18:29] John Clark says, "Await execa." [18:31] This is not the one I'm looking [18:32] for. [18:39] It's sync, right? Our [18:42] default is async, you're right. [18:49] It should be synced. It really [18:52] doesn't matter in this scenario, [18:54] but you may want to make it [18:56] async for your purposes. Let's [19:01] delete my app again. [19:06] I'm not [19:07] confident that I'm running this [19:08] local der right. This was [19:12] bothering me that I don't have a [19:13] way to debug this. We may have [19:22] to punt on this because this is [19:26] just something I assume that I [19:27] knew, so I didn't practice this, [19:30] but we'll see where this takes [19:33] us. [19:34] I actually happen to work [19:35] with the [inaudible] of execa so [19:38] I can just ask him, but where's [19:41] the fun in that? This is not [19:46] running. We need to run Yarn [19:52] install and I need to make this [19:54] local dir. Maybe it is that [19:57] other der. Should I say prefer [20:06] local true? Is that it? [sings] [20:26] No. [20:33] I'm not sure what I was...I [20:37] forgot. Jesus. Let's wire one [20:45] more thing. Let's try this Yarn [20:50] or npm library, because I've [20:51] used this one before. It has an [20:56] embedded spawning mechanism, [20:59] which is nice. It's helpful. [21:05] The problem is, so I'm trying to [21:06] run the local dir...The key goal [21:16] I'm trying to solve is, I'm [21:17] trying to run the Yarn inside of [21:18] that foreign directory. Let's, [21:21] in fact, see how Ben Awad did it, [21:24] because I'm completely blanking [21:26] right now. This is so bad. [21:32] The [21:32] nice to have is that, this code [21:34] is all over the place. You need [21:35] to go perve at other people's [21:37] code for a little bit, and then [21:38] you're like, "Ah!" I've never [21:46] had to think about it. [laughs] [21:51] This is installing it to this [21:55] place. Does that mean I should [22:01] cd? Yeah. I can do that. [22:14] [pause] Instructor: [22:14] Node share dir. That's awkward. [22:26] That means I need this one line, [22:29] and all of my problems go away. [22:38] Let's delete this myApp again. [22:42] Remember, the goal is to [22:44] replicate create-react-app's [22:45] functionality. I type in myApp, [22:47] and it's going to scaffold over. [23:05] [pause] Instructor: [23:05] I need to wait for the copying [23:07] to be done, because it creates [23:08] the folder. I called the change [23:12] dir on a folder that doesn't [23:13] exist. One more time. We're [23:17] recreating the create-react-app [23:18] functionality, where we're [23:20] scaffolding that thing and then [23:21] we're running npm install on it, [23:23] for all of the dependencies. [23:28] It's supposed to run npm install, [23:30] but we don't see any of [laughs] [23:33] the install messages, because we [23:37] haven't piped anything. Remember, [23:38] this is the whole thing that we [23:39] started out with, that we needed [23:40] to pipe stuff out to stdout. [23:44] The good thing is that now this [23:45] project works, because the Node [23:47] modules are installed properly. [23:49] I can run Yarn start. Fantastic. [23:53] Now let's do the final step of [23:54] piping the input and output. [23:57] There are a number of different [23:59] ways that we already explored, [24:00] how to do this. I will go for [24:04] the standard piping out to [24:06] stdout. We should be able to see [24:08] everything on there as well. [24:13] This is a general discipline of [24:16] your CLI might rep other CLIs, [24:18] and you may need to run a few of [24:20] them in parallel. This is how to [24:22] control them. What the hell is [24:25] this? Probably pipe does not [24:26] exist in type string. [24:35] Execa. [24:40] [sings] Execa.sync. Here we go. [24:52] Returns or throws a child [24:54] process result. What the hell is [24:55] a child process result? It's an [24:57] object with a standard out. Yeah. [25:08] Probably pipe does not exist in [25:09] type string. Execa.sync. Yeah, [25:17] executed this thing [25:19] synchronously. Returns or throws [25:21] a child process result. [25:33] No. [25:34] We're piping it to the parent [25:34] process, so it's the process. I [25:38] might have to remove .sync. That [25:41] might be it. Don't look so smart. [25:49] Although it might prematurely [25:50] execute. I don't know. Hey, look [25:54] at that. It's yawn installing. [25:57] Whoa. I think we created create [26:05] react app. Creation. [laughs] [26:12] Cool. Now we have some feedback. [26:14] We can add some nice messages [26:17] saying we've completed and [26:18] [inaudible] to do, but now we [26:22] have a nice little scaffolding [26:24] CLI. This is a standalone folder. [26:28] We can use this project like a [26:33] create react app, might be. [26:35] That's fantastic. That's [26:37] wonderful. A bit gnarly. I [26:40] apologize for that. I am rusty [26:42] on it, to be honest. [26:44] Let's keep [26:45] going because we have a lot to [26:47] do. You may want to process [26:49] skill or handle other signals of [26:55] closing. I'm the parent, I want [26:56] to close you. Because your CLI [26:59] may be run by other CLIs. If you [27:01] want to gracefully handle that, [27:03] the default is to just exit [27:05] anyway. [27:06] If you want to do any [27:07] clean-up code, that's your [27:08] opportunity. You can do [27:09] suppresses on SIGINT. All the [27:14] links are there for you to [27:17] figure it out, as and when you [27:18] need it. Most of the time, [27:19] you're not going to need it. [27:21] Quick one on update-notifier. [27:23] This is a nice one for telling [27:28] your users that your CLI might [27:30] be updated. Literally it looks [27:34] like this. I think if I show you [27:36] the [laughs] docs, you'll see [27:39] what I mean. You've definitely [27:42] seen this in some of your CLIs. [27:46] This one, "Update available, [27:47] blah-blah-blah. Run npm ig to [27:50] update." That's the one. It [27:53] literally looks like that. [27:55] You [27:55] can run that anywhere inside of [27:58] here. Source, dev...I'm going to [28:04] chuck it somewhere. Honestly, [28:05] chuck it in here, at the top [28:07] level. Doesn't matter. It's [28:09] supposed to run it first anyway. [28:13] Make sure that the paths are [28:14] correctly run. It's so simple [28:17] I'm not even going to show you, [28:18] because it's so dumb. [28:21] One thing [28:21] that is helpful, I mentioned [28:23] this before, is sharing the code [28:26] with the base oclif commands. [28:28] This is something that is [28:29] documented by oclif. If you head [28:31] over here...The core idea is, [28:35] let's say I have some common [28:38] code that I use in three [28:39] different commands. Maybe I [28:41] should extract that somewhere, [28:42] into one shared directory. [28:45] That's accounted for with the [28:48] share commands. [28:49] You can go over [28:50] here. It's tucked way down there, [28:52] but it's super useful. This is [28:54] class inheritance. [laughs] It [28:56] is really dumb once you figure [28:58] it out. Basically, you put it in [29:00] a base command, and then you [29:02] import the base command, instead [29:03] of importing the direct command [29:06] from oclif. [29:07] You have an [29:07] intermediate inheritance. It [29:09] sets some fields there, and then [29:10] you pass it on for the final [29:13] implementation. How that looks [29:16] for us is, we have a base.ts [29:19] class, for example. I'll write a [29:25] base.ts class. You don't have to [29:27] follow along on this one, this [29:28] one is really simple. [29:31] I'll [29:31] import the commands here, and [29:32] then it exports another command. [29:34] This is an abstract class, [29:36] meaning, it's not meant to be [29:37] used directly. I can do some [29:40] initializations. For example, [29:42] one good candidate for the share [29:44] command is update-notifier. [29:47] Where is my update-notifier? I [29:51] forgot it. These guys. [29:55] I can [29:56] chuck it in here, and then I'll [29:59] run it on initialization of [30:01] every command. For this command, [30:07] instead of importing directly [30:10] from oclif/command, I can import [30:12] command from ../base. [30:20] This will [30:21] extend anything base that I do. [30:24] This command can do it, the [30:26] generate command can do it, the [30:27] test command can do it, every [30:28] other command can do it. This is [30:30] a way to, "Let's put everything [30:31] boilerplate-y on here, even [30:35] instances." We have static [30:37] netlify api. [30:42] Here is a object [30:44] with all the methods that we [30:46] might possibly use. Over here, [30:49] we do this.netlify.getUser, and [30:55] it's a simple command over there. [30:57] That's a very nice way of [31:00] separating out the concerns from... [31:02] This is hooking stuff up in the [31:04] base, and then this is [31:06] implementing the implementation [31:08] details of your commands. [31:11] A lot [31:11] of ways to do that. Telling your [31:16] user that there is an update [31:18] makes sense. You give them a [31:19] sense of the SemVer change. I'll [31:23] delete all of this, because I [31:24] don't need this. Give them a [31:26] sense of SemVer change. Say, "Oh, [31:28] I've gone from version 092 to 1. [31:35] 2. That's a major version change. [31:37] That might be worth updating. [31:38] Let me go read about that." [31:43] CLI, [31:43] especially if it's globally [31:44] installed, you probably want [31:45] something like this, to nag [31:46] people, especially for security [31:48] issues. That's about it. This is [31:53] pretty simple. You can do this [31:54] on your own time. I'm going to [31:56] speed ahead, because we don't [31:58] have that much time. [32:01] Now we're [32:01] in the Polish section...That was [32:05] the Polish section. We were [32:06] Polishing the update experience. [32:07] Now we're going to explain how [32:11] to store state. Most CLI [32:14] experiences can be improved by [32:16] storing state. [32:17] For example, [32:19] let's say I go over to my dev [32:20] command. Remember, dev command [32:22] is the one with the autocomplete. [32:25] I say dev, and it's showing me [32:29] this long list. It's got [32:31] autocomplete, it's really nice. [32:48] [sings] [32:48] I'm going to cheat and [32:53] comment this out, because it's [32:54] not relevant to the stuff I'm [32:56] talking about right now. That [33:05] must have been it, that was the [33:06] source of the error just now. [33:09] I [33:09] have this long list of [33:11] autocomplete, and it's really [33:12] nice. It's the same list every [33:15] time. Let's say I pick pineapple [33:17] a lot, and then I pick pineapple [33:19] again, and I pick pineapple [33:20] again, and I pick pineapple [33:21] again. Wouldn't it be nice to [33:25] have pineapple be the first [33:31] response, because that's [33:32] something that...It's learned me. [33:34] That's what I've previously [33:38] implemented in the past. [33:41] In [33:41] order to do that, we need to [33:42] have this concept of memory. A [33:44] lot of CLIs today are [33:46] implemented as stateless scripts. [33:49] In fact, we're in the file [33:50] system, we can have as much [33:52] state as we want. It's free real [33:56] estate. Let's go for it. [33:59] Things [34:00] you can store, authentication [34:01] tokens. You log people in one [34:03] time, and then you have a token [34:06] that you can use to do [34:08] authenticative stuff with. [34:09] That's a very useful case of [34:11] CLIs. Preferences, like do I [34:14] want extra logging, do I want [34:16] dark mode, do I want to be [34:21] called swyx or Shaun or whatever. [34:24] Prior selection, so this is [34:25] relevant to our use case for [34:27] autocomplete. Performance caches, [34:29] like, "I calculated this once, [34:31] let me pull from cache instead [34:33] of recalculating it again," if [34:35] it's expensive. Offline sync, [34:37] for stuff that happened offline [34:38] that you might want to sync [34:39] online later. Frecency storage, [34:41] we'll talk about that next, and [34:42] whatever you want. [34:45] Where do you [34:46] store state? This is where it [34:48] starts to get tricky. CLIs want [34:52] to store state globally. You may [34:53] want to store some state locally [34:55] in your projects, but when you [34:56] move onto the next project, you [34:58] lose that. It's most helpful if [35:00] you can remember between [35:02] projects, that's the whole point. [35:04] You have to figure out where to [35:06] store state. There is a spec for [35:08] that. It's not the most friendly [35:12] spec, [laughs] but it is a spec. [35:14] To the furthest extent of your [35:15] abilities, if you follow those [35:17] specs, then the whole ecosystem [35:19] will help you implement things [35:21] freely. [35:23] I like following [35:24] community conventions, unless I [35:26] have a very strong reason not to. [35:28] This is when I don't. I've [35:30] definitely seen people get very [35:32] finicky about where you randomly [35:33] store state on disk. If you [35:35] [laughs] randomly show up on [35:37] your desktop and like, "Hey, I'm [35:39] just gonna dump my stuff here," [35:40] that's not great. [35:42] There is the [35:43] XDG spec, which lets users [35:45] specify, "OK, here is my config [35:48] cache. If you're gonna store [35:49] anything for caching or [35:51] configuration or data, store in [35:52] these three places, and we'll be [35:55] copacetic." Those are the main [35:58] three ones. There's an [36:00] additional one, which I never [36:01] use. [36:03] There's the question of, [36:04] you could write file, read file [36:06] all the time, or you could use a [36:08] library. Again, this is tour of [36:11] all the libraries. We're going [36:12] to use Sindre Sorhus's conf [36:14] library, short for config. [36:18] [laughs] It is the most simplest, [36:20] dumbest config library I've ever [36:21] seen. We're going to try and [36:23] implement it for our selection, [36:26] for some of these APIs. [36:32] First [36:33] of all, we have to cd back, CLI [36:34] workshop six. Again, I'm using [36:36] that z command that's so helpful. [36:39] I'm going to add Yarn add conf. [36:41] I'm going to basically copy from [36:45] the docs. There's no reason for [36:47] me to be original here. [36:51] Config. [36:52] set('unicorn'). He likes his [36:53] unicorns, that Sindre, that's [36:57] because he is one. Every time we [37:03] have a name selection, we're [37:11] going to store this in [37:21] selections. We'll say, "This is [37:24] an array of names." In fact, [37:28] we'll have to get this array [37:30] first, and then assign it [37:37] somewhere else. [37:39] Where is the [37:39] get? There's no get. Now I'll [37:46] spread it, and I'll add the last [37:50] name here. That's the best way [37:52] to do it. I could also push, but [37:55] I don't care. That's that. Let's [38:01] also add a nice debug, saying, " [38:03] selections, selections, [38:08] selection saved." Does that work? [38:17] I may have to stringify it. [38:29] I [38:29] can run debug now. The debug key [38:32] is, cliworkshop dev. Let's try [38:38] this. debug = cliworkshop dev. [38:57] I'm going to pick blackberry... [38:59] or banana, I like banana. "Use [39:01] delete to clear values." What am [39:05] I doing? Where did I go wrong? [39:14] This mapping doesn't help me. [39:21] I [39:21] should get, goddammit. We do [39:28] need to solve that source [39:30] mapping thing. That's very bad, [39:32] that it's pointing me to the [39:33] wrong source map. "Names is not [39:35] iterable." Of course, you [39:39] default to the... [39:53] [pause] Instructor: [39:53] Good. There's the question of, [39:56] where did it save that [39:58] preferences? I ran it here. It's [40:03] not saved there. The best way to [40:05] figure out where it's saved, is [40:07] to check config.path. Let's [40:09] check config.path. We're going [40:15] to say, "Debug config is saved [40:24] in config.path." Sweet. [40:42] We've [40:42] uncovered an edge case over here, [40:44] that two bananas are selected, [40:46] so we should probably de-dupe, [40:49] whatever. Also, note that it's [40:53] stored in /Users/Swyx/Library/ [40:56] Preferences/CLIWorkshop/Swyx/ [40:58] NodeJS/config.json. [40:59] This is [40:59] something that is picked from a [41:02] combination of both package.json, [41:04] as well as the proprietary Node. [41:06] js label that's slapped on my [41:08] conf. You can configure that, [41:11] but it doesn't matter, because [41:15] as long as your package name is [41:17] unique, that's fine. It'll be [41:19] saved there for good. [41:23] Let's [41:24] pull it up. You can see that [41:25] it's a JSON file with an array, [41:28] which is nice. Now we can add [41:34] some kind of recent history for [41:37] autocomplete. We can store and [41:38] retrieve our last five [41:39] selections. I'll leave the [41:43] duplication to another day, [41:46] because that's just [41:48] implementation detail. [41:51] Let's [41:51] also have a nice little [41:52] separator value here. We don't [42:00] have the colors module, so I'm [42:03] going to use star, recent picks, [42:09] and then I'll do that. We can [42:13] spread the names, they're down [42:18] here. [42:31] [pause] Instructor: [42:31] Let's see where that gets us. Oh, [42:45] we didn't spread it. [laughs] [42:52] Comma. [43:03] [pause] Instructor: [43:03] We have some memory in here. If [43:05] pick cherry, and then I rerun [43:07] again. It's a very rudimentary [43:11] way of...I should probably [43:15] reverse this, as well, then I [43:17] can pick cherry again. It's a [43:21] very rudimentary way of [43:22] improving your implementation of [43:26] autocomplete. [43:27] Obviously, keep [43:28] this to five. Try to make them [43:29] unique, and then use the [43:32] autocomplete to your advantage. [43:35] Honestly, this is such an easy [43:36] win. It's such a joke that most [43:39] people don't do this. That's [43:42] because they're not thinking [43:44] about user experience in the [43:46] same way that we treat a [43:48] frontend. These are common sense [43:51] things. [43:53] The principles here I'm [43:54] promoting are, offer to persist [43:56] state when possible, so you can [43:58] create nice defaults. Literally, [44:00] the goal is, someone should [44:03] benefit from using your CLI over [44:05] time. The only way to do that is [44:09] to have some memory. What the [44:12] hell's wintergreen? [44:15] If you have [44:16] an input, try to have an [44:17] autocomplete with three to five [44:19] things, so your users can press [44:20] down and get the thing that they [44:23] want. That's the fastest way of [44:24] doing it. Obviously, I have some [44:28] [laughs] more work to do here, [44:29] in terms of user experience. For [44:30] example, you should reverse the [44:31] order so the most recent one is [44:32] first. [44:34] You should also [44:34] deduplicate anything that's in [44:36] there, and limit it to five, [44:38] because you don't want to have [44:39] an infinite history of stuff. [44:41] That's all easy, you can do that. [44:43] I trust that you can do that. I [44:44] wanted to introduce you to the [44:45] idea that it didn't take that [44:46] much, and we already have a much [44:49] more usable CLI than we did [44:51] before. [44:54] The next step in our [44:55] Polish story is frecency. That's [45:01] the second-last step. This one, [45:02] we won't even...This is too [45:05] advanced. This is the PhD-level [45:10] CLI user experience. If you want [45:13] to have that galaxy brain [45:14] experience, remember that whole [45:16] thing with the Rupa z CLI that [45:19] I've been using to jump around [45:20] different commands and app, [45:23] whatever this thing is? It [45:25] doesn't matter. It learns me. [45:28] This is an application of [45:35] history, like remembering things [45:38] that I've done in the past, and [45:39] then sorting and ranking by [45:41] recency. You can also see this [45:43] in Slack as well. Here's a blog [45:45] post from Slack. You can also [45:46] click through this to see. [45:48] Imagine you have a 100 percent [45:49] company. You have five different [45:50] Matts because you're very [45:51] diverse. You only speak to one [45:54] Matt, Matt Hogkins, because [45:56] that's the guy that you speak to. [45:58] Instead, your UI keeps insisting [46:00] on alphabetically sorting the [46:01] Matts. Bylander keeps coming up [46:03] top, but you never talk to that [46:05] guy, he's an asshole. [46:07] What [46:07] Slack should do for you is, your [46:10] buddy should be up top. Even if [46:13] you have autocomplete here, [46:15] that's not good enough. You also [46:16] want a recency sort. It's a [46:18] combination of frequency and [46:20] recency. If by raw frequency, [46:24] you speak to Matt Hogkins the [46:26] most, then you put them up top. [46:29] That makes a lot of sense. [46:31] For [46:31] recency, if a new Matt joins and [46:33] he's your new bud, and Matt [46:35] Hogkins is falling behind in the [46:39] race, then you start referring [46:40] to that new Matt. That's what's [46:44] underlying Slack, and that's why [46:45] it's so intuitive. [46:48] I love this [46:48] chart. They did some research [46:50] because they have the logs. For [46:52] them, when they switched to this [46:54] frecency chart, the amount of [46:56] time spent, it used to be 80 [46:58] milliseconds, that's fine, but [46:59] now it's dropped [laughs] down [47:02] so much, by 10x. I don't like [47:05] the word 10x faster when you're [47:06] doing something smaller, but [47:08] whatever. [47:10] It obviously is [47:11] faster because you get to the [47:13] person that you mean so much [47:14] quicker. That's CLI in a UI, but [47:18] you can have a CLI inside of CLI. [47:21] That is faster. There is a [47:23] project for this, called [47:25] frecency. It is made for the [47:28] browser, so you need to adapt it [47:31] to do local storage. [47:32] I was lazy, [47:33] so I showed you the no local [47:36] storage package, but you can [47:37] also use conf as well. You have [47:40] to implement the API interface [47:42] on your own. Perfectly fine. [47:44] You're storing it somewhere, and [47:46] then you're sorting your [47:48] frecency results. [47:51] Basically, [47:52] nobody does this. I didn't [47:56] manage to do it, because I had [47:58] other priorities to do. This is [48:00] super interesting. It's also [48:02] another easy win. If you thought [48:04] the conf was easy, then you have [48:06] this frecency stuff, and then [48:07] you talk it up. Sounds pretty [48:10] fancy, it's a nice win. [48:13] The [48:13] overall principle is, use [48:15] information about the user, your [48:17] very intimate and privileged [48:19] position to learn from the user, [48:21] reflect it back to them and [48:22] they'll reflect that [inaudible] [48:22] back to you, is a nice overall [48:25] principle. That's frecency. I [48:29] cheated by not talking about it, [48:30] not putting any code. [laughs] [48:33] We'll talk about the three other [48:36] libraries that I picked up. [48:37] There are a ton of libraries for [48:38] Polish. All we're doing is [48:39] Polish, Polish, Polish. [48:41] Oh, [48:42] Natalya. We have people leaving. [48:44] Thank you so much for joining in. [48:46] This will obviously be recorded. [48:49] I'm always appreciative of your [48:51] feedback and emails, and [48:54] whatever. Tell me what you think, [48:56] ask me your questions. I'm here [48:58] for you. [49:00] Let's get this going. [49:02] CLI, I'll put it with ora-cli. [49:05] These are ways of taming this [49:08] mess. What we've worked on is [49:14] nice and all, but it is a lot of [49:16] junk. The more complex stuff you [49:21] make, the more unintuitive it [49:26] can be. This is fine, because we [49:27] have colors, how do we put in [49:29] colors. We'll talk a little bit [49:32] about that. [49:33] I think I talked [49:33] about Chalk? No. Chalk is the de [49:36] facto library. There's Clare, [49:37] there's other libraries as well, [49:39] but this by far the default. [49:42] Stick with that and you'll be [49:44] well set. To show you what Chalk [49:47] does, I can Yarn add chalk, [49:55] const chalk = require('chalk'). [50:00] You can import, too, but [50:01] whatever. [50:02] The simple API is [50:03] chalk.color...you chain whatever, [50:08] and then you put in a string at [50:08] the end. Let's do this, node... [50:15] If you didn't know, you can run [50:16] a node repo down in here. You [50:17] can say const chalk = require(' [50:21] chalk'). Let's do a console. [50:25] chalk.blue. What? Why is this [50:34] undefined? [50:43] [pause] Instructor: [50:43] Do I have to do a default import? [50:49] Yay, chalk.blue, bold, and then [51:00] you can look at the API. Let's [51:01] do green. I have blue on purple, [51:05] it's hard to read. Maybe a more [51:07] accessible color might be green [51:11] or magenta. These are things you [51:15] might want to think about. [51:16] You [51:16] can also take other modifiers, [51:18] like strikethrough. This doesn't [51:22] show up on my screen. You can [51:24] also see italics. italic "Hello [51:29] World!" Italics are a really [51:32] nice effect. There's a lot of [51:36] styling that you can do in here. [51:39] I tend to do this on any string [51:40] that has user input. For example, [51:44] if I have user input over here, [51:46] then I can say, "Config is saved [51:49] in chalk.cyan.config.path," and [51:55] then selection save chalk.yellow. [51:59] JSON.stringify. I'll give you an [52:01] example of what the workflow is. [52:03] If you use tag template literals [52:08] as strings, this is a really [52:14] easy workflow, because you can [52:15] just us the technical rules. In [52:18] future, you can supply this via [52:19] the name flag. For example, here [52:21] I want to highlight the name [52:22] flag to draw some attention, tog [52:24] that yellow.bold. Oh, and then [52:33] this is not template literal, so [52:35] I've got to convert it. Hang on. [52:39] The zoom window's getting in the [52:40] way. [sings] . All right, so [52:46] let's see how this looks. All [52:53] right, let's get rid of the dev. [52:55] Let's see it. I expect to see a [53:00] little bit more color in the [53:03] stuff that I choose to emphasize. [53:06] I'm going to choose ermine, cyan, [53:10] undefined. [53:12] Oh, I have to fix my [53:14] default. Sorry, guys. [sings] [53:23] Let's try that again. [sings] [53:34] You see those colors coming in? [53:36] This is new. I made this cyan, [53:38] and I made this yellow. For this [53:42] normal log, I made this flag [53:44] stand out. It just marks the [53:46] user, like this is not normal [53:48] text from the rest of the CLI [53:51] output. [53:52] Your eye just [53:53] gravitates to that more. That's [53:55] just a really good use of color. [53:58] Obviously, there are color blind [53:59] people that won't be able to see [54:00] this. Just don't depend on just [54:03] color alone to do that. I've [54:05] done that before. I regreted it. [54:07] Here are some other tips as well. [54:09] Save red for errors and green [54:11] for success. Cyan is a good [54:13] informational color. Blah, blah, [54:15] blah. Too many colors is also [54:17] confusing. Don't go overboard [54:19] with that. You know all this. [54:22] Cli-ux, this is oclif's first- [54:25] party utilities library. Hang on. [54:28] Let me check the questions. OK, [54:29] there are no questions. Cli-ux [54:32] is oclif's utilities library. It [54:34] just has a grab bag of other [54:36] stuff that you might want. They [54:39] have a spinner that's involved. [54:41] They have a wait. They have some [54:43] iterm-specific stuff that you [54:47] may want to use. It's all in the [54:52] docs. [54:54] Probably the most [54:54] interesting one is the table [54:56] that they have. I won't even [55:00] install this. I'll just show it [55:01] to you. The principle for tables [55:07] is that presenting tables of [55:10] information in CLIs, is that you [55:12] want to be able to grep it. For [55:15] grepping, you want to make it [55:17] horizontal rather than...You [55:20] want to make your columns like [55:22] columns array. What the hell? [55:26] Cli-ux. Ah. Goddamn it. Cli-ux. [55:34] It's case-sensitive. That's [55:34] weird. Let me see the tables. [55:41] Tables are like this. This is [55:43] the way that they recommend [55:46] parsing tables, which means we [55:48] want every line to be filterable [55:50] so that if people grep it they [55:52] only get the lines that they [55:54] want to see. [55:57] The head of the [55:59] name of the table is here. [56:00] Basically, the table API, it [56:02] just aligns everything [56:04] vertically for you. If you need [56:06] to present information, this is [56:08] your best bet. Obviously, you [56:10] see this natively a lot. This is [56:14] something I use a lot. Grep TCP. [56:18] These tables that you can just [56:19] grep as well. Let's say I want [56:21] to grep only the 505 ports. [56:31] These are the five-something [56:33] ports, something like that. Very [56:37] simple, but something you should [56:41] keep aware of when designing CLI [56:43] tables. The last one is Ora. Ora [56:51] or is centuries CLI library, [56:53] again, very, very simple. You [56:56] can see all the spinners here [56:58] that you can do with. [57:00] Anytime [57:01] you're kicking something async, [57:02] you probably want a spinner. You [57:04] can have immediate response. The [57:06] principle is that in CLIs, [57:08] people expect something to [57:11] happen really quickly. If you [57:13] can give them a CLI, a visual [57:15] indication, then the perceived [57:20] performance will be much faster. [57:22] Cool. [laughs] I struggled with [57:25] that so much. Holy crap. [57:29] I like [57:30] this CLI spinners. Check this [57:31] out. CLI spinners has a whole [57:36] bunch of others. Ora just has [57:38] like some of these circle-y [57:40] things. You've definitely seen [57:42] this before in Yarn and some [57:44] other CLIs. [57:45] Some of these are [57:46] just nuts. Look at this Pong [57:47] implementation, or Runner. My [57:49] favorite is Moon or Earth. Look [57:51] at Earth. I really like this. [57:56] I'm going to try and implement [57:58] this and not explode. [58:03] The API [58:04] is super simple. It just looks [58:05] so professional, like you've [58:07] just spent that little bit of [58:09] thought, just a few minutes of [58:11] your time polishing it up. It [58:14] pays off so nicely in terms of [58:16] your look and feel. It's worth [58:20] exploring. You just need to know [58:22] where to go. That's why I'm here. [58:36] I actually don't know. [laughs] [58:39] I'm more familiar with the Ora [58:40] API. I hope this is the right [58:42] one. Const CLI spinners. What [58:50] I'm going to do is inside the [58:52] generate command that we have, [58:55] I'm going to stick it in there. [59:03] When we kick off the expensive [59:05] action, which is installing Yarn, [59:09] we just say CLI spinners dots. [59:15] How do I kill it? Why are you [59:22] giving me an SE cinema thing? [59:26] Yeah, I'm using it through the [59:26] Ora module. Forget this. [59:34] [inaudible] . I'm looking at the [59:39] Ora. I know there's some [59:41] integration which I am supposed [59:43] to do. Ora. There we go. Got it. [59:57] I have to implement Ora. That's [60:00] what I'm supposed to do. I'm [60:02] supposed to install Ora, and [60:03] then load up the spinners in one [60:07] of those ways. Forget those. [60:11] We're going to start spinner. [60:20] Installing React and other [60:23] packages. [60:27] Once we're done, we [60:32] have to refer to the subprocess [60:38] exit. I'm going to go refer to [60:41] my own notes here. When the [60:53] subprocess closes, then close [61:01] the spinner. The spinner is [61:04] spinner.stop, I think. Let's see. [61:15] This is an object. I have text [61:18] here. The type or the spinner, [61:25] I'll have moon. How do I stop? [61:32] There we go, stop. [61:35] This one [61:35] will clear the spinner. I can [61:40] type succeed, spinner.succeed, [61:48] and that should give me a nice [61:49] green checkmark. I don't need [61:56] that debug anymore. I'm using [62:01] the dev command. Let's switch [62:03] back to newProject. I'm using [62:14] the generate command, cli- [62:17] workshop-six generate. It's [62:22] going to ask me for a name. I'm [62:26] going to call it the very [62:27] creative name of myApp2. [62:30] You [62:30] look at that moon, look at that [62:31] moon! Look at that! What's [62:36] happening here is that, every [62:38] time some child process pops [62:40] something out, we're going to [62:43] freeze whatever is on screen at [62:45] the time. This is what you'll [62:48] see, this little information, [62:52] intermingling that. [62:53] This is not [62:53] very nice, so we should probably [62:55] want to...We can have a few ways, [62:59] we can accumulate all these [63:00] standard out piping ins, and [63:02] then we'll dump it at the start. [63:04] We could clear this every single [63:07] time. We could clear the spinner [63:10] every single time, and then [63:11] restart the spinner. That's a [63:13] very long, laborious process. [63:15] It's up to you. [63:16] For now, I'm [63:17] going to comment out the [63:19] standard out, and then show you [63:22] how it looks, because it is nice [63:29] to see something moving on the [63:31] screen when work is being done. [63:33] myApp3. [63:36] Something's moving on [63:36] the screen, something is doing [63:37] in the background, you're [63:38] swallowing all those messages. [63:39] It may be a lot of messages that [63:41] you may not want, but you know [63:43] work is being done while this is [63:45] going on. [63:46] There's obviously [63:47] other tricks you can do to stop [63:49] and clear and re-render. This is [63:52] how you install spinners, and [63:54] give some nice...where heavy [63:57] stuff is being done in the [63:58] background. I've overused those [64:01] terms, but hopefully you [64:02] understand that we are adding [64:05] Polish. It just takes a few [64:06] minutes to add some flair onto [64:11] the CLI. That's a Polish thing. [64:15] That's the end of our Polish [64:17] section. We are about to start [64:18] on to the React Ink section, [64:22] which is the final section, and [64:24] the newest and the most [64:25] experimental. I'm going to take [64:28] a pause here, and ask if anyone [64:30] has any questions. I know I've [64:33] gone through a lot. I hope it's [64:36] interesting to you, because it's [64:37] not that hard to make nice CLIs. [64:43] Questions, questions, questions. [64:49] I know I had a couple of people [64:50] leave because they had to get [64:51] back to work. Hopefully, whoever [64:54] is watching this, asynchronously [64:56] as well, feel free to ask me [64:58] questions. Head to this repo, [65:01] file an issue, I'll answer [65:03] whatever you've got. I've been [65:06] following this for a while now. [65:09] I might have something that [65:10] might save you some time. [65:13] Honestly, I feel if we ever had [65:17] industry intro to node JS, I [65:20] think this should be in there [65:21] because it's so fundamental to [65:23] everything that we do. [65:27] Final [65:27] section, React Ink. We're going [65:30] to use React in CLIS. It's [65:33] absurd, it's ridiculous, it's [65:37] totally experimental and [65:38] sometimes buggy but it's super [65:41] exciting because of the layout [65:43] that it achieves. What is React [65:45] Ink? React Ink is a React [65:47] renderer to the CLI. [65:49] It lets [65:50] you componentized parts of your [65:52] CLI as React components which is [65:56] the same reason you want to use [65:58] components when you're writing [66:00] web interfaces. You might want [66:01] to use that for your CLI [66:03] interfaces, especially talking [66:05] about the [inaudible] engine. [66:08] If you think centering stuff in [66:11] CSS is hard, try centering [66:13] things in the terminal. It's [66:16] near impossible. It's actually [66:18] not. You can measure the width [66:20] of the terminal or calculate [66:21] that half of it and then [66:23] calculate the offsets and so on [66:24] and so forth or you could use [66:27] layout engine and just use the [66:28] Flexbox model which you as web [66:30] developers probably already know. [66:33] That's the main attractiveness [66:37] of this. There is little bit of [66:39] fudging. React Ink wasn't meant [66:42] to be used with OCLIF and OCLIF [66:44] had no idea that people would be [66:47] using React for writing UCLIS. [66:50] There's a little bit fudging [66:51] right now to have to do this. [66:52] I [66:52] think that using the best [66:55] command line, command parsing [66:57] framework and then the best [66:58] front and rendering tool, you [67:00] can get the best of both worlds [67:02] in terms of the technology stack [67:04] that you used to write your CLIS. [67:07] How do we write React Ink [67:09] projects? We're going to jump [67:11] over to back to the workshop [67:17] repo. We're going to install [67:20] React and Ink. React is this box [67:23] center React project but instead [67:25] of ReactDOM, we're now just [67:26] installing Ink which is a very [67:29] cool three-letter npm package [67:32] name. I'm very jealous. [67:35] I have [67:35] a couple as well but this is [67:37] actually getting some use so [67:40] kudos to them. We're also going [67:42] to have to modify tsconfig.json [67:44] if you're following along a [67:45] Typescript to recognize JSX. [67:50] That's just the tsconfig issue. [67:54] OCLIF doesn't recognize TSX [67:56] files. This is the situation [67:59] where if I rename this to dev. [68:03] tsx, this is valid Typescript. [68:07] Typescript will parse this. [68:09] OCLIF will not. You can try this [68:13] by running CLI workshop dev and [68:17] will just tell you, "What is dev. [68:19] I have never seen dev before in [68:20] my life and dev not found." [68:26] What an insult to my [68:26] intelligence. Dev is right there. [68:29] I just renamed Dev to dev.tsx. [68:32] This actually turned me off for [68:34] a little bit. The reason is [68:36] because they the hard coded .ts [68:39] in there. This is where I teach [68:43] you how to monkey patch your [68:44] unknown modules. You'll just [68:49] going to have to do it. [68:50] This is [68:50] what part and parcel of using [68:52] tooling, finding holes and [68:54] you're like, "Maybe, I need to [68:56] PR this upstream." In the [68:57] meantime, you need to maintain a [68:59] fork to fit your use cases [69:01] because your use cases will [69:04] differ from other people's. This [69:06] is a good general skill in [69:08] NodeJS to learn. It's, [69:09] fortunately, also very easy. [69:11] We're going to go the offending [69:13] library. It's node_modules/@ [69:15] oclif/config/lib/plugin. You get [69:26] here by tracing the calls. It [69:28] took me about 15 minutes. It's [69:31] not that hard. We have to get [69:36] command IDs. Here's the patterns [69:40] that it uses to recognize the [69:42] files that are valid. [69:44] It [69:44] understands JS, it understands [69:45] TS, doesn't understand TSX. What [69:49] I'm going to do here is, I'm [69:50] going to add a new line and call [69:51] it jsx and tsx. That would do it. [69:57] Now when I rerun, it's going to [69:59] rerun this exact code. Now it's [70:01] going to magically find the dev [70:02] command, that it previously [70:05] insisted that wasn't there [70:07] before. That's fantastic. [70:10] Now [70:11] the question is, I have this [70:12] janky thing in...I messed inside [70:16] of my Node modules. When I clear [70:18] my Node modules and reinstall, [70:19] that's going to go away. How do [70:21] I reapply my changes on top of [70:22] it? That's where patch-package [70:24] comes in. Definitely read the [70:29] docs for it, but I'm going to [70:31] show you the mechanics of how it [70:33] works. We're going to have to [70:34] reapply this change again. [70:36] Yarn [70:36] add patch-package postinstall- [70:39] postinstall. This is a [70:40] postinstall hook for Yarn, to [70:43] run the patching code after [70:50] modules are installed. We [70:55] installed this, and now we have [70:58] to add a postinstall command to [71:01] the scripts, to our package.json. [71:10] Where's our scripts? Sweet. Now [71:16] we run Yarn oclif/config. Let me [71:20] reinstall the...Rerunning Yarn [71:25] checks the integrity of all the [71:26] Node modules, and then [71:27] reinstalls anything that has [71:28] been compromised, like we did. [71:31] Now I have to apply my fix again, [71:34] and then say, "OK, for this [71:37] module, oclif/config, I have a [71:40] thing which I patched in my [71:43] package. Let me persist that [71:44] package by running this small [71:46] little CLI." It's downloading [71:55] the real package in the [71:57] temporary folder, it's diffing [71:59] the files with the clean files, [72:00] and it's creating the patch. [72:02] Guess what? This is all a CLI. [72:04] This guy did it in his free time, [72:05] just to help himself. You can do [72:07] this too. We just went through [72:10] how to do this. It's really [72:14] super nice whenever you can [72:15] benefit and build your own [72:16] tooling to help you, and you can [72:19] understand your own tooling, as [72:20] well. Let me just show you the [72:23] patch that was generated. [72:25] If [72:25] you're familiar with git, this [72:27] is exactly the same thing. This [72:28] is just a git diff, like here's [72:31] what it was before. Here is what [72:33] it was after. Here's the single [72:35] line that we added. When anyone [72:38] else clones this repo and runs [72:39] npm.install, it's going to find [72:41] this post install thing and run [72:44] patch-package. [72:45] Patch-package [72:45] CLI is going to go and look for [72:48] this patches folder. Inside, [72:49] there's this patch here. It's [72:51] going to reapply this thing on [72:52] top of the raw published [72:57] downloaded npm package. [72:58] That's [72:59] it. It's pretty simple, but a [73:02] lot of people are skip-y when [73:06] they look at something like this. [73:08] Yeah, it is a little bit scary. [73:10] OK, so let's copy and paste a [73:12] nice little Inc rendering layer, [73:14] so we can get a sense of what an [73:16] Inc command looks like. [73:19] I'm [73:19] going to cheat a little bit and [73:21] I'm going to write Inc.TSX over [73:23] here. I'm going to just paste my [73:24] prepared code. You can see, this [73:27] is a React component, right? [73:31] It's using some proprietary [73:34] components like the boxed [73:36] component from React Inc. [73:38] That's just a replacement for [73:38] div, similar to the view in [73:42] React Native. I have some hooks [73:45] here and it's so on, and so [73:47] forth. Very common, bog standard [73:49] React stuff. If you're not too [73:50] familiar with React, don't worry [73:52] about it. But you should be able [73:55] to get a hang of it after some [73:56] looking at it. [73:58] The hook between [73:59] oclif and React Inc. is down in [74:02] here where we just call the [74:03] render function, as part of the [74:05] run. We will use oclif to do the [74:08] documentation, do the flags, do [74:10] the args passing. Then we'll [74:12] parcel this stuff, and we'll [74:13] pass it in to the component and [74:16] we'll just render it. [74:20] Once we [74:20] are rendering it, the command's [74:22] just going to keep running until [74:24] we call exit. The exit command [74:27] is something that we pull off of [74:30] Inc. That's something that we do. [74:34] I'm now going to run our first [74:37] React.Inc command, which is [74:39] called Inc. [74:47] Again, it's slow [74:48] because of typescript and all [74:49] that. You see it's updating that [74:52] counter from 0to 1, to 2, to 9, [74:54] to 10? That's this hook that's [74:59] use effective and it's set time [75:00] updating constantly. That's the [75:08] integration between oclif and [75:09] React. Now we have something [75:11] that we can use componentizing. [75:14] We can build a system of [75:15] components and re-use them all [75:16] over the place to build out a [75:18] full application inside of our [75:19] terminal, which is pretty crazy. [75:22] It's something that's never [75:24] happened in CLI development. [75:27] That's pretty exciting. [75:31] I only [75:31] got two more left and then we [75:34] can break for the workshop and [75:36] more questions. Let's talk a [75:38] little bit about layout. I [75:41] introduced the box component a [75:42] little bit. I just said it's [75:44] like the dev. It basically has [75:46] by default all the flexbox [75:48] properties that you might want [75:49] on it except that the units are [75:52] in terms of characters or lines. [75:55] Here you have the width or four [75:56] characters and it's just four [75:57] characters long and inside of it [76:00] is left aligned to...The text [76:02] content inside of is left [76:03] aligned. You can have a width of [76:06] 50 percent of a parent component [76:10] so it's that as well. You can [76:12] have flex-direction. You can [76:13] switch that to column versus row, [76:16] by default it's row. [76:18] You can [76:18] set text wrapping so by default [76:21] there are certain width length [76:22] and if it exceeds, you can set a [76:26] text wrap for it to wrap around. [76:28] You can set PAT'ing. I really [76:30] like this. This is the box [76:35] PAT'ing of six. Let's see how [76:38] that looks. By the way, one way [76:44] to get TS, Typescript to compile [76:47] much faster is use the [76:48] incremental tag that uses some [76:51] caching. [76:53] I should have done [76:53] that from the start. I'm sorry. [76:56] This is a PATing of six lines. [77:00] Nice big PATing for people to [77:03] gaze at your wisdom. There's [77:09] other inbuilt components as well [77:10] so I can also call it color [77:12] component. This is similar to [77:14] the Chalk, but native to the [77:18] React [inaudible] ecosystem. [77:24] Let's just take this color. [77:39] Let's have slightly less [77:41] obnoxious PATing of two. It's [77:46] just going to output a box with [77:49] the counter. "Cannot find the [77:55] name color." What do you mean? [77:57] Oh, called it the wrong name. [78:15] [sings] [78:15] You see that nice [78:15] little color icon. I did go for [78:18] a black and white. I'm sorry [78:19] that's my bad but I can just BG, [78:23] keyword magenta. Whatever. You [78:28] got my meaning. You can have [78:30] color, you can have text. [78:32] There's native components that [78:34] they export just for all of the [78:37] stuff. There's our pink color [78:41] background. [78:44] This is a component, [78:45] this is a box component. Inside [78:47] of it, we're writing JSX like we [78:49] do normally. There is a lot of [78:52] opportunity for composition of [78:55] components. That might be [78:57] arguably a better way, or the [78:58] future way, to write CLIs. It's [79:01] worth discussing as well. [79:03] I [79:03] wanted to end this chapter with [79:05] one big, long demo. Here's a [79:10] example of the Jest UI. I think [79:14] I introduced some bugs to this. [79:19] [laughs] Any issues with this, I [79:23] take responsibility for. This is [79:24] the idea of, what you can do [79:26] with React Ink, where you're [79:30] creating components, and then [79:31] composing them up individually, [79:33] and then you're writing tests [79:34] like this. [79:35] This is the Jest [79:38] interface, that you're running [79:41] all these tests, and you're [79:42] importing them onto the screen, [79:43] dynamically updating different [79:45] sections of the screen. This is [79:47] a super polished UI. You're [79:50] writing this in these lines of [79:52] code. [79:53] Let me break it down for [79:53] you. There is a git breakdown [79:57] for status. There is a test [80:02] component. It defines the start, [80:06] middle, and end of these tests. [80:09] There's the summary section, [80:11] which gives you the summary of [80:13] all the tests that pass or fail. [80:16] Inside of the main command, the [80:18] jest command, you can declare [80:21] them, in terms of box, and then [80:23] complete a test up top, running [80:24] tests on the bottom. As they [80:25] complete, they would go smaller, [80:28] and then complete a test as [80:29] before. The summary will always [80:30] be at the bottom. [80:32] That layout [80:33] thing, it's become declarative. [80:35] You don't even think about [80:36] anything else. React takes care [80:38] of the re-render for you, which [80:40] is super nice. It's super nice. [80:44] This is super unexplored. Gatsby [80:46] has already put it into [80:47] production. There's a bunch of [80:49] other people as well, like React [80:51] Ink. Let's look at who else is [80:54] using this. [80:56] This is definitely [80:56] still early days. It's less than [80:58] two years old. It's side-project. [81:03] Definitely, it needs more [81:06] development, but there's a fair [81:09] amount of CLIs that make use of [81:10] this. It's really cool. I hope [81:14] more people explore interesting [81:16] UIs in CLIs, that take advantage [81:19] of information density, live [81:22] updating, and showing concurrent [81:24] work going on as well. [81:27] You have [81:27] all the code available here. [81:30] That's the power of the layout [81:32] model, which is pretty [81:33] interesting. On top of layout, [81:36] then you also need to take care [81:38] of interactivity. This is where [81:40] we talk about...in the input [81:42] selection as well. [81:46] There is a [81:46] bunch of first-party components. [81:49] Let's talk about useful [81:51] components. These are all of the [81:53] components that are provided [81:55] first-class, and then these are [81:56] the stuff that they're working [81:57] on to upgrade for Ink v1 to v2. [82:01] All of these are amazing already. [82:03] We'll focus on one, which is the [82:07] ink-text-input. We can show this [82:11] by, Yarn add ink-text-input. I [82:19] feel like I'm going...I pre- [82:21] prepared all this. It'd probably [82:22] take me too long to hand code a [82:25] lot of this. [82:27] I trust that you [82:28] can read React code. It looks [82:32] like [laughs] standard React [82:34] code, which is amazing. You have [82:35] a very thin rendering layer on [82:37] top of the oclif framework of [82:40] CLI parsing, so that's nice. [82:44] Let's swap this out with the [82:45] commands and see what we get. [82:53] [pause] Instructor: [82:53] I have a question from Zach [82:54] Jones about, "Can you use all [82:57] the CLI packages of React Ink [82:58] like cli-spinners?" Broadly, I [83:02] would caution against that, [83:05] because React Ink has control [83:08] over the stdout. If any other [83:11] people interfere, it may cause [83:13] that interspersing issue. That's [83:15] a very common problem with [83:17] terminals. I don't think we have [83:18] it figured out yet. [83:19] I think we [83:20] need a very eager clearing [83:21] policy, to hook into everything, [83:24] to make sure every output stream [83:28] [inaudible] . Right now, we have [83:33] a lot of crossing of the streams. [83:34] If you're a fan of "Ghostbusters," [83:35] you know that's not a good thing. [83:39] Right now, React Ink does [83:40] involve having an Ink version of [83:42] everything. That's not good, [83:44] obviously, but it is what we [83:46] have right now. In future, we [83:49] might figure out a way to [83:50] isolate these things. That would [83:54] be nice. We can reuse everything [83:55] that we already know. [84:00] Let's run [84:00] on new inputs. Again, I copied [84:02] and pasted this ink command. Now [84:06] we're no longer looking at non- [84:08] interactive output, now we're [84:10] interacting with it, and [84:11] updating it from the React [84:13] paradigm of, "I have a use state [84:16] hook over here, and I'm updating [84:17] it." I can have Zach, Natalya, [84:22] John, Chris, William, so on. [84:29] This entry is persistent. You [84:33] can imagine me doing a to-do [84:34] list application on top of this. [84:37] You can have stream on one side [84:40] and text input on the other side. [84:42] There's tab commands that you [84:43] might be able to do. ink-tab is [84:46] a tabable interface. Let's show [84:48] you the demo, because the demo [84:54] is cool. [84:55] You can tab. Look at [84:56] this. That's not it. Look at [84:59] this, you can tab, and then [85:00] change the core content over [85:02] here. You tab on the side panel, [85:04] and you're navigating through [85:06] pages. Maybe it looks like a [85:08] router, I don't know. This is [85:11] interesting. [85:13] The focus transfer [85:14] between different components is [85:16] not worked out yet, so I would [85:17] not recommend this for that, but [85:19] that is clearly the direction, [85:20] that we have accessible user [85:22] interfaces inside of the CLI. [85:27] Everything looks very well- [85:29] positioned and well-presented. [85:32] You have full declarative [85:34] control, on a component level, [85:36] of what you're outputting. [85:38] That's the galaxy brain version [85:41] of how you do React Ink [85:44] components. [85:46] That is it. We have [85:48] covered a hell of a lot with [85:51] input, output, parsing, [85:53] scaffolding, child processes, [85:55] update-notifier, story state. I [86:01] can talk all day long about the [86:03] future of CLIs. I can talk about [86:06] some notes, if you want to [86:08] really go deep. [86:10] Really CLIs are [86:11] just the interface layer. [86:12] Talking about input, output and [86:15] display, presentation, polish [86:17] that kind of thing. Everything [86:19] else inside of that logic is up [86:20] to you. That's your Node.js [86:22] logic running on file systems, [86:25] survey access, whatever. It's [86:29] really up to you. [86:31] You may need [86:32] to reach out to the Node TSE [86:34] working groups. The Node.js [86:35] tooling group is very interested [86:36] in talking to people interested [86:38] in tooling on top of Node.js. [86:41] I [86:42] recommend checking out some of [86:43] the courses. If you want to go [86:46] deep on streams, you want to go [86:47] deep on some of the core Node [86:50] APIs, going to check out [86:52] Frontend Masters as well as some [86:53] again lessons over here. [86:55] There [86:56] weren't too many on ahead which [86:57] is why I proposed this lesson. [86:59] Hopefully, this covers a lot of [87:01] different cool things that you [87:02] can do inside of Node.js, for [87:03] CLIs. There's that. [87:07] Oclif [87:07] itself is a framework that [87:09] continues to see the most [87:11] investment. They just had a [87:12] conference this year which I [87:14] spoke at, and you can see the [87:15] creator of oclif. A bunch of [87:18] other like Stripe and Twilio and [87:20] other big players also talking [87:23] about how they use oclif. [87:26] There's that. Will Johnson I'm [87:30] going to get to you in a second. [87:32] Finally, I also have a chichi [87:33] which is the source of all of [87:36] the wisdom that I've accumulated [87:38] over the years, over surveying [87:42] all this tooling. There's [87:44] alternatives to basically [87:46] everything. [87:47] Feel free to come [87:48] check out some of these links, [87:50] contribute anything that's [87:51] missing. It's really up to you. [87:54] I like this one, Sign Bunny. [87:57] This is a cool one. We actually [87:59] lobbied to get this in to...Look [88:03] at this. It says sign bunny. NPX [88:06] Sign Bunny CLIs are great. [88:14] I [88:14] hope my Internet connection is [88:16] fast. Yay! [laughs] Such a kick. [88:27] I'll get to your questions in a [88:28] sec. Then I just wanted to end [88:30] off with I just want to inspire [88:31] you. This is still a very early [88:34] field. There's no such [88:36] profession as CLI engineer yet. [88:40] Given the amount of investment [88:42] available in the field and in [88:45] developer tooling and in [88:46] developer-focused companies, [88:48] it's clear that this is a [88:49] developing industry. There's a [88:52] lot of opportunity for growth. [88:55] Just when you thought you knew [88:58] everything that was possible to [89:00] be done in CLIs, this thing came [89:03] along which I found today. I'm [89:06] going to add Ink-gradient and [89:08] Ink-big-text. Then I'm just [89:10] going to copy this little, small, [89:12] little command. [89:16] I'm going to [89:16] paste it into the Ink command [89:18] that we've been working with. I [89:22] feel like I can just end of the [89:25] workshop with this call to [89:28] action which is, drum roll, if [89:35] that ever [laughs] gets there, [89:36] go make CLIs. Yeah, that's a [89:39] nice call to action, right, if I [89:42] can reduce the font a little bit. [89:46] [laughs] [89:46] I can take questions. [89:49] Johnson's question is, "Why [89:51] again would you caution against [89:52] using React Ink with spinners?" [89:53] Basically, because React Ink [89:55] wants control over the stdout. I [90:00] want to output to the screen, [90:04] and then cli-spinners, or ora -- [90:06] actually, ora is the main [90:07] package that you want to think [90:08] about -- ora also wants to [90:09] output to the screen. Both of [90:11] them are going to output [90:12] together. [90:13] React Ink and ora, [90:15] both rely on having a strict [90:17] reference to, "This is the line [90:19] that I'm updating." When I do [90:21] animations or when I do re- [90:23] rendering, I can clear that line [90:25] and then re-render again. [90:27] When [90:27] I have crossing of the streams [90:29] like that, the line becomes [90:30] static, frozen. It would create [90:33] a new line and then re-render [90:35] from there. You get that [90:38] intermixed, very ugly visual [90:42] when you mix control streams [90:44] like that. [90:46] Hopefully, that [90:46] answers your question, Will, [90:48] that intermixing of the streams [90:52] doesn't work naively. That's [90:53] because we don't engineer for [90:56] that situation. [90:57] We could [90:57] engineer in such a way that we [90:59] have a sequence, we have central [91:01] store of, "Here's what everyone [91:03] has control over, and whenever [91:06] anyone needs to re-render, we'll [91:07] clear the preceding stuff first, [91:09] and then we'll re-render that [91:10] stuff, and then we'll reinstate [91:11] that preceding stuff." We need [91:15] some coordination between [91:16] libraries in order to take care [91:17] of that cooperation effort. [91:24] "At [91:24] what point in your development [91:26] of CLIs do you start reaching [91:27] for something like React Ink? Do [91:28] you start with oclif and switch [91:29] to something nicer like React [91:30] Ink, or just start with the [91:31] latter?" [91:32] Pretty much the way [91:34] that I've gone through in this [91:36] workshop, is exactly what I [91:37] would recommend. Start with [91:38] oclif, keep it simple. React Ink [91:40] is newer. I have run into bugs [91:43] writing this, like with the [91:44] layout and all that. It'd be [91:47] annoying to debug. The whole [91:49] point is to save productivity. [91:52] All of this stuff at the end is [91:54] nice polish. You want to make it [91:56] look professional and then [91:58] pretty and all that. [91:59] If you [91:59] want just core functionality, go [92:00] with something that's tried and [92:01] tested and deployed in [92:03] production, at scale. What my [92:07] point of trying to prove to you [92:09] is, you don't have to choose. [92:11] You can start with oclif, write [92:13] the commands, and if you want to [92:15] swap it out with a new rendering [92:16] layer, like React Ink, it's a [92:19] simple one-liner. You have the [92:21] code for it now, you've seen me [92:23] do it multiple times. That's it. [92:27] Any other questions before I go? [92:30] I can also talk a little bit [92:31] about the future of CLIs. Feel [92:40] free to use the microphone as [92:41] well to jump on in, because I [92:42] think we're at the discussion [92:44] stage of the chat. [92:49] A couple [92:49] notes on the future of CLIs, and [92:51] then we'll call it a day. [92:57] Everything that we've written is [92:58] in TypeScript and JavaScript. [93:01] That is obviously not the [93:02] fastest runtime. There is a [93:04] project to rewrite everything in [93:05] Rust. There's a prediction that [93:08] the core of everything that we [93:10] use, all the hot paths, should [93:12] be rewritten in something that's [93:13] faster anyway, because we run it [93:15] so much. [93:16] Here's an alternative [93:18] to Babel written in Rust. It's [93:19] 16 times faster, because it [93:23] doesn't have the overhead of V8. [93:27] That makes a lot of sense. [93:33] Speed is one concern, and then [93:34] UI is another concern. The [93:37] question is, we're creating all [93:39] this UI inside of the CLI, maybe [93:40] we should just launch Electron [93:42] app. Vue is leading the way on [93:44] that. Vue CLI has a UI on top of [93:47] the CLI that drives the CLI. [93:50] It's a one-to-one correlation [93:52] with what they do. [93:54] Here's the [93:54] Vue CLI UI. Literally, is you [93:59] can add dashboard you add [94:00] plugins just by clicking point [94:02] and click point and click, add [94:03] plugins. It's just a more [94:05] beginner friendly experience. [94:07] You can see the log streaming in [94:08] from here and you obviously do [94:11] something that works across as [94:12] well. [94:13] The argument is that this [94:14] is faster or just you can use [94:18] real CSS and real visual assets [94:22] to show inside of your UI. If [94:25] you're going to make all these [94:26] UIs anyway maybe you might as [94:27] well just want to run something [94:29] on the screen. There is an [94:31] argument for that. There's an [94:32] argument for not switching [94:33] between terminals and back and [94:35] forth. [94:36] It's totally up to your [94:37] design traces that's what the [94:38] Vue community has done. I really [94:40] respect them for it because it's [94:42] a big undertaking. Dry run CLIS [94:45] these are good ideas for [94:46] anything expensive. Other times [94:49] like sometimes when you need to [94:51] debug your webpack config or [94:53] your rollup config is to just [94:55] run them. [94:56] That's really [94:57] annoying especially if you don't [94:59] know if you've configured things [94:59] right. If you add a dry run [95:02] feature then you just test like, " [95:04] I'm going to run this this this [95:05] this" It's kind of planning [95:06] without actually executing. You [95:08] can run the things a lot faster [95:10] and you can report any errors [95:13] and warn on project structure or [95:16] anything like that. [95:17] It's a [95:17] little bit...I actually compare [95:19] and liken it more to database [95:21] world where before you execute [95:23] the query, you do query planning [95:24] and then you do query [95:25] optimization. You vent against [95:27] anything of that. Once you have [95:29] a valid query, then you execute [95:30] it. [95:32] We jumped straight to the [95:34] execution part but maybe we [95:35] should actually have a dry run [95:36] piece. That's the inspiration [95:40] for the dry run movement in CLIS. [95:44] Then the last bit is my sort of [95:46] think piece bit on adaptive CLIS. [95:50] This idea is that a lot of the... [95:53] This is a talk that I did. You [95:54] can go check it out on what I [95:56] mean by adaptive state machines. [95:59] Basically, the idea is that I [96:00] have all these if then else [96:02] statements. [96:06] Let's say I have [96:07] five different requirements and [96:08] I have like, if this and that [96:10] and then do this and if then [96:12] else if then else if then else [96:13] in a one long script, that's [96:15] basically doing a bunch of [96:18] resolution edge cases. If you do [96:19] this enough in one command and [96:22] the next command and the next [96:22] command, you want to extract [96:24] them and put them in a different [96:26] utils folder. [96:27] Then you want to [96:29] make them intelligent like have [96:31] some logic jumping between like, " [96:33] If this is done, go check the [96:34] next thing, go check the next [96:35] thing and make it a reusable [96:36] library." Pretty soon, you'll [96:38] have a pretty adaptive logic [96:40] that's says, "If this field is [96:43] required and is not available, [96:45] prompt for it and then move on [96:48] to the next thing or the next [96:49] thing." [96:50] That's the adaptive [96:51] CLIS. I actually also have... [97:00] It's a state machine in the [97:01] sense of you start here and you [97:03] want to end state here. You [97:04] should use the state machine to [97:05] draw this where this pass and [97:06] then generate all the prompts [97:11] that are required. [97:12] Instead of [97:12] manually coding the prompts, you [97:14] let a framework take over and [97:16] you only declare the data [97:17] dependencies on that. Now I [97:20] started thinking that I started [97:21] based on this. I have NVP built [97:25] out but again nobody has time to [97:29] invest in all of this yet. [97:30] I do [97:31] think that some part of this is [97:32] in the future because this [97:35] doesn't really scale. This is a [97:36] lot of duplicative edge case [97:39] checking. The only way you can [97:41] really plan all this out is with [97:43] the state machine. [97:46] That's what [97:46] I'm thinking about. Cool. That's [97:49] it. I have a lot of private [97:53] thanks and people leaving. Thank [97:54] you so much for joining. I [97:57] really appreciate you. Please [97:58] come by to Repo and contribute [98:01] any questions and comments. This [98:03] is definitely a working progress. [98:05] Thanks very much.

egghead
egghead
~ 29 minutes ago

Member comments are a way for members to communicate, interact, and ask questions about a lesson.

The instructor or someone from the community might respond to your question Here are a few basic guidelines to commenting on egghead.io

Be on-Topic

Comments are for discussing a lesson. If you're having a general issue with the website functionality, please contact us at support@egghead.io.

Avoid meta-discussion

  • This was great!
  • This was horrible!
  • I didn't like this because it didn't match my skill level.
  • +1 It will likely be deleted as spam.

Code Problems?

Should be accompanied by code! Codesandbox or Stackblitz provide a way to share code and discuss it in context

Details and Context

Vague question? Vague answer. Any details and context you can provide will lure more interesting answers!

Markdown supported.
Become a member to join the discussionEnroll Today