Still haven’t found what I’m searching for

Just a quick TIL post this week.  Don’t worry, I’ve got another rambler lined up for next week involving converting data for display then to another form for sorting, then back again- what a ride!

I thought the search function was complete.  It had a bit of complexity to format the content being searched correctly, but the ‘guts’ were fairly simple, using Javascript’s built in string.search method to identify a the submitted substring within a larger object’s group of properties.  This is for an Angular application, so throw it all in an observable and watch your grid/list filter as you type!

Side note: we are only searching through content that’s already been loaded from the server, so it’s nice and fast, but if we were hitting the server on search, there’s a great rxJs operator- debounceTime.  Pass it a number and it will wait that period (in milliseconds) after your event before actually firing the action.  Basically, chain it to your subscription before your .subscribe operator and you’re good to go!  Super simple client side rate limiting!

Back on topic: the search worked, except when I tested with a special character.  The dollar symbol (“$”) in this case.  The grid had a column where this symbol would regularly appear in the text, but each time I tried to search for “MM$vx”, I would get no results- even if that text appeared in my grid.

Turns out- string.search doesn’t appear to work with some (or maybe all?) special characters inside a string.  I did a quick Google search as to why, but didn’t come up with any specific reasons for this so I might be totally wrong, but I did test this out in an awesome little tool at repl.it – it’s great for quickly checking if code works like you think it should.


const test = 'U$11M'.toLowerCase();
const nope = test.search('u$11m');
const works = test.includes('u$11m');
console.log(nope); // -1 not found
console.log(works); // true
console.log(test, 'u$11m', test === 'u$11m'); //same string

Interactive version at https://repl.it/H5AJ/2

You’ll see the solution in that snippet too- switch to string.includes.  That method doesn’t have any issues with any symbol I tested so far, so that’s what we’re using now.  I’ll be honest- I didn’t even know string had an “includes” method- that’s why we were using search!

One thing to note: string.includes returns boolean, while string.search returns the index of your substring, so be sure to do your checks for the proper thing (particularly when using the all powerful === check).

Staying on Script

I like npm scripts.  I’m sure real programmers can memorize long strings of command line gibberish without even waking up the full 10% of our brain capacity movies tell me is all we have access to, but I mess those commands up all the time.

Don’t get me wrong- it’s fun to be able to cruise around a system on the command line.  You feel like you really know secret stuff.  I like that you don’t really even need a monitor to get real work done on a server- just a secure login method and some useful commands.  ssh in, check out where you are and where you can go (ls -la), change directories and do it again (cd var/www && ls -la).  See something you want to check out?  Cat is your friend (cat package.json).  See something you want to change?  Use Vim (vim package.json).  Though be sure to install it first (sudo apt-get vim -y) and be ready to look up the commands just to get around the page (note: I am not a Vim expert- I only use it when I have no other option).

But when it’s time to get to work, it’s really nice to just be able to type ‘npm start’ and have your dev environment spin up.  Particularly when you’re a less than stellar typist.

Npm scripts just go in your package.json file- in the appropriately named “scripts” object.  They can be extremely simple- for example, a project built with the angular cli automatically sets up a couple: “test”: “ng test”, “start”: “ng serve”.  These just make it easier to find those (admittedly already simple) commands.  A developer will know they can run “npm start”, but if they’re not familiar with the angular cli, they might not know about “ng serve” right away.  Npm scripts work well as aliases- unifying those commands under one umbrella- and one less thing for me to try to remember.

You can extend those simple commands as well.  I made a simple edit to my start command: “start”: “ng serve –open”.  This passes the ‘open’ flag to ng serve, automatically opening my default browser.  There are flags for many other options- check out the docs!

But where Npm scripts have really shined for me is in abstracting away any directory changes and environment settings that may have to be done in order to fire up other aspects of a project.  Sure- when you’re just working on the frontend, it’s easy to remember that you run “npm start” from the same directory your package.json resides.  But one project I’m working on uses .net core as a backend API.  It runs locally, so one option is to fire up the full Visual Studio program, but this feels big and slow compared to VS Code now.  Luckily, .net core now comes with a command line interface.  I can run the command “dotnet run”, and my backend dev server starts right up.

But there are complications.  This executable is not run from the same directory as package.json- it’s technically a completely different project in a completely different folder.  An environment variable also has to be set to “Development” in order for it to run properly.

But Npm scripts don’t care!  A script will just take your commands and run them.  So, my new script is:

“backend-start”: “cd ../path/to/dotnet/project/folder && set ASPNETCORE_ENVIRONMENT=Development && dotnet run”
The path really is longer- this is just the way a .net project is initialized.  The point is- instead of typing everything to the right of the colon, I just type everything to the left (from my package.json directory).  The Npm script changes directory, sets my env variable, and starts the process- pretty cool and useful!  I created another to publish the backend and can now forget those file paths and environment settings and other commands.  More brain space for me!

Javascript’s Days of Future Past

Working with ES6 features when writing Javascript is really great.  It’s a pretty big update to the language, and smooths out many of the rough edges.  My original hesitation around using the new features was browser support- though that’s really no longer an issue thanks to the major browsers now rapidly adopting the updates.  Plus, just about all my projects these days run through some kind of transpiler (Typescript/Webpack for Angular or Babel/Webpack for React), so even some of those old browsers can be targeted (and really, Webpack is not that hard!).

There is one project (still tinkering- don’t judge too harshly!) that necessitates writing old ES5 style syntax, and it’s not much fun.  We’re looking at bolting on a countdown timer feature to an older system (that is in the process of being massively upgraded, but that will take time).  It’s pretty simple, but gets pretty ugly around the string templating aspect.

Anyway, that’s the project that got me thinking about strings.  In the old days, if you needed to include a variable in your string, fragments went into quotes, then out of quotes with a ‘+’ operator to concatenate your variable, then back into quotes.  If something you were adding onto the string had to remain a number type, you had to be very careful not to add two of them together back to back (or instead of 1 + 1 = 2, you’d get ‘1’ + ‘1’ = ’11’).

The introduction of template strings with ES6 really made this smooth.  The syntax reminds me of writing PHP- start a string with a backtick, then include text as normal.  When you want to include a variable, or some evaluated Javascript inside that string, start with a dollar sign and wrap in curly braces: ${myAwesomeVariable}.  End the string with another backtick.

But there’s always a but.  No matter how well designed or tested or coded your software is, someone will find a way to break it, misuse it, or complain about it.  That even applies to a software language itself.  In this case, trying to use the latest and greatest (ES6) with the old and busted (SQL queries over my database schema) caused a breakdown (of my code and my mental state).

The case: I’m currently working to update on old PHP/MySQL application to run on Node.  I’m using the awesome mysql node library to make calls to the database.  The database information can’t be changed- the application already has many users, and just dumping their data and starting over is not an option.

The mysql library lets me send those SQL queries to my database and gives me back the data I need (it even has a great ‘escape’ function to improve security on those requests).  But it doesn’t fix the difficulties with strings in SQL, and it doesn’t fix a 15 year old naming conflict in our schema.

When the application was created, a column in some tables was dedicated to the order of items (menu items, or pages, etc).  Unfortunately, this column was titled “order”.  Also unfortunately, “order” is a reserved keyword in SQL (used to sort your results).  In the old PHP code, the query string was just concatenated together over many lines.  It’s really a forest of single quotes, periods (the PHP string concat operator), double quotes, and backticks.  It’s hard to follow, so I decided to use ES6 string templating in my awesome new update.  But every query that included “order” blew up in my face.  And SQL errors are not exactly helpful.  Think: “You have an error in your sql syntax around ‘some random place in my query string here'”.

Turns out, if you have a reserved keyword as a column title, you need to escape it in a query with backticks.  You might be able to use single quotes, I’m not sure, but either way that won’t work inside an ES6 template string (at least, it didn’t work when I tested).

So, for requests to those specific tables, it’s back to queries like: “SELECT * FROM my_table WHERE id = ” + id + ” AND  ‘order’ = ” + order;  That’s a simple example- one query has to get the next available “order” value (the table wasn’t set up as auto increment), so first we have to get the max(order), then insert that variable into the “order” column.  Again, ES6 is awesome, but I’ve somehow found a way to have to go back to the old days!

The Refactor Factor 2: The Rerefactoring

A while ago, I wrote part one of this saga.  I can’t remember what it was about, and am too lazy to look it up, but the theme was refactoring.  I went back to some working code and made it (arguably) better.  At least slightly less crappy.  Today, I did it again.

The aspect of our application under the knife was the sorting function.  Most of our views are just table type grids of data.  These grids have headings (name, address, admin status, etc).  Click the heading and the grid sorts according to that criteria.

I’d created the sorting logic- generally pretty easy, I mean, Javascript has a .sort method built into Array.prototype- but there were complications.  The function would not know what type of data it would be sorting, so we will have to have branches for string and date and others.  Nor would it know the parameter(s) it would be sorting by.  It would have to toggle ascending vs descending sort (click the header once for asc, again for desc).  It would have to be pretty flexible with its inputs.

And- to be added currently, hence the need for the refactoring- the ability for a second sort criteria.  We found that with only one sort criteria (for example: name), the function worked great- as long as everything was unique.  However, what about a column like ‘admin status’.  That’s just a boolean field- only two possible values.  If a user clicked to sort by ‘admin status’, they’d get one listing the first time, but then might get a totally different listing on subsequent clicks.

To make a more stable sort order, we added a defaultOrder variable on each grid component’s class.  That defaultOrder would serve as the initial sort (when the view first loads), and be passed to the sorting service as the second criteria.  So, if a user sorts by admin status, the secondary sorting is done by the defaultOrder (example: name).  The output is consistent and less confusing.

Example time- and please know that I know this is still a bit of a mess.  As I refactored, I started breaking some of the logic out into helper functions, but found that this was creating more work and complexity than simply taking care of it within the .sort call.  I may take another look, but sometimes it just makes sense to write the code in a ‘top down’ type layout- though I know this isn’t often the case.

Version 1: A no good dirty mess (that still kind of works)

orderByParamAscending(param, group) {
    return group.sort((a, b): any => {
        if (typeof a[param] === 'number') {
        //numbers- simple subtraction sort
            return a[param] - b[param];
        } else if(param.indexOf('date') !== -1 || param.indexOf('time') !== -1) {
            //date from string- convert to date obj and compare
            const first = newDate(a[param]);
            const second = newDate(b[param]);
            return +first - +second;
        } else if (typeof a[param] === 'string') {
            //strings- check for empty
            let first = '', second = '';
            //convert to lowercase and compare number code
            if(a[param]) {
                first = a[param].toLowerCase();
            }
            if(b[param]) {
                second = b[param].toLowerCase();
            }
            if(first < second) {
                return -1;
            } else if(second < first) {
                return 1;
            }
            return 0;
        } else {
            //other (usually bool)
            if(a[param] < b[param]) {
                return -1;
            } else if(b[param] < a[param]) {
                return 1;
            }
            return 0;
        }
    });
}

I know- not pretty.  We’re using Typescript, which is awesome, but for a while I couldn’t get this to work.  TS would throw an error about a function (.sort in this case) not having a unified return value.  And that’s because it’s a bad function.  But you can shut Typescript up by providing the ‘any’ return type for a function- just know that this is an almost surefire indicator of bad code.

So- how could I make it better?  Well- instead of doing all those checks on each parameter, I could create a couple variables that would hold the value- then update them based on the type.  Also, .sort works well if you return 1, -1, or 0 depending on the result of your comparison function, so once the values are properly parsed, we can compare and return one of those 3:

Version 2: Slightly simpler, but still no secondary sort

orderByParamAscending(param, group) {
    return group.sort((a, b) => {
        let first = a[param], second = b[param];

        //check if this is a datetime param or string and parse so we can sort numerically

        if(param.indexOf('date') !== -1 || param.indexOf('time') !== -1) {
            first = +newDate(a[param]);
            second = +newDate(b[param]);
        } else if(typeof a[param] === 'string') {
            first = a[param].toLowerCase();
            second = b[param].toLowerCase();
        }

        //parsing done- time to compare

        if(first < second) {
            return -1;
        } else if(second < first) {
            return 1;
        }
        return 0;
    });
}

A bit easier to follow- and we can lose the ‘any’ return type- we are now always returning a number.  We handle all the formatting of the comparators (the ‘first’ and ‘second’ variables) before doing any comparing.

But the proposed additional functionality still isn’t there.  I initially thought it would just be a case of calling the function twice on the same array- the first time with the default sort param (‘name’ for example) to make sure the data is in the right order at the outset, then call it again with the new param (‘admin status’) to re-order, with the original as a fallack.  But this does not work- the first call has no carry-over to the second (which makes sense, but I had hopes for a simple solution!).

And, in the end, the solution wasn’t that complicated- it was just testing to make sure the sort returns the right order that was a hassle.  The only real changes were to the arguments passed and to the sorting logic at the bottom of the function- nesting the secondary sort into the initial in case of a ‘tie’.

Version 3: A little more complex than V2, but a little more functionality too!

orderByParamAscending(param, group, altParam) {
    return group.sort((a, b) => {
        let first = a[param], second = b[param];
        let altFirst = a[altParam], altSecond = b[altParam];
       

        //check if this is a datetime param or string and parse so we can sort numerically

        if(param.indexOf('date') !== -1 || param.indexOf('time') !== -1) {
            first = +newDate(a[param]);
            second = +newDate(b[param]);
        } else if(typeof a[param] === 'string') {
            first = a[param].toLowerCase();
            second = b[param].toLowerCase();
        }
        //do the same for the secondary sort parameter
        if(altParam.indexOf('date') !== -1 || altParam.indexOf('time') !== -1) {
            altFirst = +newDate(a[altParam]);
            altSecond = +newDate(b[altParam]);
        } else if(typeof a[altParam] === 'string') {
            altFirst = a[altParam].toLowerCase();
            altSecond = b[altParam].toLowerCase();
        }

        //parsing done- time to compare

        if(first < second) {
            return -1;
        } else if(second < first) {
            return 1;
        } else {
          if(altFirst < altSecond) {
               return -1;
          } else if(altSecond < altFirst) {
               return 1;
          }
          return 0;
    });
}

There are other aspects we added- a ‘sortDirection’ toggle, a check for null values passed to the sort to convert them to strings (the comparison was failing on null- but some values from the database were null), but this is the meat of the function.  And I’m sure there are still improvements to be made- programming seems a little more art than science sometimes.  It can be hard to tell when you’re ‘done’ with something.  I was ‘done’ on version 1, but version 3 is much better.  At some point, you just have to move on to the next task, or your product will never ship!

Pass the reference, please

The current landscape of Javascript, with all the fancy front end frameworks, is great.  It allows for rapid development of really cool applications.  But it’s important not to forget the basics.

On our Angular application, we get data from a database on many actions (shocking- I know).  To normalize the process (making sure the right authentication is passed with each request, that the data returned is in the proper format, etc), we created a service- “backendService”.  This wraps around Angular’s http module, so it returns an Observable to the component that calls to it.

Generally, that observable provides an array of objects- let’s say “companies” in this case.  The company will have a name, location, number of users, etc.  When our backendService fetches the company list, there are a few tasks we need to do with the data.

  1. There is a d3-driven chart for the top 5 companies by user count (the 5 companies with the most users).
  2. There is a drop down to jump to a list of that company’s users.

For task 1, we manipulate the data into the top 5 by user count and sort accordingly (most users to least within those 5).  For task 2, we just sort alphabetically by company name.

Experienced JS developers are probably already seeing where I’m going with this.  And I should have seen it too- but theoretically knowing that an original array is modified when a shallow copy of that array is modified is a bit different from managing that process in practice.

In the subscription to the data, we had simply assigned the response to the class ‘companies’ variable (an array initialized with no items) so it could be passed around to other methods and even other components/services.  Something like this (general idea only):

this.companies = []; //class variable
this.backendService.getData(urlBaseHere).subscribe(response => this.companies = response);

Works fine- as long as you don’t need that ‘response’ array for anything else.  When we added the d3 graphs, we (I) forgot to go back and check the source.  We just added a service to filter and sort this.companies to get the top 5 by number of users.  It wasn’t even in the same JS file.  It’s generally best practice to break out your code into small, reusable components, but there are some drawbacks, particularly when working on a team.  If you’re sharing a reference to an array, one file may manipulate it one way while another is trying to do something different.

So, when we added the chart code (order by number of users), I found that I couldn’t sort the drop down alphabetically anymore).  Initially, I blamed the back end.  I started on a PHP/MySQL stack, so I know a bit of SQL, but this project uses SQLserver- the Microsoft flavor, and I’m less experienced with that specific brand.  I thought maybe the default sort I put on it (ORDER BY company_name) was flawed.  I like working on the backend, but with a language like C# (that is, a compiled language), it’s a bit slower development process.  Make a change, kill the dev server, re-compile, and restart the server.  I know- it’s not a big deal, but again, the cool frontend frameworks have spoiled me with their ‘hot reloading’ and ‘automate all the things’ awesomeness.

Anyway, after wasting the better part of an hour researching MsSql syntax and discovering it was not the culprit, I went back and double checked the component getting the actual data.  When I logged the ‘response’ parameter directly, I noticed that it was not giving me the array sorted by company_name- it was giving me the array sorted by number of users, not alphabetically by company_name.  I’d manipulated this.companies and therefore manipulated the base array returned.

The answer was easy- though there are a few options we could have gone with.  We just initialized two arrays: this.companies and this.companiesForChart.  Then, we assigned both to the response before doing any manipulation.

this.companies = response.map(company => company);
this.companiesForChart = response.map(company => company);

Using .map ensures that each is a deep copy of the array- not just a copy of the reference, and also reminds anyone else using the code to use the same process (and that they can do any manipulations there as well). Each one gets a reference to the original data, but this.companiesForChart is free to do it’s own thing and not mess with the default sorting (alpha).  It’s passed off to our chart service file where it’s .filter and .sorted as much as we need. Another option would be to use the slice method:

this.companies = response.slice(0);

Although we didn’t test this- I think it might also cause issues when we manipulate this.companies, so the need to have the second array (this.companiesForChart) would remain – also assigned to response.slice(0) in this case. Either way, it made a bit more sense to me to have two copies of the array, as there were two tasks and two different files using those arrays. It might be a little slower to initialize 2 arrays (not sure on that), but for other people on the team looking at the code, this makes it a bit more explicit exactly what each one is for.

So- the moral here is: don’t forget the basics.  When I logged the ‘response’ to the console (right after it was retrieved from the backend) and saw the array ordered by number of users, I assumed the problem was with the backend sorting.  How could anything in my front end code manipulate the array that just came back?  But the nature of JS arrays was the real issue- actually, me forgetting about reference types vs value types was the real issue.

Post Truthy World

Don’t worry- this is not about to get political.  Unless you get really fired up that Javascript equates zero to false.  If you’re a real zero-rights advocate, trigger warning.

If I remember correctly, a little while ago I went off on a tangent about ways to pass default parameters to a function in Javascript.  One of the ways mentioned (back before the glory of our new ES6 overlords) was to check for a parameter’s existence at the top of a function, and if it’s not there, assign a default.

Something like:

function defaults(foo, bar) {
    const myFoo = foo || 'Foo default';
    const myBar = bar || 'Bar default';
}

And that generally works pretty well.  But it’s important to remember that this is the wild world of JS we’re talking about.  That logical or operator – the || bit up there- checks the ‘truthiness’ of the param.  That means, if it’s an empty string, or undefined, or the boolean false value, or many other options, it will evaluate to false and grab the second value.  When it doubt, check MDN (Truthy, Falsy).

I’m usually pretty good about making sure this is going to work, but got a bit overconfident on a recent project.  Another throwback to an old post- I was working on a pagination feature for our Angular application.  One method in the service was called setGrid- this is what any other component could call when a page number was clicked, new data came in, or a filter/search was applied to the page, and it would update the page display and other page listings.  It went something like this (remember we are using typescript and this is inside an ES6 class:

setGrid(start: number, length?: number) {
    let sentLen = length || this.prefilteredItems.length;
    this.paginationService.setGrid(start, sentLen);
}

It seemed to work great, so I moved on to other aspects of the project. No one else noticed anything either at first.  But the trouble was with a search/filter that yielded no results.  If there were no items found, nothing displayed in the grid (as it should function), but the page numbering and links below would display the previous search numbers (or the full list numbers, which the ‘prefilteredItems’ array defaults/resets to).

And that was because the number zero is a perfectly valid result.  Sometimes, the length parameter would be zero- as in ‘no results found’- and there should be no pages or numbering.  But when zero was passed to the function above, it just registered as ‘falsy’ and evaluated to the right hand side- giving the full list (or prefiltered list) total as length.

Once I noticed that issue, the fix was easy: Check for undefined specifically.  The final version looks more like this.

setGrid(start: number, length?: number) {
    let sentLen = length;
    if(length === undefined) {
        sentLen = this.prefilteredItems.length;
    }
    this.paginationService.setGrid(start, sentLen);
}

It’s a bit longer and doesn’t look as slick without the || operator, but in some cases the shortcut just won’t work.  Sometimes zero is a valid submission.  As in the sign that should hang above my desk: “It’s been 0 days since our last bug created”.

Don’t Fear the Webpack

Complaining about things is fun.  My favorite one lately is that ‘getting a project started is so much harder now- back in the day, we just dropped a script tag at the bottom of the page and were off to the coding’.

And that used to be true- when ‘frontend development’ really just meant some form validation and simple page interactions and $ as far as the eye could see (jQuery, not money).  But the landscape has changed- grown, really.  Using modern frameworks and patterns, we can create much more interactive, responsive applications with much less work.  But with great power comes great responsibility- and in this case, that means a build process.

I dabbled in Gulp and Grunt- both were very useful- but was always intimidated by Webpack.  I think my early experiences with Angular 2 contributed to this.  Early on, Angular 2 configuration was done in SystemJS (at least in our project’s first iterations).  It was tricky and tempermental and tough to wrap my head around, but eventually, I was able to get it to work and even make useful updates.

Then we started looking at Webpack as an alternative.  I’d read some blog posts and it seemed to be the wave of the future- making things like lazy loading routes, minification, and even tree-shaking easy to handle.  But the process of manually migrating our existing project to Webpack proved to be a bit over my head.  We would need a dev server and a scss loader and a typescript config and a module loader- and this is just for the development process.  None of the awesome production build gains were even in view yet!  I had jumped in the deep end and am not a great swimmer.

When our team switched our project over to the Angular-CLI, however, Webpack came along with it.  No config necessary- the ng command takes care of that- and really does deliver on all the things (our production build version of the app became 3 times faster).  They don’t let you see the webpack.config file when you use the CLI- and that’s probably for the best in my case.  It takes care of so much that it would be quite intimidating to look at for someone just getting started with Webpack- and would be much easier to really muck things up than actually improve anything.  If/when the project is advanced enough that we need something the CLI can’t provide, then we’ll go digging.

Despite my initial intimidation, the performance gains were so good that I started looking at Webpack for my other projects.  My mistake: I started looking at config options others had used before really digging into the basics.  Sometimes this is great- getting started with Angular or React, I find it’s best to just generate a project and start experimenting with it/changing things.  It lets me see how the framework handles certain things- and if it won’t do what I want, then I can dig in to see what makes it tick.

But for Webpack to really ‘click’, I had to start from the beginning.  One current project on  my plate is updating an old php application to a node backend.  While we’re at it, we are also updating some of the frontend javascript (which was written circa 2003).  After a couple days, I discovered that it is possible to live without ES6, but I’d really rather not.  So, I installed Webpack and Babel and started from scratch.

This is part of the tradeoff I mentioned at the top of the post.  The time spent installing and configuring Webpack was not technically time moving forward with the project- no features were migrated to the new layout during that time- but the faster dev experience will more than make up for that in the end.  We get to use import statements and template strings and all that current generation javascript goodness.  Not to mention the possible performance gains when we incorporate a production build.

Webpack is for more than just single page apps, and doesn’t have to be complicated.  My project’s webpack.config file is current less than 20 lines and give me access to all ES6 and module loading (thanks to an assist from Babel).  With one line (and a couple node modules), I could add support for React/JSX transpiling as well.  It doesn’t have to be complicated- though I’m sure eventually it will.  If I want that great power (minifcation and tree-shaking and lazy loading and so on), I’ll have to extend this- but it sure was easy to get started.

module.exports = {
  entry: {
    adminPage: './static/js/src/admin/page.js',
    adminDashboard: './static/js/src/admin/dashboard.js'
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'static/js/dist')
  },
  module: {
    loaders: [
      { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ }
    ]
  }
};

One note on the above
In the output section, the ‘filename’ property seems a bit odd. That [name] in brackets is a cool tip I learned- it automatically grabs the name of the entry property.  So, if you have more than one bundle (we will have a different one depending on the page the user is on), you can tell the output to just create a bundle with those names.  For example, in this case, the config builds two bundles: adminPage.bundle.js and adminDashboard.bundle.js.  After that, it’s up to me to remember to put the correct script tag for each bundle in the correct html file and we are good to go!