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

Generate different declaration types #267

Open
2 tasks done
VividLemon opened this issue Sep 5, 2023 · 11 comments
Open
2 tasks done

Generate different declaration types #267

VividLemon opened this issue Sep 5, 2023 · 11 comments
Labels
enhancement New feature or request

Comments

@VividLemon
Copy link

Description

One of the differences introduced by TypeScript v5 is that .d.ts files are affected by esm and cjs contexts. So, sharing a single .d.ts file between the two can lead to invalid formats when a library is dual publishing for these contexts.

The solution is to generate .d.mts & .d.cjs files.

https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseCJS.md
https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseESM.md

Under what circumstance would Vite create these wrong, I am not sure. However, for integrity, it would be best for these to be split up.

Suggested solution

Create an option to generate different, or multiple versions of the output files. It's not necessarily a requirement for the library to add specific support to generate .d.mts, .d.cts variants, but the ability to do it yourself would work.

Perhaps this is already possible through one of the included hooks. However, this I am not sure of.

Alternative

I don't really think there is an alternative.

Additional context

Alternatively/additionally, a section on the FAQ could also help.

This could also be a vue-tsc related issue, as I don't think they generate those files.

Validations

  • Read the FAQ.
  • Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
@qmhc
Copy link
Owner

qmhc commented Sep 6, 2023

I have read the information you provided, but I can't find the difference in content between .d.cts and .d.mts files. Are they just different file extensions?

@VividLemon
Copy link
Author

VividLemon commented Sep 6, 2023

I have equally had the same questions but have found little information on what the true differences actually are syntactically. Sadly, the js ecosystem is really weird and this question is very obscure.

The only differences I can think of are the differences with how things are exported, cjs exports map to export =, mjs exports map to export {} or export default

The original issue was found when looking at https://publint.dev/rules#export_types_invalid_format

So, it is quite possible that these things are the same. But there isn't a whole lot of info. In my opinion, they are likely the same. But perhaps this is a deeper question to TypeScript as there isn't a whole lot of information on this topic.

Indeed this https://arethetypeswrong.github.io/?p=bootstrap-vue-next%400.12.0 displays that some of the issue described contain these resolution issues

@qmhc
Copy link
Owner

qmhc commented Sep 6, 2023

I found this in TypeScript docs: https://www.typescriptlang.org/docs/handbook/esm-node.html#packagejson-exports-imports-and-self-referencing.

It says: It’s important to note that the CommonJS entrypoint and the ES module entrypoint each needs its own declaration file, even if the contents are the same between them.

And I have never seen a declaration file uses export =.

So, currently we can consider they are only different in their file extensions. For this, we can add some options for custom the declaration file extensions (while considering multiple outputs with different file extensions).

@VividLemon
Copy link
Author

And I have never seen a declaration file uses export =.

This was a mistake. I meant to account for the differences between export default/export {} & module.exports.

So, currently we can consider they are only different in their file extensions. For this, we can add some options for custom the declaration file extensions (while considering multiple outputs with different file extensions).

This will likely work. If this is implemented, I would like to check and see what arethetypeswrong.github.io says. If it comes out with everything being clean, then imo there should likely be a section on this projects README as it should become the standard

@BibiSebi
Copy link

Hello, facing the exact same issue. Any updates so far?

@will-stone
Copy link

The way we've handled this in our company's internal component library, is to use the afterBuild hook to copy and rename the files:

import { copyFileSync } from "node:fs"

import { defineConfig } from "vite"
import dts from "vite-plugin-dts"

// https://vitejs.dev/config/
export default defineConfig({
  build: {...},
  plugins: [
    ...,
    dts({
      afterBuild: () => {
        // To pass publint (`npm x publint@latest`) and ensure the
        // package is supported by all consumers, we must export types that are
        // read as ESM. To do this, there must be duplicate types with the
        // correct extension supplied in the package.json exports field.
        copyFileSync("dist/index.d.ts", "dist/index.d.mts")
      },
      exclude: [ ... ],
      include: ["src"],
    }),
  ],
})

@mkilpatrick
Copy link

One small change to make publint happy. Change to copyFileSync("dist/index.d.ts", "dist/index.d.cts"); and have your package.json exports look something like:

"exports": {
    ".": {
      "import": {
        "types": "./dist/index.d.ts",
        "default": "./dist/main.js"
      },
      "require": {
        "types": "./dist/index.d.cts",
        "default": "./dist/main.cjs"
      }
    }
  },

Specifically, the CJS types need to use the .cts extension rather than having a .mts extension for ESM, at least when you're using "type": "module".

@will-stone
Copy link

@mkilpatrick yes! I had to do that version when converting to proper esm-first.

@Juice10
Copy link

Juice10 commented Apr 16, 2024

And I have never seen a declaration file uses export =.

If I only have a default export attw complains about node16 (from CJS) │ ❗️ Incorrect default export. If I modify the .d.cts file's export default MyDefaultExportedFn to export = MyDefaultExportedFn then the error goes away.

Or as an odd workaround, if I add an extra export, next to the default export, then the error also goes away in attw

@waldronmatt
Copy link

I handle my types slightly different than approaches above. I have separate esm and cjs folders via dist/types/esm/ and dist/types/cjs/. This is my approach to generate esm and cjs typings with the correct file extensions for both .d.ts and .d.ts.map files:

tsconfig.json

{
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true
  }
}

vite.config.ts

import { defineConfig } from 'vite';
import fs from 'fs-extra';
import dts from 'vite-plugin-dts';
import { glob } from 'glob';

export default defineConfig({
  plugins: [
    dts({
      outDir: ['dist/types/esm', 'dist/types/cjs'],
      afterBuild: async () => {
        const files = glob.sync('dist/types/cjs/**/*.d.{ts,ts.map}', { nodir: true });
        await Promise.all(
          files.map(async (file) => {
            const newFilePath = file.replace(/\.d\.ts(\.map)?$/, '.c.ts$1');
            await fs.move(file, newFilePath, { overwrite: true });
          }),
        );
      },
    }),
  ];
})

package.json

{
 "exports": {
    ".": {
      "import": {
        "types": "./dist/types/esm/index.d.ts",
        "default": "./dist/esm/index.js"
      },
      "require": {
        "types": "./dist/types/cjs/index.c.ts",
        "default": "./dist/cjs/index.cjs"
      }
    },
    "./*": {
      "import": {
        "types": "./dist/types/esm/*",
        "default": "./dist/esm/*"
      },
      "require": {
        "types": "./dist/types/cjs/*",
        "default": "./dist/cjs/*"
      }
    }
  }
}

@waldronmatt
Copy link

Update:

After several failed attempts, I found a way to use this plugin to generate valid esm and cjs .d.ts files that passes arethetypeswrong 's audit (and publint). The following conditions must be met (which was already mentioned above):

  • you should have different .d.ts extensions for esm and cjs: .d.ts and .d.cts in separate folders is my personal preference (distinguish cjs files to promote esm adoptability)
  • these paths and extensions should be referenced separately in package.json

Using the examples above, I had to do a few extra things with this plugin to ensure the audit passes:

  • manually copy esm .d.ts files and rename to .d.cts for cjs dts files
  • manually update the sourceMappingURL references in the newly created .d.cts files to reference the .d.cts.map files
  • manually update .d.cts file extension references to .cjs
  • manually update the .d.cts.map source map file reference to point to the .d.cts files

Here is what my new config looks like:

arethetypeswrong audit results: https://arethetypeswrong.github.io/?p=%40waldronmatt%2Fdemo-ui%403.5.7

package.json

    "exports": {
      ".": {
        "import": {
          "types": "./dist/types/esm/index.d.ts",
          "default": "./dist/esm/index.js"
        },
        "require": {
          "types": "./dist/types/cjs/index.d.cts",
          "default": "./dist/cjs/index.cjs"
        }
      },
      "./*": {
        "import": {
          "types": "./dist/types/esm/*",
          "default": "./dist/esm/*"
        },
        "require": {
          "types": "./dist/types/cjs/*",
          "default": "./dist/cjs/*"
        }
      },
      "./styles/*": {
        "import": "./dist/styles/*",
        "require": "./dist/styles/*"
      }
    }

vite.config.ts

dts({
      // create two type folders, one for esm and cjs
      outDir: ['dist/types/esm', 'dist/types/cjs'],
      // modify type files after they have been written
      afterBuild: async () => {
        // Fetch all .d.ts files recursively from the dist/types/cjs directory
        const files = glob.sync('dist/types/cjs/**/*.d.{ts,ts.map}', { nodir: true });
        // Since TypeScript 5.0, it has emphasized that type files (*.d.ts) are also affected by its ESM and CJS context.
        // This means that you can't share a single type file for both ESM and CJS exports of your library.
        // You need to have two type files when dual-publishing your library.
        // see https://www.typescriptlang.org/docs/handbook/modules/reference.html#node16-nodenext and
        // https://publint.dev/rules#export_types_invalid_format
        await Promise.all(
          // Ideally, this plugin will support different types in the future
          // See https://github.com/qmhc/vite-plugin-dts/issues/267
          files.map(async (file) => {
            // Generate the new files with the new .c.ts/.c.ts.map naming
            const newFilePath = file.replace(/\.d\.ts(\.map)?$/, '.d.cts$1');
            await fs.move(file, newFilePath, { overwrite: true });

            // Update sourceMappingURL references
            if (newFilePath.endsWith('.d.cts')) {
              const content = await fs.readFile(newFilePath, 'utf-8');
              let updatedContent = content.replace(/\/\/# sourceMappingURL=.*\.d\.ts\.map/g, (match) =>
                match.replace('.d.ts.map', '.d.cts.map'),
              );
              // Update .js references to .cjs
              updatedContent = updatedContent.replace(/(from\s+['"].*?)\.js(['"])/g, '$1.cjs$2');
              await fs.writeFile(newFilePath, updatedContent, 'utf-8');
            }

            // Update source map file references
            if (newFilePath.endsWith('.d.cts.map')) {
              const content = await fs.readJson(newFilePath);
              content.file = content.file.replace('.d.ts', '.d.cts');
              await fs.writeJson(newFilePath, content);
            }
          }),
        );
      },
    }),

I haven't tested this fully yet with cjs and my subpath wildcards, but I think manually updating the .d.ts files to point to the new extensions correctly should help resolve the issues.

Ideally this plugin should be updated to reflect Typescript 5's new guidelines. All that would be needed is the option to specify different extensions and have this plugin reflect those extension names during the generation process so we don't have to do it manually here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants