Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type Declarations not Working for ESModule #17

Closed
manuth opened this issue Dec 1, 2022 · 11 comments · Fixed by #18
Closed

Type Declarations not Working for ESModule #17

manuth opened this issue Dec 1, 2022 · 11 comments · Fixed by #18
Assignees
Labels
bug Something isn't working

Comments

@manuth
Copy link
Contributor

manuth commented Dec 1, 2022

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
Copy link
Owner

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.

@ym-project ym-project self-assigned this Dec 2, 2022
@manuth
Copy link
Contributor Author

manuth commented Dec 2, 2022

Sure - will do asap

@manuth
Copy link
Contributor Author

manuth commented Dec 2, 2022

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
Copy link
Owner

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
Copy link
Contributor Author

manuth commented Dec 5, 2022

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
Copy link
Contributor Author

manuth commented Dec 5, 2022

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
Copy link
Owner

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
Copy link
Owner

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
Copy link
Contributor Author

manuth commented Dec 6, 2022

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:

export default createGulpEsbuild()

gulpEsbuild does not have a createGulpEsbuild member. Rather, createGulpEsbuild is exported separately as seen here:

export {createGulpEsbuild}

index.d.mts (taken from my PR) address this:

export { createGulpEsbuild, CreateGulpEsbuild, CreateOptions, GulpEsbuild, Options } from "./index.js";
export default gulpEsbuild;

While index.d.ts does not:

gulp-esbuild/index.d.ts

Lines 19 to 23 in d169482

declare const gulpEsbuild: GulpEsbuild & {
createGulpEsbuild: CreateGulpEsbuild
}
export = gulpEsbuild

@ym-project
Copy link
Owner

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

@ym-project ym-project added the bug Something isn't working label Dec 6, 2022
@manuth
Copy link
Contributor Author

manuth commented Dec 6, 2022

Awesome! Thank you so much for taking time 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants