node.jstypescriptes6-modulests-nodetop-level-await

Unable of get running a pure ESM node project


I've been days trying to get a pure ESM Node(express, graphql) project to run but no success yet, There is a lack of information regarding pure ESM and I give up

What is actually my next milestone is to get the paths working again in the project in the imports statements

What I am getting:


    [nodemon] 2.0.16
    [nodemon] to restart at any time, enter `rs`
    [nodemon] watching path(s): src/**/*
    [nodemon] watching extensions: ts,html
    [nodemon] starting `./node_modules/.bin/ts-node ./src`
    /xxxxxxx/src/server.ts:1
    Error: Cannot find module '@routes/api'
    Require stack:
    - /xxxxxxx/src/server.ts
    - /xxxxxxx/src/index.ts
        at Object.<anonymous> (/xxxxxxx/src/server.ts:1) {
      code: 'MODULE_NOT_FOUND',
      requireStack: [
        '/xxxxxxx/src/server.ts',
        '/xxxxxxx/src/index.ts'
      ]
    }
    [nodemon] app crashed - waiting for file changes before starting...


My package.json is as follows:


    {
      "name": "xxxxxxx",
      "version": "0.0.0",
      "scripts": {
        "build": "./node_modules/.bin/ts-node build.ts",
        "lint": "eslint . --ext .ts",
        "start": "node -r esm -r module-alias/register ./build --env=production",
        "start:dev": "nodemon",
        "test": "nodemon --config ./spec/nodemon.json",
        "test:no-reloading": "./node_modules/.bin/ts-node ./spec"
      },
      "nodemonConfig": {
        "watch": [
          "src"
        ],
        "ext": "ts, html",
        "ignore": [
          "src/public"
        ],
        "exec": "./node_modules/.bin/ts-node ./src"
      },
      "esm": {
        "await": true
      },
      "_moduleAliases": {
        "@services": "build/services",
        "@repos": "build/repos",
        "@entities": "build/entities",
        "@shared": "build/shared",
        "@server": "build/server",
        "@routes": "build/routes",
        "@graphql": "build/graphql"
      },
      "eslintConfig": {
        "parser": "@typescript-eslint/parser",
        "plugins": [
          "@typescript-eslint"
        ],
        "extends": [
          "eslint:recommended",
          "plugin:@typescript-eslint/recommended",
          "plugin:@typescript-eslint/recommended-requiring-type-checking"
        ],
        "parserOptions": {
          "project": "./tsconfig.json"
        },
        "rules": {
          "max-len": [
            "error",
            {
              "code": 100
            }
          ],
          "no-console": 1,
          "no-extra-boolean-cast": 0,
          "@typescript-eslint/restrict-plus-operands": 0,
          "@typescript-eslint/explicit-module-boundary-types": 0,
          "@typescript-eslint/no-explicit-any": 0,
          "@typescript-eslint/no-floating-promises": 0,
          "@typescript-eslint/no-unsafe-member-access": 0,
          "@typescript-eslint/no-unsafe-assignment": 0
        }
      },
      "eslintIgnore": [
        "src/public/",
        "build.ts"
      ],
      "dependencies": {
        "class-validator": "^0.13.2",
        "command-line-args": "^5.2.1",
        "cookie-parser": "^1.4.6",
        "dotenv": "^16.0.1",
        "esm": "^3.2.25",
        "express": "^4.18.1",
        "express-async-errors": "^3.1.1",
        "express-graphql": "^0.12.0",
        "graphql": "^15.3.0",
        "helmet": "^5.1.0",
        "http-status-codes": "^2.2.0",
        "jet-logger": "^1.1.5",
        "jsonfile": "^6.1.0",
        "module-alias": "^2.2.2",
        "morgan": "^1.10.0",
        "pg": "^8.7.3",
        "pg-hstore": "^2.3.4",
        "reflect-metadata": "^0.1.13",
        "sequelize": "^6.21.0",
        "type-graphql": "^1.1.1"
      },
      "devDependencies": {
        "@types/command-line-args": "^5.2.0",
        "@types/cookie-parser": "^1.4.3",
        "@types/express": "^4.17.13",
        "@types/find": "^0.2.1",
        "@types/fs-extra": "^9.0.13",
        "@types/jasmine": "^4.0.3",
        "@types/jsonfile": "^6.1.0",
        "@types/morgan": "^1.9.3",
        "@types/node": "^17.0.45",
        "@types/supertest": "^2.0.12",
        "@typescript-eslint/eslint-plugin": "^5.28.0",
        "@typescript-eslint/parser": "^5.28.0",
        "eslint": "^8.18.0",
        "find": "^0.3.0",
        "fs-extra": "^10.1.0",
        "jasmine": "^4.2.1",
        "nodemon": "^2.0.16",
        "sequelize-cli": "^6.4.1",
        "supertest": "^6.2.3",
        "ts-node": "^10.8.1",
        "tsconfig-paths": "^4.0.0",
        "typescript": "^4.7.4"
      }
    }


and my tsconfig.json:


    {
      "compilerOptions": {
        "target": "ES2022",                       
        "module": "ES2022",                       
        "strict": false,                           
        "moduleResolution": "node",              
        "baseUrl": "./",                          
        "allowSyntheticDefaultImports": true,  
        "experimentalDecorators": true,           
        "emitDecoratorMetadata": true,            
        "paths": {
          "@config/*":   [  "config/*"       ],
          "@repos/*":    [  "src/repos/*"    ],
          "@models/*":   [  "src/models/*"   ],
          "@shared/*":   [  "src/shared/*"   ],
          "@server":     [  "src/server"     ],
          "@services/*": [  "src/services/*" ],
          "@routes/*":   [  "src/routes/*"   ],
          "@graphql/*":  [  "src/graphql/*"  ]
        },
        "useUnknownInCatchVariables": false
      },
      "include": [
        "src/**/*.ts",
        "spec/**/*.ts"
      ],
      "exclude": [
        "src/public/",
        "node_modules"
      ],
      "ts-node": {
        "esm": true,
        "require": ["tsconfig-paths/register", "esm"]
      }
    }

I need module in ES2022 since I use top level awaits, as you can see I am registering the esm and tsconfig-paths in the ts-node configuration, but I am out of ideas of what should I do to get this finally running, before the problem was "type": "module bacause I was unable to got it running with "esm": true on ts-node with ES2022


Solution

  • Check the code that your transpiler is emitting. It won't be ESM, it'll be written in the older node-module specification known as Common-JS (aka CJS when typed as an acronym).

    This is because of the value you have assigned to the ./tsconfig.json file's field: "moduleResolution", which is the value "node". That setting doesn't mean that you want to support the Node.js RTE, but rather that you want to resolve modules as if they were classic Node.js modules.

    To quote the official documentation on this subject, which is easy enough to locate:

    NOTE: this documentation is a little behind. The docs should state that Node16 (current LTS version) is also available, for those who stick to the LTS versions _(which includes most post-launch apps running in production mode).

    {
        "module": "ES2022", // this can be set to ES2020, ES2021, etc... Its all the same after ES2020
        "moduleResolution": "Node"
    }
    

    ...does two things, it defines the module-type that you will adhere too when authoring your project, and the specification that will be used to resolve modules.

    Both of the settings, module & moduleResolution affect the transpiled output emitted by our TSC compiler (or transpiler, which is a specific type of compiler). Lately, it seems that people have been associating moduleResolution with the syntax implemented by a module specification, however, its not an accurate understanding of what "Module Resolution" is. It is easy, however, to see why some might perceive "Module Resolution" (I'm talking about the concept specifically, not the configuration) as pertaining to a syntax. Module resolution is the system (or process) used by a module, with-in a modular-system (obviously, if it wasn't in a modular system it wouldn't be a module), to locate and implement other modules. In not so many words, its the logic used to connect many modules together to form a larger system. Syntax is customized for each different modular system, as the goal of the syntax is to best reflect the mechanics that the syntax implements.

    Anyways, I am getting off topic. The point is, there are several different configurations you can use to compile to ESM, however, the configuration you are using is not one of them (I am referring to your tsconfig.json file of course).

    It does appear that you have another problem. You need to tell, not just TypeScript, but Node, how you plan to resolve modules. Since you want to use the ESM specification, you need to configure your package.json file as such.

    To let node know that you want to use the ESM Specification for resolving modules, add the following to your package.json file.

    {
        "type": "module"
    }
    

    ...you could, alternatively, use the .mts/.mjs file extensions, which, when found by node, lets node know that the file your using is to be resolved as an ES-Module.

    There are two links that you should read, both can enlighten you further.

    The first is the TSConfig link on "Module Resolution" (both the configuration & the concept).

    The second is how to properly configure Node.js to resolve modules using the ECMAScript-Module Specification, which is found HERE