Tagman
Tagman

Reputation: 186

Node cannot find required modules with non-relative path after transpiling them with tsc

We are building an application with plain typescript. To make the imports easier to read, we configured typescript to be able to resolve non-relative paths via the tsconfig.

Transpiling the code into JS code with tsc works fine. But whenever we want to start the app with node dist/demo/demonstration.js we get an cannot find lib/input/parser module error. This is the first import of the generated .js file. Its a general problem with all the imports.

At first we thought it might be a problem with our tsconfig.json. But any tries to modify the paths and baseurl keys were met with the same or different errors upon running the js file. The case that tsc itself is completing without errors also invalidates this.

Running jest through the jest.config.js in Webstorm is also working.

We found two possible workarounds. One is copying the folders inside dist into nodes_modules. Doing this via symbolic links removes some of the manual work.

The other is running the app through the terminal like this NODE_PATH=dist dist/demo/demonstration.js.

Both of these feel very hacky. Am I missing a concept here or is this how you do non-relativ imports in typescript? How can I configure Webstorm Run Configurations to be able to run our app?

Example of an import in typescript:

import {Parser} from "lib/input/parser";

tsc is able to resolve these.

tsconfig.json, cleared of unused options. Left the ones we played around with:

{
    "compilerOptions": {
        /* Basic Options */
        "target": "es2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
        "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */

        "outDir": "dist", /* Redirect output structure to the directory. */
        //     "rootDir": ".",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */


        /* Strict Type-Checking Options */
        "strict": true, /* Enable all strict type-checking options. */

        /* Module Resolution Options */
        "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
        "baseUrl": "src", /* Base directory to resolve non-absolute module names. */
        //     "paths": {
        //       "lib/*": ["lib/*"],
        //       "demo/*": ["demo/*"],
        //       "test/*": ["test/*"]
        //     },                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */

        "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */

        /* Source Map Options */

        "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
        "inlineSources": true /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */

    }
}

jest.config.js:

module.exports = {
  "roots": [
    "<rootDir>/src/lib", //src is our folder where sources reside
    "<rootDir>/src/test",
    "<rootDir>/src/demo"
  ],
  "transform": {
    "^.+\\.tsx?$": "ts-jest" //tells jest to use ts-jest for ts / tsx files
  },
  //"testRegex": "(/test/.*|(\\.|/)(test|spec))\\.tsx?$", //tells Jest to look for test in any __tests__ folder
  // AND also any files anywhere that use the (.test|.spec).(ts|tsx) extension e.g. asdf.test.tsx etc.
  "testRegex": "/test/.*",
  "moduleFileExtensions": [
    "ts",
    "tsx",
    "js",
    "jsx",
    "json",
    "node"
  ],
  moduleDirectories: ['node_modules', 'src']
};

package.json

{
  "name": "questionsys",
  "version": "0.1.0",
  "description": "Context-based questionnaire engine",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Tagman",
  "license": "ISC",
  "devDependencies": {
    "@types/axios": "^0.14.0",
    "@types/jest": "^24.0.17",
    "@types/node": "^12.7.2",
    "@types/uuid": "^3.4.5",
    "jest": "^24.8.0",
    "ts-jest": "^24.0.2",
    "typedoc": "^0.15.0",
    "typescript": "^3.5.3"
  },
  "dependencies": {
    "axios": "^0.19.0",
    "uuid": "^3.3.2"
  }
}

Upvotes: 2

Views: 3829

Answers (1)

ford04
ford04

Reputation: 74770

TypeScript uses paths and baseUrl only for compile checks and expects you to convert path aliases physically in a separate build step:

Our general take on this is that you should write the import path that works at runtime, and set your TS flags to satisfy the compiler's module resolution step, rather than writing the import that works out-of-the-box for TS and then trying to have some other step "fix" the paths to what works at runtime. (core developers' standpoint)

  1. Preserve runtime behavior of all JavaScript code. (TypeScript design goals)

Downstream runtime environments like Node.js won't have notion of your mapped absolute paths and emit an error, if paths are not converted yet. There is a proposal called Import Maps (see the corresponding Node.js issue), which might ease up mapped path resolution in future. Until then, here are some examples for common build tools:

babel-plugin-module-resolver

Example: Convert @/foo/bar/index./src/foo/bar/index

.babelrc:
{
  // ...  
  "plugins": [
    [ "module-resolver", { "alias": { "^@/(.+)": "./src/\\1" }} ]
  ]
}

In a separate build step after tsc compilation:

babel src --out-dir dist # or similar

tsconfig-paths / tsconfig-paths-webpack-plugin

webpack.config.js:
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
module.exports = {
  // ...
  resolve: {
    plugins: [new TsconfigPathsPlugin({/* options */})]
  }
}

More infos

Upvotes: 4

Related Questions