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

How to document custom elements without too many hacks #2844

Open
hesxenon opened this issue Feb 2, 2025 · 11 comments
Open

How to document custom elements without too many hacks #2844

hesxenon opened this issue Feb 2, 2025 · 11 comments
Labels
question Question about functionality

Comments

@hesxenon
Copy link

hesxenon commented Feb 2, 2025

Search terms

global, export

Question

So I tried to find a way to document what is essentially a big "mess" around globals.

As a CE author I want to be able to document the usage of my CE without changing the implementation.

Ideally what I would like to do is the following:

// not exported on purpose, should only be available on global object as importing this would
// throw an "ce already defined" error
namespace Module {
  export class MyCE extends HTMLElement {}
  customElements.define("my-ce", MyCE); // side effect!

  export namespace MyCE {
    // types and utils specific to my custom element
  }
}

declare global {
  namespace globalThis {
    // Apparently the only way to re-export a symbol without changing its type explicitly?
    // other approaches include declaring the `typeof class` as a `var`, `let` or `const` or some
    // variation thereof
    /**
     * documentation about the class itself
     */
    export import MyCE = Module.MyCE;
  }

  interface HTMLElementTagNameMap {
    /**
     * documentation about the element
     */
    "my-ce": Module.MyCE
  }
}
Object.assign(globalThis, Module);

Do you see this happening in the future? Or should I rather change the doc tool here?

@hesxenon hesxenon added the question Question about functionality label Feb 2, 2025
@phoneticallySAARTHaK
Copy link

@hesxenon
Copy link
Author

hesxenon commented Feb 2, 2025

the way I read this this would only help if I wanted to document something that isn't exported. The problem/behaviour I'm seeing however is, that as soon as anything is exported the global definitions are completely omitted.

Now the answer could ofc simply be to not export anything, but sometimes I do want to export stuff from a module, e.g. types to enable consumer plugins.

Also, this plugin would not help with the documentation on the merged HTMLElementTagNameMap, would it? As a matter of fact I never managed to include that interface at all...

@phoneticallySAARTHaK
Copy link

The problem/behaviour I'm seeing however is, that as soon as anything is exported the global definitions are completely omitted.

I'm not sure what you mean by that. If the type is being referenced it should be included.

Also, this plugin would not help with the documentation on the merged HTMLElementTagNameMap, would it? As a matter of fact I never managed to include that interface at all...

AFAIK, nope.

I'd use @module for extra docs

@phoneticallySAARTHaK
Copy link

phoneticallySAARTHaK commented Feb 2, 2025

Also, why are you adding documentation inside the declare tag, and not at the place it is defined and exported?

    /**
     * documentation about the class itself
     */
    export class MyCE extends HTMLElement {}

@Gerrit0
Copy link
Collaborator

Gerrit0 commented Feb 2, 2025

TypeDoc expects to document exports, support for globals is tenuous at best, and every globals-based setup seems to want something slightly different and likely incompatible with the last globals user...

as soon as anything is exported the global definitions are completely omitted.

Yes, if an entry point uses exports, TypeDoc expects those exports to fully describe the module.

importing this would throw an "ce already defined" error

That doesn't sound right. The only way that the element gets defined once is that the file must have been imported at some point... and ignoring hot reload nastiness, modules should only be defined once. (hot reload I'd expect to also break in the same way, so that can't be the issue...)

I'm guessing you're doing the side effects in each module to make it possible to import part of your module... I'd be very tempted to use normal imports/exports everywhere and do the global augmentation in one place so that normal exports could be used to define what is documented.

Also, this plugin would not help with the documentation on the merged HTMLElementTagNameMap, would it? As a matter of fact I never managed to include that interface at all...

That didn't seem right to me, I checked on it and realized that TypeDoc was treating members as external if any declarations were external, with c372df3 that's changed, so it should show up now. With no exported members from the example file, the current dev branch produces this now (I duplicated Module into a Module2 to have more than one):

@hesxenon
Copy link
Author

hesxenon commented Feb 2, 2025

Also, why are you adding documentation inside the declare tag, and not at the place it is defined and exported?

because it shouldn't be exported :) You don't have to import HTMLDivElement to use it and I want my CEs to be available in the same manner.

likely incompatible with the last globals user...

Well I feel you and I would also have liked if CEs weren't class based. Alas, this is the way to extend the definition of the available element tags :/

if an entry point uses exports, TypeDoc expects those exports to fully describe the module.

fair enough, but it's also not working as I would hope (not even expect) if the export keyword is within the global declarations

The only way that the element gets defined once [...and then again] is that the file must have been imported at some point [...and then again]

This can easily happen if a module can be consumed in multiple ways - e.g. when including a bundled version and then accidentally importing the exported class directly.

make it possible to import part of your module

no, the module should be an ambient import, nothing else. Any and all values and types should be accessible from the global object.

do the global augmentation in one place

hmmm, that sounds like it might save me some pain, I'll have a look at whether that's possible.

The thing is though, that when using libraries to define custom elements people might not even be able to separate the augmentation from the definition...

with c372df3 that's changed

nice, so that just leaves the awkward way to declare global classes? I'll try it out in a sec, is this published already or should I install a git dependency?

@hesxenon
Copy link
Author

hesxenon commented Feb 2, 2025

this is looking a lot better with the mentioned commit, thank you. However, there are still some "issues", with the following module (and some existing modules around it, sorry for that)

import { customElement } from "simple-custom-elements";

namespace Module {
  /**
   * a test CE
   */
  @customElement({ tagname: "my-ce" })
  export class MyCe extends HTMLElement {}
}

declare global {
  namespace globalThis {
    export import MyCe = Module.MyCe;
  }

  namespace MyCe {
    /**
     * an example aug
     */
    export type Foo = number;
  }

  interface HTMLElementTagNameMap {
    /**
     * the my-ce element
     */
    "my-ce": MyCe;
  }
}

Image

  1. I didn't define window?
  2. the Foo module now "steals" the augmentations from the datepicker module

In any way, thank you for your quick answer

@Gerrit0
Copy link
Collaborator

Gerrit0 commented Feb 2, 2025

if the export keyword is within the global declarations

That isn't a module export, which is what TypeDoc looks for, TypeDoc asks the compiler what a file exports, it doesn't walk the entire syntax tree manually (it used to, ~4 years ago, and that was a nightmare, every single TS release broke things, and it never worked quite right)

This can easily happen if a module can be consumed in multiple ways - e.g. when including a bundled version and then accidentally importing the exported class directly.

In my opinion, that's a bug in the consuming code! They should either use a bundled version and not import your code at all, or import your code and let their bundler include it. Including a module twice and expecting it to play together with another opens up all sorts of problems...

I didn't define window?

This doesn't happen in my setup... so it must be getting augmented somewhere, if you have disableSources set, try turning that off and looking at the page for "Defined in" lines, of them declarations will point to your code.

the Foo module now "steals" the augmentations from the datepicker module

This is a design limitation. There is only one global symbol, so if you're going to document globals, you should only have one entry point. This is another problem with globals -- there is no reasonable way to determine which entry point they should belong to in the case of there being multiple entry points.

You could technically probably work around this by creating a separate tsconfig for each module, running TypeDoc on each module separately, and then merging the results together, but that's going to be horribly slow and likely still not produce good results.

@hesxenon
Copy link
Author

hesxenon commented Feb 3, 2025

So I'm not sure what the differences are yet but I created a quick demo repo and with the current dev branch this works nearly as intended. Just two nits:

  1. it would still be awesome if I didn't have to create a dedicated type just to merge it with the global type a few lines later.
    1. is it possible in some way to give a dedicated name a default comment or to somehow include it explicitly without a comment?
  2. not sure if this is related to the dev branch, but somehow the docs watcher keeps refreshing now and (also maybe unrelated) my .gitignore is being recreated (with the same content) all the time while the watcher is active??

@hesxenon
Copy link
Author

hesxenon commented Feb 3, 2025

found another issue: https://gitlab.com/hesxenon/ce-docs-how-to/-/blob/main/lib/my-ce.ts#L20

produces:

Image

the first link only links to the namespace, the second link works. I think that the link resolution stops after a qualifier...

(btw, are the docs there out of date? tsdoc seems to know about qualifiers, yet typedocs docs state something else?)

@Gerrit0
Copy link
Collaborator

Gerrit0 commented Feb 4, 2025

  1. Given that you want a separate type in each module, this feels like a pretty low cost way to do it
    i. Plugins can add comments, there's no inferred-comments support built in
  2. Bah. That's annoying, this works perfectly in TypeDoc's repo... not sure what's different. Switching to npm still works, so it likely isn't pnpm weirdness, though pnpm's patching of TypeScript always makes me nervous.
  3. TypeDoc implements the "new" form of declaration references, you are trying to use the old form, which is not supported. I decided to support the new form rather than the old form when adding support for it as it more closely aligns with how people generally write links and expect them to work.

TypeDoc's declaration references are slightly different than JSDoc's namepaths. They are based off of the "new" TSDoc declaration references with slight modifications to make their resolution behavior more closely match the TypeScript language service (e.g. what VSCode does).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Question about functionality
Projects
None yet
Development

No branches or pull requests

3 participants