Skip to content

Conversation

@joshkel
Copy link

@joshkel joshkel commented Jul 24, 2025

Note: I've done some testing of this and believe it's correct, but I'm continuing to read up on CJS versus ESM and how various tools handle them, so corrections are welcome.

Note: I've done some testing of this and believe it's correct, but I'm continuing to read up on CJS versus ESM and how various tools handle them, so corrections are welcome.
joshkel added a commit to joshkel/chartjs-plugin-annotation that referenced this pull request Jul 27, 2025
This reworks .d.ts handling to address the issues in chartjs#977 and reported by [Are the Types Wrong](https://arethetypeswrong.github.io/?p=chartjs-plugin-annotation%403.1.0).

Background information:

* According to the [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/modules/guides/choosing-compiler-options.html#im-writing-a-library):

    > If you’re using a bundler to emit your library... You must ensure that your declaration files get bundled as well. Recall the [first rule of declaration files](https://www.typescriptlang.org/docs/handbook/modules/theory.html#the-role-of-declaration-files): every declaration file represents exactly one JavaScript file.

* According to [Are the Types Wrong](https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseESM.md),

    > A golden rule of declaration files is that if they represent a module—that is, if they use import or export at the top level—they must represent exactly one JavaScript file. They especially cannot represent JavaScript files of two different module formats.

* chartjs-plugin-annotation's CJS build uses a CJS default export (see [Rollup's docs](https://rollupjs.org/configuration-options/#output-exports)). Properly representing that in a `.d.ts` file requires using `export = Annotation`, but the `export =` syntax both raises challenges for exporting types alongside the default export and is incompatible with ESM. (The [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/modules/appendices/esm-cjs-interop.html) has even more background information here. See also [tshy's documentation](https://github.com/isaacs/tshy?tab=readme-ov-file#handling-default-exports), "`export default`` is the bane of hybrid TypeScript modules" - although I believe that the situation is [not quite as bad as it says](isaacs/tshy#105).)

Pulling this together for chartjs-plugin-annotation:

* Use [rollup-plugin-dts](https://www.npmjs.com/package/rollup-plugin-dts) to bundle declaration files alongside the bundled library files (as recommended by the TypeScript Handbook).
* Invoke rollup-plugin-dts twice to create separate `.d.ts` files for the ESM and CJS builds (and update package.json to reference those).
* Although a CJS file with a default export can't export additional values, it is apparently possible to export types alongside the default value, if the types are placed in a namespace with the same name as the default value.
* I had trouble finding a solution for the `export =` default issue. (I couldn't get rollup-plugin-dts to create an `export =` style definition. Changing chartjs-plugin-annotation to not use a CJS default export may be the simplest long-term solution, but it would break backward compatibility.) To solve this, I added @rollup/plugin-replace to apply the necessary namespace and `export =` statement via string replacement.

Fixes chartjs#977
joshkel added a commit to joshkel/chartjs-plugin-annotation that referenced this pull request Jul 28, 2025
This reworks .d.ts handling to address the issues in chartjs#977 and reported by [Are the Types Wrong](https://arethetypeswrong.github.io/?p=chartjs-plugin-annotation%403.1.0).

Background information:

* According to the [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/modules/guides/choosing-compiler-options.html#im-writing-a-library):

    > If you’re using a bundler to emit your library... You must ensure that your declaration files get bundled as well. Recall the [first rule of declaration files](https://www.typescriptlang.org/docs/handbook/modules/theory.html#the-role-of-declaration-files): every declaration file represents exactly one JavaScript file.

* According to [Are the Types Wrong](https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseESM.md),

    > A golden rule of declaration files is that if they represent a module—that is, if they use import or export at the top level—they must represent exactly one JavaScript file. They especially cannot represent JavaScript files of two different module formats.

* chartjs-plugin-annotation's CJS build uses a CJS default export (see [Rollup's docs](https://rollupjs.org/configuration-options/#output-exports)). Properly representing that in a `.d.ts` file requires using `export = Annotation`, but the `export =` syntax both raises challenges for exporting types alongside the default export and is incompatible with ESM. (The [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/modules/appendices/esm-cjs-interop.html) has even more background information here. See also [tshy's documentation](https://github.com/isaacs/tshy?tab=readme-ov-file#handling-default-exports), "`export default`` is the bane of hybrid TypeScript modules" - although I believe that the situation is [not quite as bad as it says](isaacs/tshy#105).)

Pulling this together for chartjs-plugin-annotation:

* Use [rollup-plugin-dts](https://www.npmjs.com/package/rollup-plugin-dts) to bundle declaration files alongside the bundled library files (as recommended by the TypeScript Handbook).
* Invoke rollup-plugin-dts twice to create separate `.d.ts` files for the ESM and CJS builds (and update package.json to reference those).
* Although a CJS file with a default export can't export additional values, it is apparently possible to export types alongside the default value, if the types are placed in a namespace with the same name as the default value.
* I had trouble finding a solution for the `export =` default issue. (I couldn't get rollup-plugin-dts to create an `export =` style definition. Changing chartjs-plugin-annotation to not use a CJS default export may be the simplest long-term solution, but it would break backward compatibility.) To solve this, I added @rollup/plugin-replace to apply the necessary namespace and `export =` statement via string replacement.

Fixes chartjs#977
@isaacs
Copy link
Owner

isaacs commented Oct 1, 2025

Huh, seems that exporting a namespace that matches the module-internal name of the value now Just Works, but it still fails if the value gets re-exported, as the types will not ride along with it.

So, in many cases, you still do have to use a global namespace, or there's just no way to get the types to be exported in the same way for both CJS and ESM.

For example, if I refactor node-tap's default export in that way, I get this in CJS mode:

import tap from 'tap'
tap
export type T = tap.TestBase
Screenshot 2025-10-01 at 08 55 13

But, if I leave it as-is, it works fine.

Still best to just avoid, if at all possible.

@isaacs isaacs closed this Oct 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants