Playing Next Lesson In
seconds

Transcript

  1. So far, we have been making use of Visual Studio Code's auto-import feature to import the various files and packages that we've been using throughout our application so far. If we take a brief moment here to actually

  2. inspect some of the imports that we have, for example, I'm within our movie model right now, we'll see something that we might not be familiar with, this hash services/cache service.

  3. Particularly, it's the hash services portion that you may not be familiar with. However, as we hover over this particular import, we see that Visual Studio Code is successfully able to map

  4. this back to a module at our code, let's learn AdonisJS 6 at services/cache service file, which is absolutely correct.

  5. We go at services/cache service, and that is indeed the file that we're importing here. So how exactly is that mapping happening? That's what we want to discuss here today.

  6. So this is using a feature called Node.js subpath imports. If we dive into the documentation here, so let's open up our browser and head to

  7. nodejs.org/api/packages.html/subpathimports. There's a whole section here talking about what these are. If we go ahead and scroll down here a little bit further,

  8. we'll see subpath patterns. This is the particular feature that AdonisJS is making use of to allow us to specify that hash services

  9. to then import from our app services directory. Within the example here, we see that we have an imports object,

  10. which has a mapping of hash internal/star.js. The hash internal is defining a subpath import namespace.

  11. The star is allowing us to specify any file within the internal directory that we're in turn pointing to right here. And then the .js is required

  12. because we're working with ECMAScript modules, so we need to specify the file extension. The right-hand side of that is then pointing to the underlying directory and the files therein

  13. that our import namespace should resolve to. So if we were to specify an import for hash internals/test.js, the underlying file would wanna be within

  14. source internal/test.js. If we scroll down just slightly further, Node.js will describe this within their documentation. So you have an import for various features

  15. from es-module-package/features/x.js, and then they'll have the underlying resolved package from that import right here. If we scroll up a little bit further,

  16. they also describe why the hash is there. So right here, entries in the imports field must always start with a hash to ensure that they are disambiguated from external package specifiers.

  17. Essentially meaning it's a special character that allows them to know specifically that you wanna use a subpath import to import the particular file that you're working with,

  18. as opposed to like the at or ./ designation that you could be doing to import from Node.js or a relative path file. Cool, so if you wanna dig into this further,

  19. there's the documentation path right there. Let's go and switch over to our application now and hide our browser back away. So we know that these are defined within our package.json,

  20. so let's scroll down and take a look at our package.json. You wanna scroll down just a little bit until you see this imports object right here, where we see a number of different subpath namespaces defined.

  21. We have one for our controllers, exceptions, models, mails, services, listeners, et cetera. With every fresh AdonisJS 6 project, Adonis has set these up for us automatically so that we don't need to go in and manually define them

  22. whenever we get to the point where we're working with them. They know that these are most likely going to be directories that we need to work with at some point through our applications development. So they've gone ahead and defined them for us here

  23. so that we can make easy use of them. However, there may come a time where you need to define your own because you're using something to your own taste or specifications.

  24. For example, a lot of people like to use DTOs or ViewModels, so you could add those into your project and then define the subpath alias for those various import paths.

  25. So we're gonna end up getting rid of the work that we do within this lesson here. I just want to highlight how you can add your own subpath imports in should you need to. So let's roll with the ViewModel example.

  26. So within our app directory, let's create a new file and let's create a new directory called view_models. And within here, we'll create a new file called movie.ts.

  27. So now we have both a movie model and a movie view model. Now, as the name suggests, the movie model defines what a movie is per our database.

  28. The movie view model then defines what a movie is to our views. So they're just models with two different purposes within this particular example.

  29. So that we don't spend too much time on this example, I'm just going to go ahead and export default class, movie VM, and I'm just going to extend our movie model just like so.

  30. If we wanted to make it obvious which one we were working with, we can go ahead and add an additional property on here. So public type equals ViewModel, something like that. So now this type property only exists on our ViewModel

  31. and not on our movie model. Okay, next, let's go ahead and jump into our movies controller and let's make use of it. So instead of using our movie to query all of our different movies, let's go ahead and switch this over to our movie model.

  32. So we'll go ahead and just use automatic imports here and we'll scroll down to our movie VM right here and go and give that an import. And the first thing you'll notice

  33. is that this is a relative path import that it has added. And that's because we haven't defined a sub path import quite yet for our ViewModels directory.

  34. So it's telling Node.js to look for this file at ./ which goes back a directory from where we're at. So out of controllers and into app, into ViewModels

  35. and then into movie.js. Now the file extension is here as we described earlier because we're working with ECMAScript modules and the file extension is needed for relative imports for those.

  36. Now you might notice that this is a .js file despite the actual file being movie.ts. The reason behind that to my understanding is that it was a TypeScript decision

  37. because at the end of the day TypeScript compiles down to JavaScript and they didn't want to mix and match the file extensions. So that's why that is there to my understanding.

  38. Okay, so before we switch this over to a sub path import, let's go and give it a test just to make sure everything's still working A-okay. So let's dive back into our browser and give our homepage a refresh where we swap this in.

  39. Okay, good, everything's still working. We can hide that back away. And now we know that we need to define our sub path import within our package.json file. So let's dive down into there. And just after our models,

  40. let's go ahead and add in our view models. So we'll do a string hash view_models/* designating that the view models namespace

  41. will work for any file within here, colon. And now we can add in the value for this, which should be a relative path to the files. So add in a string ./, which is the start of a relative path

  42. coming out of our applications root. So if we scroll up a little bit, we'll want to go into app view_models/*.js to support any file within our view models.

  43. So we'll go app view_models/*.js. Give this a save. And now we can jump back into our movies controller

  44. and let's switch this relative import for our sub path import. So we can get rid of the ./ and replace it with a hash. And now we can just get rid of the file extension

  45. and voila, everything is happy so far. However, there is one more step that we need to take. Sub path imports are one area where JavaScript and TypeScript don't communicate too well with one another

  46. and TypeScript isn't actually going to read these imports from our package.json file. So we need to inform TypeScript about them separately. So we'll have kind of a one-to-one mapping

  47. in two different places within both of our nodejs package.json configurations and our tsconfig.json file. So if we scroll down to our tsconfig.json file,

  48. we will see a paths object like so with all of the same mappings that we have within our package.json file, except they're here now for TypeScript.

  49. So in order to make use of the sub path alias, we'll also want to define our view model sub path alias within our TypeScript paths as well. Everything here will look pretty much the exact same.

  50. The only difference is the value will be an array rather than just a string. And now we should be able to jump back into our browser,

  51. give our homepage a refresh and everything still works A-OK despite us now importing using sub path aliases for our movie view model. Okay, so within this series,

  52. we're not actually going to be working with view models. So we'll go ahead and get rid of what we've just done. But before we do, let's go ahead and summarize what exactly we covered in this lesson. So first, hashes within your imports

  53. designate sub path imports, and you can define your own, but you need to do so within both the tsconfig.json file and the package.json file.

  54. Within the tsconfig.json, it will be within paths. I'm gonna go ahead and get rid of our view model import here. And this paths property is specifically within the compiler options. Within our package.json file,

  55. it's gonna be within imports. So we'll go ahead and get rid of the view models there as well. If we scroll up, imports is just a baseline property within our package.json file.

  56. The package.json import is needed for node.js, whereas the tsconfig.json import is needed for TypeScript. You can do relative path imports. So if we go back a couple of steps here,

  57. this works A-okay, but you will need to add in .js because we're working with ECMAScript modules. And you'll need to use .js regardless if it's a TypeScript file. So we'll give that a save.

  58. And now we're ready to go ahead and just delete out of our view model. So we'll go into here, right click it and delete. And now it's gone. And then we'll need to remove the import and switch this back to just movie.all.

  59. Give it a save. Let's jump back into our browser one more time just to make sure everything's working A-OK. And there we go.

Easy Imports with NodeJS Subpath Imports

@tomgobich
Published by
@tomgobich
In This Lesson

We'll learn about NodeJs Subpath Imports and how AdonisJS leverages them to help simplify our import paths throughout our application.

Want to dig further into NodeJS Subpath Imports? Checkout the documentation referenced within this lesson here:

https://nodejs.org/api/packages.html#subpath-imports


Update

As of TypeScript 5.4, TypeScript will read and use subpath imports directly from our package.json, meaning we no longer need to redefine them inside our tsconfig.json file.

As such, AdonisJS has now removed these definitions from the tsconfig.json file by default and you only need to define them inside your package.json file.

Join the Discussion 10 comments

Create a free account to join in on the discussion
  1. @n2zb

    Hello Tom, what's the difference between imports using subpath starting with # and another starting with @ ?

    1
    1. Responding to n2zb
      @tomgobich

      Hi n2zb! Imports using the @ character, like @adonisjs/core are actual NPM packages/dependencies installed within our application. The @ character in NPM designates a scoped package. Scoped packages provide a way for the package maintainer to keep all their packages grouped together within the NPM registry. Additionally, it also provides some verification to installers as once a scope is claimed, one the user/organization that claimed the scope may use it.

      For example, since the AdonisJS Core Team claimed @adonisjs only they can register packages using the @adonisjs scope on NPM.

      The # imports, on the other hand, are an actual NodeJS feature called Subpath Imports. These are path aliases pointing to actual files within your project. Let's say we're importing a model:

      import User from '../models/user.js'
      Copied!

      Without using a path alias, we need to traverse to the directory our models are within, for example using ../ to go back a directory. We also need to use the .js extension, required by ES Modules as they resolved and cached as URLs.

      However, if we define a Subpath Import for our models:

      {
        "imports": {
          "#models/*": "./app/models/*.js"
        }
      }
      Copied!

      NodeJS will use this as a reference to fill in the blanks, allowing us to drop the relative paths to the files we're after and simplifying our imports.

      import User from '#models/user'
      Copied!

      Hope this helps clear things up!!

      1
  2. @nidomiro
    @nidomiro

    Hi,
    thanks for creating the series.
    I'm used to creating folders for features and storing controllers, services, … for that feature inside the folder. I like this approach, because it keeps things actually related to each other together.
    Is this also viable for AdonisJS or considered an anti-pattern? If it is considered an anti-pattern, what is the reasoning?

    1
    1. Responding to nidomiro
      @tomgobich

      Hi Nidomiro!

      Yep, that's absolutely a valid approach with AdonisJS! I haven't done it myself, but you can check out Romain Lanz's website which uses this setup. He is one of the AdonisJS Core Members.

      You'd need to move some files around to your liking manually, then update the imports within your package.json to match your feature names (if you want to use import aliases) rather than having one for controllers, models, etc.

      1
      1. Responding to tomgobich
        @nidomiro
        @nidomiro

        perfect, thanks :)

        1
        1. Responding to nidomiro
          @tomgobich

          Anytime, Nidomiro!! 😊

          0
  3. @news-zanndo

    Not present by default in tsconfig.json

    0
    1. Responding to news-zanndo
      @tomgobich

      Hi news-zanndo! Yes, as of TypeScript 5.4, TypeScript will now automatically use the subpath imports defined within your package.json, meaning we no longer need to redundantly re-define them inside the tsconfig.json.

      Apologies, I thought I had updated the body text for this lesson noting this, but evidently, I did not. Will do that now.

      2
      1. Responding to tomgobich
        @noctisy

        Thank you for the explanation!

        1
        1. Responding to noctisy
          @tomgobich

          Anytime, noctisy!!

          0