ym-project / gulp-esbuild

gulp plugin for esbuild bundler
MIT License
42 stars 7 forks source link

Type Declarations not Working for ESModule #17

Closed manuth closed 1 year ago

manuth commented 1 year ago

When trying to import this package from an ESModule TypeScript ecosystem, it won't work. TypeScript shows an error stating that the corresponding types could not be found.

This is caused by the package.json setting the file for import calls to be index.mjs. The setting mentioned before causes TypeScript to go search for the type declarations at gulp-esbuild/index.d.mts.

There are basically two ways on how to fix this: A separate index.d.mts file can be created or - if both type declarations match - the types can be set to resolve to index.d.ts:

"exports": {
    "types": "./index.d.ts",
    "default": "./index.mjs"
}

Sadly, the type declarations do not match (index.js uses an export assignment, index.mjs uses a default export), so I think a separate type declaration file should be the proper solution.

I'll go create a PR concerning this matter.

ym-project commented 1 year ago

Hello @manuth! Thank you for the issue! Can I see an example to reproduce the problem? I just don't quite understand what's going on.

manuth commented 1 year ago

Sure - will do asap

manuth commented 1 year ago

About This Error

Using ESModules in TypeScript

TypeScript allows you to emit modules both written as CommonJS or as ESM. The corresponding setting is called module: https://www.typescriptlang.org/tsconfig#module

Files written in CommonJS can import ESModules using dynamic import()s:

(await import("gulp-esbuild"))({});

Files written in ESM can import other ESModules using import statements:

import gulpEsbuild from "gulp-esbuild";

gulpEsbuild({});

The module Setting

In tsconfig.json, When setting module to CommonJS, TypeScript will use the types field (if present) or the main field in your package.json file in order to determine the corresponding types, so everything is working out just fine.

However, when setting module to Node16, NodeNext or ES2022, the module resolution of TypeScript changes. Just like NodeJS, TypeScript will first check the file extension.

.cts/.cjs files will be considered CommonJS, .mts/.mjs will be considered as ESModule. For .ts/.js files, it will check the project's package.json and decide, depending on whether the package.jsons type is set to commonjs or module, whether the corresponding file is cjs or mjs.

If nothing else is specified (for example using the types-projerty in package.json or the exports["."].import.types in package.json), for .js files, TypeScript will look for ".d.ts" files, for .cjs files, TypeScript will look for .d.cts

Furthermore - and this is the point where the use of gulp-esbuild breaks - TypeScript checks the exports field and, based on whether the file performing the import is cjs or mjs, check the import or require field in the package.jsons exports.

In the exports field, people can specify custom paths to the type declarations if they'd like to do so:

{
    "exports": {
        ".": {
            "import": {
                "types": "./index.d.ts", // TypeScript will resolve types to "[gulp-esbuild]/index.d.ts"
                "default": "./index.mjs" // node will resolve "gulp-esbuild" to "[gulp-esbuild]/index.mjs"
        }
    }
}

For further reading, you might want to have a look at this: https://www.typescriptlang.org/docs/handbook/esm-node.html

This means, that, when importing (instead of requireing) your module from an ESM file, TypeScript would resolve to [gulp-esbuild]/index.mjs. Based on this, TypeScript will try to find its corresponding type declarations at [gup-esbuild]/index.d.mts resulting in an error.

Reproducing The Error

  1. Create a new npm project (in fact, an empty folder will just do)
  2. Install typescript and gulp-esbuild
  3. Create a tsconfig.json file which has its module set to Node16, NodeNext or ES2022 (and its moduleResolution undefined):
    {
       "compilerOptions": {
           "module": "ES2022",
           "lib": [
               "ES2020"
           ],
           "target": "ES6"
       },
       "include": [
           "./src/**/*"
       ]
    }
  4. Create a file which is an ESModule file (either by setting type to module or by creating an .mjs file
  5. Import the gulp-esbuild module using an import-statement:
    import gulpEsbuild from "gulp-esbuild";
  6. Take note, that TypeScript reports an error that the types could not be found.
ym-project commented 1 year ago

I tried your reproduction sequencing but I didn't face any types problems. What did I do wrong? Example here https://github.com/ym-project/gulp-esbuild-issue17

manuth commented 1 year ago

It's nothing big, really TypeScript won't do actual type checking on .js files on its own without tweaking the settings a little.

You might want to add something like this to your compilerOptions:


{
    "compilerOptions": {
        "allowJs": true, // Allow JavaScript files to be part of your project
        "checkJs": true, // Enable type checking in JavaScript files

        /* With `outDir` unspecified, compiling a JavaScript file would cause it to overwrite itself.
         * Adding this option will tell TypeScript that this project is not supposed to be compiled and
         * thus not causing JavaScript files to overwrite themselves. */
        "noEmit": true
    }
}
manuth commented 1 year ago

Oh sorry... I just noticed that I said to create an .mjs file where you'd actually have to create an .mts file. But anyways - tweaking tsconfig.json will work as well.

Sorry for the inconvenience.

ym-project commented 1 year ago

As I understand typescript documentation correctly, module resolution Classic will work correctly only if the package has typing via @types/package library.

You can run command tsc --noEmit --traceResolution and see:

For gulp-esbuild

======== Resolving module 'gulp-esbuild' from '/home/ymdev/Dev/esbuild-test/src/file.mjs'. ========
Explicitly specified module resolution kind: 'Classic'.
File '/home/ymdev/Dev/esbuild-test/src/gulp-esbuild.ts' does not exist.
File '/home/ymdev/Dev/esbuild-test/src/gulp-esbuild.tsx' does not exist.
File '/home/ymdev/Dev/esbuild-test/src/gulp-esbuild.d.ts' does not exist.
File '/home/ymdev/Dev/esbuild-test/gulp-esbuild.ts' does not exist.
File '/home/ymdev/Dev/esbuild-test/gulp-esbuild.tsx' does not exist.
File '/home/ymdev/Dev/esbuild-test/gulp-esbuild.d.ts' does not exist.
File '/home/ymdev/Dev/gulp-esbuild.ts' does not exist.
File '/home/ymdev/Dev/gulp-esbuild.tsx' does not exist.
File '/home/ymdev/Dev/gulp-esbuild.d.ts' does not exist.
File '/home/ymdev/gulp-esbuild.ts' does not exist.
File '/home/ymdev/gulp-esbuild.tsx' does not exist.
File '/home/ymdev/gulp-esbuild.d.ts' does not exist.
File '/home/gulp-esbuild.ts' does not exist.
File '/home/gulp-esbuild.tsx' does not exist.
File '/home/gulp-esbuild.d.ts' does not exist.
File '/gulp-esbuild.ts' does not exist.
File '/gulp-esbuild.tsx' does not exist.
File '/gulp-esbuild.d.ts' does not exist.
Directory '/home/ymdev/Dev/esbuild-test/src/node_modules' does not exist, skipping all lookups in it.
File '/home/ymdev/Dev/esbuild-test/node_modules/@types/gulp-esbuild.d.ts' does not exist.
Directory '/home/ymdev/Dev/node_modules' does not exist, skipping all lookups in it.
Directory '/home/ymdev/node_modules' does not exist, skipping all lookups in it.
Directory '/home/node_modules' does not exist, skipping all lookups in it.
Directory '/node_modules' does not exist, skipping all lookups in it.
File '/home/ymdev/Dev/esbuild-test/src/gulp-esbuild.js' does not exist.
File '/home/ymdev/Dev/esbuild-test/src/gulp-esbuild.jsx' does not exist.
File '/home/ymdev/Dev/esbuild-test/gulp-esbuild.js' does not exist.
File '/home/ymdev/Dev/esbuild-test/gulp-esbuild.jsx' does not exist.
File '/home/ymdev/Dev/gulp-esbuild.js' does not exist.
File '/home/ymdev/Dev/gulp-esbuild.jsx' does not exist.
File '/home/ymdev/gulp-esbuild.js' does not exist.
File '/home/ymdev/gulp-esbuild.jsx' does not exist.
File '/home/gulp-esbuild.js' does not exist.
File '/home/gulp-esbuild.jsx' does not exist.
File '/gulp-esbuild.js' does not exist.
File '/gulp-esbuild.jsx' does not exist.

For react

======== Resolving module 'react' from '/home/ymdev/Dev/esbuild-test/src/file.mjs'. ========
Explicitly specified module resolution kind: 'Classic'.
File '/home/ymdev/Dev/esbuild-test/src/react.ts' does not exist.
File '/home/ymdev/Dev/esbuild-test/src/react.tsx' does not exist.
File '/home/ymdev/Dev/esbuild-test/src/react.d.ts' does not exist.
File '/home/ymdev/Dev/esbuild-test/react.ts' does not exist.
File '/home/ymdev/Dev/esbuild-test/react.tsx' does not exist.
File '/home/ymdev/Dev/esbuild-test/react.d.ts' does not exist.
File '/home/ymdev/Dev/react.ts' does not exist.
File '/home/ymdev/Dev/react.tsx' does not exist.
File '/home/ymdev/Dev/react.d.ts' does not exist.
File '/home/ymdev/react.ts' does not exist.
File '/home/ymdev/react.tsx' does not exist.
File '/home/ymdev/react.d.ts' does not exist.
File '/home/react.ts' does not exist.
File '/home/react.tsx' does not exist.
File '/home/react.d.ts' does not exist.
File '/react.ts' does not exist.
File '/react.tsx' does not exist.
File '/react.d.ts' does not exist.
Directory '/home/ymdev/Dev/esbuild-test/src/node_modules' does not exist, skipping all lookups in it.
Found 'package.json' at '/home/ymdev/Dev/esbuild-test/node_modules/@types/react/package.json'.
'package.json' does not have a 'typesVersions' field.
File '/home/ymdev/Dev/esbuild-test/node_modules/@types/react.d.ts' does not exist.
'package.json' does not have a 'typings' field.
'package.json' has 'types' field 'index.d.ts' that references '/home/ymdev/Dev/esbuild-test/node_modules/@types/react/index.d.ts'.
File '/home/ymdev/Dev/esbuild-test/node_modules/@types/react/index.d.ts' exist - use it as a name resolution result.
======== Module name 'react' was successfully resolved to '/home/ymdev/Dev/esbuild-test/node_modules/@types/react/index.d.ts' with Package ID '@types/react/index.d.ts@18.0.26'. ========

As you can see, typescript goes node_modules folder only once and only for @types/* package. So I think we can not fix this problem without @types/gulp-esbuild package.

ym-project commented 1 year ago

If we set "moduleResolution": "nodenext", we can fix this without new files. Just change package.json file

  ...
  "exports": {
    "import": {
      "types": "./index.d.ts",
      "default": "./index.mjs"
    },
    "require": {
      "types": "./index.d.ts",
      "default": "./index.js"
    }
  },
  // for old versions
  "types": "index.d.ts",
  "main": "index.js",
  ...
manuth commented 1 year ago

Yes but the index.d.ts types mismatch index.mjs a tiny bit.

You might notice the little differences in my PR

Namely:

gulpEsbuild is not exported using an assignment (module.exports = gulpEsbuild) but rather default-exported (export default gulpEsbuild) as seen here:

https://github.com/ym-project/gulp-esbuild/blob/d169482c3ed50b70183d1d74dd2fa1e5abaed295/index.mjs#L330

gulpEsbuild does not have a createGulpEsbuild member. Rather, createGulpEsbuild is exported separately as seen here: https://github.com/ym-project/gulp-esbuild/blob/d169482c3ed50b70183d1d74dd2fa1e5abaed295/index.mjs#L331

index.d.mts (taken from my PR) address this: https://github.com/ym-project/gulp-esbuild/blob/2fa6abe09547eb1010a507cd1e50549cdb698462/index.d.mts#L2-L4

While index.d.ts does not: https://github.com/ym-project/gulp-esbuild/blob/d169482c3ed50b70183d1d74dd2fa1e5abaed295/index.d.ts#L19-L23

ym-project commented 1 year ago

Ok. I have understood. I will check your PR in the evening and merge it.

manuth commented 1 year ago

Awesome! Thank you so much for taking time 😄