تماشای این درس نیاز به اشتراک حرفه‌ای دارد.

Introduce mix.combine refactor0:00

What's up, everybody? Today, we are working with JavaScript and the Laravel Mix codebase. I've recently added a little bit of functionality to the mix.combine command, but I haven't really cleaned it up, so in its current state, it's a big ol' mess. So in this video, we are going to refactor it together before I push it up to GitHub. Okay, but first, if you're not familiar with combine, what's that? Well, to demonstrate, you could call mix.combine and then provide an array of files or wildcards that will be merged together and then saved to this output file. And then for production, that will even be minified for you. So it's really great for older legacy projects that don't necessarily need to work with Webpack.

Extract glob helper1:14

All right, they're all passing, and here's what this component can do. Let's get started. All right, so where do we start? Usually, I look for low-hanging fruit. What is the most immediate seam I can introduce to clean up the code? And I'm not sure I'm doing this in real time here. There is one thing I see, though, glob.sync. So I see that repeated one, two, three, three different times. So if you're not familiar, glob allows us to effectively scan a directory. So for example, if we had, excuse me, if I had something like glob public/css, that's

So if you're not familiar, glob allows us to effectively scan a directory. So for example, if we had, excuse me, if I had something like glob('public/css/*'), that's going to give me all of the files recursively within this directory. So have a look here. For example, if I said mix.combine, we're checking if the source includes a star. So public/css/*. Then we're going to run glob on this file path, and that's going to return an array of file paths. That's what's going on here. But I see it repeated over and over.

That's what's going on here. But I see it repeated over and over. So let's see that one. There was another occurrence. So if the source is an array, yeah, notice we have different ways you can call this method. You can pass a directory to mix.combine, or it looks like you can pass an array to mix.combine. And if that's the case, once again, we call glob.sync. And then it looks like here, yeah, there's just a lot going on here. So the first thing I'm thinking is, let's just extract a method. And often, I don't even worry too much about what it's called.

So the first thing I'm thinking is, let's just extract a method. And often, I don't even worry too much about what it's called. If you want, you can even call it foo to get yourself started. But for now, we'll call it glob. And I'm just taking that repeated code, and I'm moving it here. So now the only dependency looks to be file. But file isn't quite right. We really want to glob a path. And if it's a path to a file, that's fine as well. So we'll do that, and then replace this with this.glob file, and reformat.

And if it's a path to a file, that's fine as well. So we'll do that, and then replace this with this.glob file, and reformat. Okay, so now I've made a change. I'm always going to run my tests after each update. Yeah, and immediately, we can see, I didn't even notice it, but I've made a mistake. Path.join is not a function. Oh, yeah, we already have path as a variable. So this is a node import. So why don't we change this to, yeah, it's amazing, tests proving themselves right out of the gate.

So why don't we change this to, yeah, it's amazing, tests proving themselves right out of the gate. So let's try that. And it passes. Great. So now, let's see if I can keep using this method. We have another case here. But I'm pretty sure if I just say this.glob file, I don't think it's going to work. And yep, it immediately fails. So what's the difference here?

And yep, it immediately fails. So what's the difference here? Well, in this iteration, we were saying glob.sync the file, whatever it happens to be. But down here, the method actually does a path.join. So this assumes that we are scanning a full directory. So it's basically assuming, well, you gave us a directory. So let's tack on * like this to give us all files within that directory. So how do we deal with this? Well, to start, it looks like if the file includes a *, then we don't want to tack anything on.

Well, to start, it looks like if the file includes a *, then we don't want to tack anything on. So yeah, we could start by saying filePath.includes('*'). If so, then just stick with what you currently have. Otherwise, do path.join. And that's just going to build up the path that I just showed you. So we'll give that a reformat. And with any luck, I think we'll be back to green. There we go. So now I can remove that.

There we go. So now I can remove that. And there should be one more case of glob. Yeah. Yeah, this is a big old mess. We have some work to do here. So here's the last case where we do, yeah, look at this. It's kind of the same thing. So we're seeing some repeated code. So that's already a sign that this could be much cleaner than it currently is.

So we're seeing some repeated code. So that's already a sign that this could be much cleaner than it currently is. So I'm going to say this.glob again, source. And is that OK? Yes. OK. So now I have a dedicated method that scans a directory. And we might even want to call it scan at some point. All right. What else?

Reduce conditionals strategy5:52

All right. What else? I'm doing this with you. What jumps out at you? Of course, the things that really jump out at me are the conditionals. So if you think about it, a pure function is a function that accepts input and returns a result. So add is such a common one. Return 1 plus 2. This is the purest of functions we could create.

Return 1 plus 2. This is the purest of functions we could create. There's no external dependencies. We have some input, and then we return a result. But once we have these conditionals, it adds layers of complexity because you're now introducing different paths through the method, right? So for register, we have a path where all of these conditionals fail. So we never run this code. We never run this code. We never run this code.

So as a general rule, the more conditionals you add, the more likely that bugs will be introduced and the more complexity that will be added to it. So often when I'm refactoring code, I'm trying to reduce the conditionals. Or if it helps, think of it as trying to reduce indentation. The more indentation you see, as a general rule, the more complexity, because that means there are nested conditionals, as you see here. So if you can focus on reducing indentation, you will drastically improve the file and reduce complexity in the process. Okay, so let's start right up here. It looks like when you call mix.combine—remember, that's going to concatenate files—it looks

Clarify babel option7:39

Okay, so let's start right up here. It looks like when you call mix.combine—remember, that's going to concatenate files—it looks like there's a third option called babel, which means, do you want to run babel over it? Or do you want to compile modern JavaScript to vanilla JavaScript? We accept that. So we default to what you give us there. Or if the caller—so we have an alias for mix.combine called mix.babel—it just does the exact same thing. So if the user called mix.babel, we want to explicitly turn that on.

the exact same thing. So if the user called mix.babel, we want to explicitly turn that on. Otherwise, if they just do combine here with a source and an output, they could manually turn that on if they want. A little tip, I don't love Booleans like this because you may know what it means, but your users won't know, which means they have to dig down to figure out what that refers to. So often in these cases, just add to your API to make it more clear. So that's specifically why we have babel. That means you can do the exact same API, but you don't have to provide a confusing Boolean.

Normalize source inputs9:12

I feel like I can eventually get rid of this, but I'm not sure how yet. Okay, fair enough. We'll come back to that in a minute. Next, I don't love, we're handling the code differently based on whether you gave us a string or an array. Ideally, I want to assume that you gave us an array, and if you didn't, let's normalize it to an array. So if you gave us a string, let's just turn it into an array and then proceed as if you had given us an array all along. That's what I'm thinking.

had given us an array all along. That's what I'm thinking. So let's see if we can do that. If type of source is a string, let's just go through this together. If the string includes a *, meaning it's a wildcard, then we are going to re-trigger this register method, but instead pass it a glob of the source, which is going to be an array. So we're already doing that. Does that make sense? If you give us register with a string, we check for it, then we find all the files as

Does that make sense? If you give us register with a string, we check for it, then we find all the files as an array that match that wildcard, and then we recall that method with the new array of source files or paths. So on this next iteration, think about it. We now have an array, so this never gets called, and it moves on here. And by the way, notice here, if the source is an array, that's about as redundant as I can imagine, so we don't need that. Okay. Now coming back up, if the source is a string, but it's not a *, if it's not a wildcard,

Okay. Now coming back up, if the source is a string, but it's not a star, if it's not a wildcard, we then check, well, is it a directory path? And if so, yet again, we are going to call register, but just pass that path as an array. So here's what I'm thinking. Do we have to check if it's a directory? I'm not sure. Let's just try this out. Can I remove that and just say, well, if you gave us a star, then let's get an array. Otherwise, whether it's a file path or a directory path, let's just re-trigger the

Can I remove that and just say, well, if you gave us a star, then let's get an array. Otherwise, whether it's a file path or a directory path, let's just re-trigger the register method, but now turn source into an array. All right. I have no clue. Does that still work? Nope. Yep. All that talking and it didn't work. Maximum call stack.

All that talking and it didn't work. Maximum call stack. So this means we are calling the register method recursively. So let's see. You know what I think it is? We turn it into an array, and then we call this method, and then we have this section here that says, well, if it's an array, then iterate over it, and then call the method recursively again, but give it a string. So yeah, it's going down here, it's going back up. It's given an array.

Let's have a look. There we go. That works. Okay. We were getting too fancy here trying to trigger the register method again. So with that in mind, do we have to do it here? Let's just comment that out, and then say source equals this.clob source. All right. Fingers crossed. No, it didn't work.

Okay. So now we're getting rid of some of that confusing recursive calling. So next, I think I can clean this up further. Either way, we are setting source. So the source path will be equal to, well first, check if it includes a *. If so, this.clob source, so I'm just using ternary for now, but that should allow me to get this gone. They're kind of cheating because it's still, ternary is still an if-else, have no doubt, but I'm at least cleaning it up a little bit, and then maybe I can shorten it further. For example, often when I'm doing these refactors, I sort of want to normalize everything.

So even though it wasn't necessary to run glob, I'm going to do that because now, have a look, I got rid of that if else, so I removed two branches there. Kind of cool, right? Now at this point, if array, so if we're dealing with an array, well, you would never not be dealing with an array at this point because we check, well, we have an assertion that will throw an error if it's anything other than a string or an array, but then we have a check here. If it's a string, then turn it into an array. So I think this check in this conditional and this indentation is now superfluous. So get rid of that, reformat, run it again, and we're good.

Collect is actually a node implementation of layer of our collections. So I like that. So we are turning that array into a collection and then calling flatMap on it. So that will map over it and then flatten the results into a one dimensional array. So let's have a look here. Assume this is now an array of source file paths. For each one, we're going to check, well, does that file include a *? Is it a wildcard? If so, let's do glob on that file. So this is allowing for, let's see if I can go back.

If so, let's do glob on that file. So this is allowing for, let's see if I can go back. Yeah, it's allowing for this. So it's checking for each file. Well, do we have a wildcard? If so, run glob on that path and return an array of all files that match it. So now if I switch back, that's why we're running flat at the end, because otherwise you would have an array of nested arrays. And I don't want that. I just want a single level array of all matching file paths.

It's excluding the output. So that's probably to deal with situations where your output is the same as, or your output file is in the same directory as your source path. So that's a precaution. That returns an array, and then it flattens it all down into a single array, if you want to have a look at that, because it's kind of confusing. Let's give it a run, and we'll see this output for a bunch of them. Yeah, so either way, you're getting one level, or a single level array of all matching files there. Can we clean this up?

So it's checking, did you give us a wildcard? Then we're just going to glob what you gave us. But otherwise, we're going to grab all files. We're going to assume it's a directory. So I'm thinking that's what's wrong. Let's think about it. This is only temporary. What are the possible paths here for your glob? If it's a file path, then there's nothing to glob, right? So just stick with, let's say we're dealing with a source here, just stick with the original.

If it's a file path, then there's nothing to glob, right? So just stick with, let's say we're dealing with a source here, just stick with the original source file. But if the source you gave us is like a wildcard, then that too would be the source, right? Because it would be public/css/*.css, just glob whatever you gave us. In this case, it would be public/css/foo.css. In this case, we're going to pass that to glob too, for the reasons we talked about earlier, even though we're not doing anything with it, it's just going to return an array. And then finally, if it's a directory, then, which would be public/css, then we do want to say path.join, or we want something like this, source plus like that.

And then finally, if it's a directory, then, which would be public/css, then we do want to say path.join, or we want something like this, source plus like that. Okay, so this is pseudocode. But the point is, it looks like the only unique situation is when you give us a directory, right? Otherwise, I think I can just stick with the original file path. So why don't we tweak this? You know what, I need to undo this, don't I? Okay. I'm getting too cocky here.

Okay. I'm getting too cocky here. Let's get rid of that. I want to get back to green, and then I'll make a different tweak. And then once that works, we will return to what we had earlier. Okay, we're back at green. So let's say let search equals, and I have this File class here, I could say, find the filePath. And then I have a method called directory or isDirectory. So if it's a directory, we do need to append path.join(filePath) like that.

And then I have a method called isDirectory. So if it's a directory, we do need to append path.join(filePath) like that. Now, if it's not a directory, then our search is just going to be what we said earlier, the original sourceFilePath. So I can get rid of that. And then let's say glob($search) reformat. And I don't know, let's see if that still works. It does. Okay. So did that change anything?

I can get rid of this, this, this. How good does this feel? Get rid of all of that. And it's getting a little better. Okay. Next, this glob file, but then here it's called filePath. We don't know what it is. So let's change that to filePath. Next, I'm, this is fine, but maybe, maybe all of this can go into glob. So it can be responsible for receiving a filePath and then figuring out what to do.

Next, I'm, this is fine, but maybe, maybe all of this can go into glob. So it can be responsible for receiving a file path and then figuring out what to do with it. So if we took that approach, this would then become source equals this.glob source, I think. And of course, we know that's going to fail immediately. Yeah. Okay. So let's see. Here's what I'm going to do. Once again, I'm going to extract a variable.

Here's what I'm going to do. Once again, I'm going to extract a variable. So I'm going to bring back search, even if only temporarily. And then I'm going to reproduce what we have here. So actually, let's change this to, well, to start source, but then we're going to say collect source. I'm just writing what we have in comments above. flatMap over the filePath, and then return, collect, and then we have to call glob. So this would be what we have here. Getting a little messy.

It allows you to tinker around a little bit. So I don't even know. This looks a little rough, but it always feels good when every single test fails. Cannot read property relativePath of undefined. Oh, well, apparently it didn't return. Maybe it was catching that somewhere else. Let's run that. No. I cannot, hmm, what's going on here? Cannot read property on line 70 of combine.

Hmm. I don't know at this point. Let's see if it works. No. Why can't it find the damn relative path? Line 70. You see this right now, and I don't, and it's annoying me. Hmm. We're calling glob. We give it the output.

Okay. Let's figure this part out. The path argument must be a type string. So combine line 65. At some point, we are giving glob.syncfile. Not source. There it is. I'm an idiot. All right. Come on.

Okay, so now notice we accept babble. That could be true or false. But then we do have this section where if you instead called mix.babble, we're going to explicitly turn it on, because that's what you want. But we never referenced it anywhere else at this point. So I think I could just say babble is itself, if you turned it on, or we're going to make it equal to whether this.caller is babble and reformat. So we're just going to do that check down there, which allows me to remove this conditional and I think that will be okay. It is, okay.

Extract minify and task methods28:14

and I think that will be okay. It is, okay. What else? We have this section here. So if the caller is minify, I'll give you a little tip if you're a Laravel Mix user. If you ever take a look at one of the underlying mix components, sometimes there are aliases. So in this case, just to the legacy of Laravel Mix, all of these methods are aliases of one another, but they just add a little more semantics, if that makes any sense. So you could call mix.combine, but if you're really just concatenating some scripts, we allow you to say mix.scripts and it does the same thing.

So you could call mix.combine, but if you're really just concatenating some scripts, we allow you to say mix.scripts and it does the same thing. Or we allow you to say mix.styles and it'll do the same thing. Now among those, we could say minify these files, which will take place during production, but it's still going to merge them and minify them. So it's just an alias. Notice what we're checking here. If you called mix.minify, well then we need to know what the output path would be. Think about it. If you said mix.minify source foo.js, well, we need to know what should be the file name.

Think about it. If you said mix.minify source foo.js, well, we need to know what should be the file name of where you're minifying it, because we can't overwrite foo.js, because then that would be the minified file. So what it does is it finds the extension and it replaces it with .min.extension. So effectively, you're going to get a minified file in the same directory called foo.min.js. That's what we're doing right there. So either way, this is all logic that handles the registration of minification. So with that in mind, let's just remove it. I'm not sure what we're going to need yet.

So with that in mind, let's just remove it. I'm not sure what we're going to need yet. Paste it in. It looks like it wants the source and the output. So let's do that. Source output. But then, oh yeah, we have this problem. Output. So sometimes we return this, sometimes we would return the output. In situations like this, sometimes what I'll do is instead store the properties on the

So sometimes we return this, sometimes we would return the output. In situations like this, sometimes what I'll do is instead store the properties on the instance. So let's see what that would look like. If I did this, output, and then this.babel, then I don't have to provide this here. We could instead reference the source. But you know what? I'm doing this. I'm getting out of sequence here. So be careful about your sequence.

I'm getting out of sequence here. So be careful about your sequence. You don't want to go off the rails too much. So I'm going to bring this back to what it originally was, and that passes. But now I'm going to introduce that new thing where we store it on the instance, and then I would have to update anywhere we call it, like that. This.source, and then make sure we update that. And then the same is true for the output. So let's do this quickly. This.output, like that, and then babel would be the same thing here.

So let's do this quickly. this.output, like that, and then babel would be the same thing here. Next glob, I'm not sure we even have to, maybe I'll keep that. I was just going to say glob can grab source and output itself too, but, hmm, let's just see how we're doing here. Does that still work? And is it even worth doing this? Okay, that works. So now if I were to try that thing again, where I say registerMinify, and we take all of that, move it down here, this.registerMinify.

So now if I were to try that thing again, where I say registerMinify, and we take all of that, move it down here, this.registerMinify. Let's see. Does that change anything? Now it fails. That must, dang, failure every single time. Yeah, so at some point earlier we were going to return, but now we don't return. I mean, we could just do this, but I don't like adding another piece of indentation, but would that fix it? Or we could check to see if registerMinify returned this.

This is where we add the task, right? So when we register it, we assign the properties, we handle registration of minification, and then basically we add the task. And you'll see down here it's called ConcatFiles. That is the name of the task class that handles the job for mix. So my instinct is to make that a method, like just add the task. And that way I can say addTask, and then maybe all of this can go in there. Maybe. Let's see. Does that work?

Let's see. Does that work? It does. It's only a basic file or method extraction, but at least it now has a home. I noticed that comment is now redundant. Okay. Register is looking a little better. And actually, on that note, with Babel, I don't think that needs to be there anymore. We could do that as part of the initial registration, like this. Babel or this.caller equals Babel.

We could do that as part of the initial registration, like this. Babel or this.caller equals Babel. And then down here, okay, good. So yet again, as I'm doing this, notice we're not doing massive refactors. We're doing micro refactors. But then you do a hundred of them, and the file becomes much easier to reason about. All right, let's go through all of this. First, sometimes I like these to line up a little better. It doesn't matter, but this looks a little better to my eyes. Next, we have our API.

So now, if we wanted to remove this, this might look gross. Let's see. collect. Accept this.output.relativePath.all. Reformat. And then, let's see. At this point, glob receives an array. So do we have to do that again? I don't think so. Which would mean I could just say source.

Run it? Yeah. There we go. So I don't love that we're doing collect twice. It doesn't matter. But let's see. If I did source.map over it, that will return an array, and then I could always just call... What is it? source.flat? Is that right?

So this is where we bring back our good old friend, concat. And I bet that works. Or just use collect. Yeah. But that would do it. Okay. So now is glob more targeted. It accepts a source, whether it's a file path or an array. Maps over it, finds everything it needs to, flattens it down. That's not too bad.

Maps over it, finds everything it needs to, flattens it down. That's not too bad. And then here, we move it up a level. Kind of the specifics of why we would exclude this. And actually, if I wanted to remove collect entirely... Let's see. This.glob... That's an array. And then filter it. The only ones I want are the ones where the filePath does not equal the outputPath.

Well, that solved itself. So let's have a look. Register. We set up the source and output path and whether to perform Babel compilation. We assign that to the instance. We handle the situations where you call minify. That's now in its own logic. So if I ever need to update that, I have one method to do so. Otherwise, go ahead and add the mix task. We do that by turning output into a file instance.

Otherwise, go ahead and add the mix task. We do that by turning output into a file instance. We perform some basic assertions. And then we add the task. The task requires an array. That should be all files that need to be merged. So we defer to the glob method. That will scan your source path in whatever form it might take. It will glob any wildcards and then flatten the results. And that's it.

I mean, how many times have we done that? But it allows me to turn that into one line, which might clean it up a little bit. We were talking about this earlier. Sometimes the temporary $variable does help. I think sometimes people are so militant about removing them that it makes the code harder to reason about, which is not the point. But in this case, the variable or the logic is long enough, I think it might be worth it. Or, again, make that a method, you know, glob or searchQuery, globQuery. You know, sometimes you want to do things like this, where that would accept the file

Or, again, make that a method, you know, glob or search query, glob query. You know, sometimes you want to do things like this, where that would accept the file path. Nothing wrong with creating another method. So then you could say, this.glob.query. Is that okay? No, it fails. Oh, yeah, let's just return. So again, I'm just kind of playing around here. We're pretty much wrapped up.

دوست دارید گاهی خبرهای Laracasts را ایمیل کنیم؟