Typescript and Subpath Imports - node.js

I am trying to get Node subpath imports and typescript to work. My IDE has no problem resolving the imports, but Typescript is never happy.
Github repo with code: https://github.com/doronrosenberg/ts-subpath-imports.
package.json:
"imports": {
"#internal/*": "./internal/*.ts",
"#internal2": "./internal"
}
tsconfig.json:
"paths": {
"#internal/*": "./internal/*.ts",
"#internal2": ["./internal"]
}
and the code:
import { foo } from "#internal/index";
import { bar } from "#internal2";
No matter how I set things up, I always get:
src/test.ts:1:21 - error TS2307: Cannot find module '#internal/index' or its corresponding type declarations.
1 import { foo } from "#internal/index";
~~~~~~~~~~~~~~~~~
src/test.ts:2:21 - error TS2307: Cannot find module '#internal2' or its corresponding type declarations.
2 import { bar } from "#internal2";
~~~~~~~~~~~~
Any ideas?

After hacking around with Typescript for a few weeks, I got a working solution.
Let's say I have a package called #kodadot1/metasquid with multiple submodules (consolidator and entity).
In my package.json I declare imports
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./consolidator": {
"types": "./dist/consolidator.d.ts",
"import": "./dist/consolidator.mjs",
"require": "./dist/consolidator.cjs"
},
"./entity": {
"types": "./dist/entity.d.ts",
"import": "./dist/entity.mjs",
"require": "./dist/entity.cjs"
}
}
The trick is to create a .d.ts file for each submodule in the project's root.
So for submodule entity I will make a file called entity.d.ts that contains
export * from './dist/entity'
Now to publish it correctly in npmjs extend your package.json like:
"files": [
"dist",
"*.d.ts"
]
Now just publish, and you can enjoy subpath imports:
import { get } from '#kodadot1/metasquid/entity'
Whole code is available here

The support of subpath exports requires newer module resolutions such as Node16 and NodeNext:
{
"compilerOptions": {
"moduleResolution": "Node16" // or `"NodeNext"`
}
}

Related

Nx - how to import from another module when building with the tsc executor?

I have a problem that is similar to the one described in Building library with imports from another library using NX Monorepo.
Using nx monorepo with a node app and a library. The app is built with #nrwl/js:tsc (not webpack as it is by default) and then executed using #nrwl/node:node. This is what the project.json looks like:
"build": {
"executor": "#nrwl/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"main": "apps/dep/src/main.ts",
"outputPath": "dist/apps/dep",
"tsConfig": "apps/dep/tsconfig.app.json"
}
},
"serve": {
"executor": "#nrwl/node:node",
"options": {
"buildTarget": "dep:build"
}
},
Importing anything from another library causes a problem with the build due to files not being under rootDir:
import { MyEnum } from '#zorro/types';
This I resolved using the advice from the question above, adding the following settings to tsconfig.app.json:
"compilerOptions": {
...
"incremental": false,
"paths": { "#zorro/*": ["dist/libs/*"] }
},
This made tsc work, but when running with node, I get an error:
Error: Cannot find module '#zorro/types'
Can't figure out what needs to be changed in order to properly resolve the path of the library for the compiled main.js file.

Make "import/extensions" require the .js extension in a Node.js TypeScript project

First of all, some facts:
Node.js requires that all local imports include the imported module's extension (e.g. import hello from './hello.js', not import hello from './hello').
TypeScript will compile imports with or without the .js extension, which means a missing .js extension is a runtime error.
TypeScript doesn't transform imports to add the .js extension or convert .ts to .js.
In my Node.js project, I want to make missing a missing .js extension be a build-time error using the import/extensions ESLint rule. However, when I enable this rule using the following configuration:
{
"root": true,
"env": {
"node": true
},
"parser": "#typescript-eslint/parser",
"plugins": [
"#typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:import/recommended",
"plugin:import/typescript",
"plugin:#typescript-eslint/eslint-recommended",
"plugin:#typescript-eslint/recommended"
],
"settings": {
"import/resolver": {
"typescript": {},
"node": {
"extensions": [".js"]
}
}
},
"rules": {
"import/extensions": ["error", "ignorePackages"]
}
}
running eslint gives me the following error:
/sandbox/src/index.ts
1:19 error Missing file extension "ts" for "./hello.js" import/extensions
Source files:
// index.ts
import hello from "./hello.js";
hello();
// hello.ts
export default function hello() {
console.log("Hello");
}
CodeSandbox link: https://codesandbox.io/s/elated-germain-13glp7
I fixed this with the following config:
{
"root": true,
"env": {
"node": true
},
"extends": [
"eslint:recommended",
"plugin:import/recommended",
"plugin:import/typescript",
"plugin:#typescript-eslint/eslint-recommended",
"plugin:#typescript-eslint/recommended"
],
"rules": {
"import/extensions": ["error", "ignorePackages"],
"import/no-unresolved": "off"
}
}
The main thing is to disable the "import/no-unresolved" rule and remove "settings"."import/resolver"."node". ("import/no-unresolved" is redundant as unresolved imports are resolved at the compilation stage.) Other items removed here were already being added as a result of extending the #typescript-eslint plugins.
I found an eslint plugin that can fix missing .js extensions for imports in .ts files, instead of just showing an error:
https://github.com/AlexSergey/eslint-plugin-file-extension-in-import-ts
https://www.npmjs.com/package/eslint-plugin-file-extension-in-import-ts
Install:
npm i -D eslint-plugin-file-extension-in-import-ts
Add to .eslintrc file:
{
"plugins": [
"file-extension-in-import-ts"
],
"rules": {
"file-extension-in-import-ts/file-extension-in-import-ts": "error"
}
}
NOTE: I ran into an issue similar to https://github.com/import-js/eslint-plugin-import/issues/1292 when using this package, and it will incorrectly try to add .js extensions on these paths when fixing automatically.
You could try ts-add-js-extension package to append .js extension to the transpiled JavaScript files. After you install you can do
ts-add-js-extension add --dir={your-transpiled-outdir}

Use of alias paths inside a referenced project

Since TypeScript 3, it is possible to use projects as references within a given project.
I was trying to use such feature on two personal projects I have but it was not working. So I decided to use a given example as a starting point: https://github.com/appzuka/project-references-example
When I launch npm run build, all projects compile successfully. I then decided to modify the tsconfig.json file of package animals to include the support of aliases. The file was modified as such:
{
"extends": "../../tsconfig-base.json",
"compilerOptions": {
"outDir": "lib",
"rootDir": ".",
"baseUrl": ".",
"paths": { "#animals/*": [ "*" ] }
},
"references": [
{ "path": "../core" }
]
}
(baseUrl and paths properties were added inside compilerOptions)
I then modified the first import of dog.tsx file this way:
import { Animal, Size } from #animals/animal;
When I try to build again the main project, it fails. The error is:
ERROR in C:\Users\User\Documents\project-references-example\packages\animals\dog.tsx
[tsl] ERROR in C:\Users\User\Documents\project-references-example\packages\animals\dog.tsx(1,30)
TS2307: Cannot find module '#animals/animal' or its corresponding type declarations.
To make it working, I need to add the alias also on the main project, like this (in tsconfig.json):
"paths": {
"#animals/*": [ "packages/animals/*" ]
}
Is there a possibility to use alias in a referenced project without having to specify such alias in the parent project?
Thank you!

How can I get ESLint to recognize aggregate export namespaces?

The Situation
I have a NodeJS project that uses Babel and ESLint (6.8).
I'm using the relatively new syntax for aggregate exports (export * as name1 from …;).
The Code
constants.js
export const x = 5
export const y = 6
index.js
export * as constants from './constants'
sandbox.js
import { constants } from './index'
console.log(constants.x)
When I run babel-node sandbox.js everything works just fine, and the value for x (5) is rendered.
.eslintrc
{
"extends": "airbnb-base",
"parser": "babel-eslint",
"env": {
"es6": true,
"node": true,
"jest": true
}
}
.babelrc
{
"presets": [
[
"#babel/preset-env",
{
"targets": {
"node": "13.10"
}
}
]
],
"plugins": [
"#babel/plugin-proposal-export-namespace-from"
]
}
The Problem
ESLint seems to be confused by my aggregate export, rendering the following error when I lint:
sandbox.js
1:10 error constants not found in './index' import/named
The Question
How do I get ESLint to recognize that the named aggregate does in fact exist? I would like to be able to still benefit from the import/named checks overall.

How to import esm modules with NodeJS 13, and Typescript 3.8?

I have a problem with some imports in NodeJS. I want to use the new features of Typescript 3.8, like private fields : #myPrivateField
I don't know how to correctly import the module "typescript" in my class. I tried many options, but impossible to solve my problem.
My files :
package.json
{
"name": "test",
"scripts": {
"start": "tsc && node --experimental-modules --es-module-specifier-resolution=node main.js"
},
"dependencies": {
"#types/node": "^13.13.2",
"app-root-path": "^3.0.0",
"fs-extra": "^9.0.0",
"tsutils": "^3.17.1"
},
"devDependencies": {
"ts-node": "~8.3.0",
"typescript": "^3.8.3"
},
"type": "module"
}
tsconfig.json
{
"compilerOptions": {
"lib": [
"ESNext",
"es2016",
"dom",
"es5"
],
"module": "esnext",
"moduleResolution": "Node",
"sourceMap": true,
"target": "es6",
"typeRoots": [
"node_modules/#types"
]
}
}
main.ts
// import ts = require("typescript");
import * as ts from "typescript";
export class Main {
node: ts.Node;
#test = 'zzz';
constructor() {}
process(): void {
ts.forEachChild(this.node, function cb() {
});
console.log('#test', this.#test);
}
}
const main = new Main();
main.process();
With this code, when I run npm run start, I have the error TypeError: ts.forEachChild is not a function
Without the line with ts.forEachClid() it logs correctly the value of the private field #test.
If I try to replace import * as ts from "typescript"; by import ts = require("typescript");, I have the error TS1202: Import assignment cannot be used when targeting ECMAScript modules. Consider using 'import * as ns from "mod"', 'import {a} from "mod"', 'import d from "mod"', or another module format instead
Of course, I tried many changes in tsconfig.json and in package.json (with `"type" = "module"), but impossible to solve this problem.
I even tried to replace "module": "esnext" by "module": "commonjs", but I have an error exports is not defined.
Remark :
This is not specific to the module "typescript". I have the same problem with other modules like "fs-extra", which are making exports in a different way than most of classic NodeJS modules.
For example, the module "typescript" exports with export = ts.
I found this reference too, but it didn't help me a lot :
https://www.typescriptlang.org/docs/handbook/modules.html
My nodeJs version is 13.3.0 and my typescript version is 3.8.3
Thanks for your help
Finally, the good response was : you need commonjs and es2018 to be able to use the typescript #privateFields in a node module.
Here is the correct tsconfig.json to use :
{
"compileOnSave": false,
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"target": "es2018",
"typeRoots": [
"node_modules/#types"
],
"lib": [
"es2018",
"dom"
]
}
}
ESM Modules are still in experimental mode, there are a lot of modules that do not support it, e.g. uses module.exports (CommonJS) instead of export (ESM).
Your best choice here is to use commonjs module and run node without any flags. Also, type of the package in package.json must be commonjs as well.
Answering your question "How to import ESM module"... In case, module does support ESM modules, you can just use import as you usually do:
import { something } from './something';
UPD: as OP author mentioned, for private fields to work, there is must be set a target es2018. The reasoning behind this is that private fields is not a part from ES2015 specification and you need to upgrade to the minimal supported target to do that.
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"target": "es2018"
}

Resources