Giter VIP home page Giter VIP logo

web-component-analyzer's People

Contributors

43081j avatar aarondrabeck avatar bicknellr avatar danielkoek avatar dependabot-preview[bot] avatar dependabot[bot] avatar jrobinson01 avatar justinfagnani avatar mwcz avatar priandsf avatar reecelangerock avatar renovate-bot avatar rictic avatar runem avatar telpalbrox avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

web-component-analyzer's Issues

cssProperties type/typeHint

hey,

it would be really nice to have types/typeHints for css properties (you will need to hand write them). These types can then be used to either link to documentation or use them to display color pickers 💪

Types I would see so far:

  • Color (e.g. #fff, orange, rgb(255,0,0), ...)
  • Length (e.g. 10px, 10rem, ...)
  • String e.g. default

Stahman proposal:

/**
 * This is a container looking like a card with a back and front side you can switch
 *
 * @cssprop --demo-wc-card-header-font-size <Length> - Header font size
 * @cssprop --demo-wc-card-front-color <Color> - Font color for front
 */
export class DemoWcCard extends LitElement {}

output in customElements.json:

// tags.cssProperties[
{
  "name": "--demo-wc-card-font-front",
  "description": "Font color for front",
  "type": "Color"
}

Add methods to JSON output

I tried to do this myself, but quickly got out of my depth. I'd like to see support for JSDoc methods in the JSON format. It's already there in the markdown format.

image

Installation docs have a typo

Your docs say to do npm install -g web-component-analyer. The 'z' is missing. It should read npm install -g web-component-analyzer 😄

Feature request: analyzeProject()

For programmatic use, one of the simplest APIs I can think of would be to analyze a TypeScript project either by folder, or by reference to tsconfig.json file. The entrypoint, rather than the caller, would create the TypeScript program and checker.

[LitElement] Default array values not extracted

Components that are written with array input do not out-put the programmed default values in the properties table.

Example code

class MyComp extends LitElement {
  /**
   * should render default values
   */
  @property({
    type: Array
  })
  myArray = ["value"]
}

Expected output

Property Attribute Type Default Description
myArray myArray string[] ["value"] should render default values

support @attr @readonly

This issue follows up on #125 (comment)

Input

/**
 * Whether or not the element has an error
 * @attr error
 * @readonly
 */
get error() { return this.#error }
set error(_) {}

static get properties() { return { error: { reflect: true } } }

onError(error) {
  const oldError = this.error;
  this.#error = error
  this.requestUpdate('error', oldError)
}

[request] - support @deprecated

We want to use the @deprecated JSDoc tag to announce via the README and intellisense that a certain property should not be used, without releasing breaking changes.

It would be wonderful if this input

/**
 * Size of the internal icon in px
 * @attr icon-size
 * @type {Number}
 * @deprecated Since 3.0.0. Use `--icon-size` CSS property instead. Will be removed at next major version.
 */
@property({ type: Number, attribute: 'icon-size' }) iconSize = 20;

Which currently produces this README table:

## Properties

| Property| Attribute | Type| Default | Description |
|---------|-----------|-----|---------|-------------|
| `iconSize` | `icon-size` | `number` | 20| The internal icon's size. |

Would produce instead this expected table

## Properties

| Property| Attribute | Type| Default | Description |
|---------|-----------|-----|---------|-------------|
| `iconSize` | `icon-size` | `number` | 20| The internal icon's size.<br/>\
*DEPRECATED* Since 3.0.0. Use `--icon-size` CSS property instead. Will be removed at next major version.                          |

Feature request: document CSS shadow parts

CSS shadow parts are available in Chrome 73+, Firefox 69 + (behind a flag) and WebKit nightly.
They are important part of the public CSS API, e.g. we are using part in Vaadin components.

Let me suggest a syntax to document CSS Shadow Parts like this:

@csspart thumb - A part representing a switch thumb
@csspart track - A part representing a switch track
@csspart track-fill - An inner part (child of "track")  

Parsing should be most likely the same as for Custom CSS properties.

If you think this feature would be a nice addition, I could try to submit a PR.

[discussion] property analysis behavior is strange

Given a plain JS lit-element like so:

/**
* @prop {boolean} myProperty - description for myProperty
*/
class MyElement extends LitElement {
    static get properties() {
        return {
           myProperty: { type: Boolean }
        }
    }
    constructor() {
        super();
        this.myProperty = false;
        this.myBoundFunction = this.someFunction.bind(this); 
    }
    
    someFunction() {
    }
}

The (JSON) output contains both the myProperty and myBoundFunction properties. It seems that wca is analyzing not only the jsdoc but also the component's constructor. This seems to be causing two issues. The first is that the descriptions are not present in the output, and the second is that I wouldn't expect myBoundFunction to be present in the output. I see that there is work to support @private/@protected, which may solve the latter issue.

[request] support @protected properties

This input

import { LitElement, html, query } from 'lit-element';

/** @element */
class LeakyDocs extends LitElement {

  /**
   * Value
   * @attr
   */
  @property({ type: String }) value;
  
  /**
   * Internal input
   * @protected
   */
  @query('#input') input

  render() {
    return html`<input id="input" .value="${this.value}"/>`;
  }
}

produces this current output:

## Properties

| Property | Attribute | Type    | Default | Description    |
|----------|-----------|---------|---------|----------------|
| `input`  |           | `any`   |         | Internal input |
| `value`  | `value`   | `string`|         | value          |

WCA should produce instead an expected output without the protected property

## Properties

| Property | Attribute | Type    | Default | Description    |
|----------|-----------|---------|---------|----------------|
| `value`  | `value`   | `string`|         | value          |

Support Array<Type> vs Type[] in JSON output

This might not be a bug, but rather unexpected behavior. So, I have this property:

  get items(): Array<VaadinAccordionPanel> {
    return super.items as Array<VaadinAccordionPanel>;
  }

Which produces the following JSON:

{
  "name": "items",
  "type": "VaadinAccordionPanel[]"
},

IMO the expected behavior here would be to preserve Array<Type> notation.
Currently I have to add a following JSDoc in order to enforce this notation:

/**
 * @type {Array<VaadinAccordionPanel>}
 */

Private properties in static properties

Hey @runem,

I use _propertyName in my static get properties () {} but it is not filtered out.

Seems like parseStaticProperties does not use isPropNamePublic.

Can I propose a PR maybe?

Issue with extending an imported interface that shares the same name with a value

Consider:

// checkbox-mixin.d.ts
interface CheckboxMixin {
  checked: boolean;
}

declare const CheckboxMixin: object;

export {CheckboxMixin};
// main.ts
import {html} from 'lit-element';

import {CheckboxMixin} from './checkbox_mixin';

interface TheCheckbox extends CheckboxMixin, HTMLElement {}

declare global {
  interface HTMLElementTagNameMap {
    'the-checkbox': TheCheckbox
  }
}

console.log(html`
   <the-checkbox .checked=${true}></the-checkbox>
`);
// .checked not found on the-checkbox!

Analyze property assignments in the constructor

It should be possible for the analyzer to pick up on properties used in the constructor of the custom element.

Properties prefixed with "#" or "_" are private and shouldn't be shown as properties on the element.

Example:

constructor() {
  super();
  this.title = 'You are awesome';
  this.darkMode = false;
  this.bar = { x: 0, y: 0, title: 'I am dot' };
  this.formatter = null;
  this._timeout = setTimeout(console.log, 1000);
}

Not getting default values if output type is JSON

This is a really impressive utility.

I've noticed that if I use the default formatter, I get a column for default values for properties.

But the default value is not provided if outputting to JSON. Would be great if JSON could also support default values.

Also, if the default value is a JS object, the keys get truncated to the first letter, e.g.:

newDataPointTestValue2 = {
    label: "test",
    index: 10,
    valueFromEachDataset: []
};

produces {"l":"test","i":10,"v":[]}

Support original jsdoc event implementation

The official jsdoc implementation requires the event to be defined with class and event type. Using the method described in the readme gives me a warning in IntelliJ editors: Unresolved variable or type 'event:input-switch-check-changed'

So instead of :
/* @fires input-switch-check-changed Fires when check property changes */
I need to write the following doc to get rid of the warning:
/* @fires InputSwitch#[CustomEvent]input-switch-check-changed Fires when check property changes */

I can understand you still want to support the original example because it's less verbose, but is it possible to also support the original jsdoc event definition?

JSON output changes

Hi @runem! I just discovered wca and have been enjoying experimenting with it.

One thing I'd like to use wca for is to automatically generate JSON schemas for our components (the project is PatternFly Elements). My plan is to ask wca for JSON, then feed that JSON into a schema template, which will then output the final schema.json file.

Before going all-in on that though, I wanted to ask about this output from wca:

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
WARNING: The json transformer hasn't been finished yet. You can expect large changes in this output.
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

What sort of JSON changes do you have in mind for wca? For now, I pinned the version of wca so we won't be surprised by any JSON format changes, so we're all good there. I just wanted to get a sense of what you plan to change.

`@private` tag doesn't work as expected

Current behavior

If I initialize a prop like:

/**
   * This should be hidden from generated documentation
   * @private
   */
  @property({ attribute: false }) loading;

It will still be added to the table markdown.

Expected result:

Since it's a @private it should be hidden from documentation

Support using lib types with the @property JSDoc tag

This is an intersection with the VSCode plugin, but from my logs this is the underlying component that has the issue.

First of all, thank you very much for your plugin, it's awesome!

I just noticed that a nullable type in a @prop JSDoc tag seems to crash language server features. For example, after typing the below example @prop line, other custom elements in my template no longer get hover-info, autocomplete etc.

Example:

import {html, css, LitElement} from 'lit-element';

/**
 * @customElement error-dialog
 * @prop {?Error} error
 * @slot - Header Text
 * @event errorack When the error acknowledgement button is pressed
 *
 *   - some comments about css variables omitted -
 */
class ErrorDialog extends LitElement {
  //... code
}

The @prop {?Error} error bit seems to mess it up. Just for sureness' sake, I tried nullable notation on other types, eg. Number and the same result occurred.

The logs I get when I enable verbose logging in the VSCode plugin:

�[31m ERROR: �[0m[ �[32m'Error [getQuickInfoAtPosition]: (TypeError: Cannot read property \'toLowerCase\' of undefined\n    at parseJsDocTypeString (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:198:17)\n    at parseJsDocTypeString (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:226:20)\n    at /home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:817:43\n    at /home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:750:20\n    at Array.map (<anonymous>)\n    at parseJsDocForNode (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:748:14)\n    at parseDeclarationMembers$1 (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:811:26)\n    at JsDocFlavor.parseDeclarationMembers (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:907:16)\n    at _loop_1 (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:1814:22)\n    at executeFirstFlavor (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:1826:27)\n    at visitComponentDeclaration (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:1766:25)\n    at parseComponentDeclaration (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:1680:5)\n    at parseComponentDefinitions (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:1936:31)\n    at Object.analyzeComponents (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:2066:32)\n    at StoreUpdater.findComponents (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/ts-lit-plugin/lib/language-service/store-updater.js:104:54)\n    at StoreUpdater.findInvalidatedComponents (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/ts-lit-plugin/lib/language-service/store-updater.js:70:22)\n    at StoreUpdater.update (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/ts-lit-plugin/lib/language-service/store-updater.js:42:18)\n    at TsLitPlugin.getQuickInfoAtPosition (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/ts-lit-plugin/lib/language-service/ts-lit-plugin.js:106:27)\n    at /home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/ts-lit-plugin/lib/decorate-language-service.js:108:30\n    at Object.nextLanguageService.(anonymous function) [as getQuickInfoAtPosition] (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/ts-lit-plugin/lib/decorate-language-service.js:56:71)\n    at IOSession.Session.getQuickInfoWorker (/opt/visual-studio-code/resources/app/extensions/node_modules/typescript/lib/tsserver.js:131043:62)\n    at Session.handlers.ts.createMapFromTemplate._a.(anonymous function) (/opt/visual-studio-code/resources/app/extensions/node_modules/typescript/lib/tsserver.js:130049:61)\n    at /opt/visual-studio-code/resources/app/extensions/node_modules/typescript/lib/tsserver.js:131665:88\n    at IOSession.Session.executeWithRequestId (/opt/visual-studio-code/resources/app/extensions/node_modules/typescript/lib/tsserver.js:131656:28)\n    at IOSession.Session.executeCommand (/opt/visual-studio-code/resources/app/extensions/node_modules/typescript/lib/tsserver.js:131665:33)\n    at IOSession.Session.onMessage (/opt/visual-studio-code/resources/app/extensions/node_modules/typescript/lib/tsserver.js:131687:35)\n    at Interface.<anonymous> (/opt/visual-studio-code/resources/app/extensions/node_modules/typescript/lib/tsserver.js:132978:27)\n    at Interface.emit (events.js:182:13)\n    at Interface._onLine (readline.js:290:10)\n    at Interface._normalWrite (readline.js:433:12)\n    at Socket.ondata (readline.js:149:10)\n    at Socket.emit (events.js:182:13)\n    at addChunk (_stream_readable.js:283:12)\n    at readableAddChunk (_stream_readable.js:264:11)\n    at Socket.Readable.push (_stream_readable.js:219:10)\n    at Pipe.onStreamRead [as onread] (internal/stream_base_commons.js:94:17)) Cannot read property \'toLowerCase\' of undefined'�[39m,
  TypeError: Cannot read property 'toLowerCase' of undefined
      at parseJsDocTypeString (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:198:17)
      at parseJsDocTypeString (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:226:20)
      at /home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:817:43
      at /home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:750:20
      at Array.map (<anonymous>)
      at parseJsDocForNode (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:748:14)
      at parseDeclarationMembers$1 (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:811:26)
      at JsDocFlavor.parseDeclarationMembers (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:907:16)
      at _loop_1 (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:1814:22)
      at executeFirstFlavor (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:1826:27)
      at visitComponentDeclaration (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:1766:25)
      at parseComponentDeclaration (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:1680:5)
      at parseComponentDefinitions (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:1936:31)
      at Object.analyzeComponents (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/web-component-analyzer/lib/index.cjs.js:2066:32)
      at StoreUpdater.findComponents (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/ts-lit-plugin/lib/language-service/store-updater.js:104:54)
      at StoreUpdater.findInvalidatedComponents (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/ts-lit-plugin/lib/language-service/store-updater.js:70:22)
      at StoreUpdater.update (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/ts-lit-plugin/lib/language-service/store-updater.js:42:18)
      at TsLitPlugin.getQuickInfoAtPosition (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/ts-lit-plugin/lib/language-service/ts-lit-plugin.js:106:27)
      at /home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/ts-lit-plugin/lib/decorate-language-service.js:108:30
      at Object.nextLanguageService.(anonymous function) [as getQuickInfoAtPosition] (/home/nick/.vscode/extensions/runem.lit-plugin-1.0.2/node_modules/ts-lit-plugin/lib/decorate-language-service.js:56:71)
      at IOSession.Session.getQuickInfoWorker (/opt/visual-studio-code/resources/app/extensions/node_modules/typescript/lib/tsserver.js:131043:62)
      at Session.handlers.ts.createMapFromTemplate._a.(anonymous function) (/opt/visual-studio-code/resources/app/extensions/node_modules/typescript/lib/tsserver.js:130049:61)
      at /opt/visual-studio-code/resources/app/extensions/node_modules/typescript/lib/tsserver.js:131665:88
      at IOSession.Session.executeWithRequestId (/opt/visual-studio-code/resources/app/extensions/node_modules/typescript/lib/tsserver.js:131656:28)
      at IOSession.Session.executeCommand (/opt/visual-studio-code/resources/app/extensions/node_modules/typescript/lib/tsserver.js:131665:33)
      at IOSession.Session.onMessage (/opt/visual-studio-code/resources/app/extensions/node_modules/typescript/lib/tsserver.js:131687:35)
      at Interface.<anonymous> (/opt/visual-studio-code/resources/app/extensions/node_modules/typescript/lib/tsserver.js:132978:27)
      at Interface.emit (events.js:182:13)
      at Interface._onLine (readline.js:290:10)
      at Interface._normalWrite (readline.js:433:12)
      at Socket.ondata (readline.js:149:10)
      at Socket.emit (events.js:182:13)
      at addChunk (_stream_readable.js:283:12)
      at readableAddChunk (_stream_readable.js:264:11)
      at Socket.Readable.push (_stream_readable.js:219:10)
      at Pipe.onStreamRead [as onread] (internal/stream_base_commons.js:94:17) ]

Looking at that call stack, the second section looks like this:

    // Match:
    //  {?number}       (nullable)
    //  {!number}       (not nullable)
    //  {...number}     (array of)
    var prefixMatch = str.match(/^(\?|!|(\.\.\.))(.+)$/);
    if (prefixMatch != null) {
        var modifier = prefixMatch[1];
        var type = parseJsDocTypeString(prefixMatch[2]);
        switch (modifier) {

which led me to infer that it might be the nullable notation, given the regex used there.

If I change the @prop line to: @prop {Error | null} (ultimately, the same thing), everything works again.

This is obviously not a showstopper, as like I mentioned you can just use the union type notation instead and work around it in the meantime.

Just wanted to let you know - thanks for your time!

Add more info to description fields in the vscode output

We can include information currently not present in the vscode format by adding it to the description fields.

attributes and tags in the vscode output format have description fields that will show when the user hovers above them in the IDE.

Tags

Here we can include events, slots, attributes, properties, css vars like the following:

{
   "name": "my-tag",
   "description": "Here is the description\n\nEvents:\n * changed: Event description\n\nSlots:\n * right\n *left"
}

Attributes

Here we can include type, associated property, default value.

{
   "name": "text",
   "description": "Here is the description\n\nType: string\nProperty: text\nDefault: 'default value'"
}

I will also have to look into if the vscode format supports using markdown in the description fields :-)

Multiline @fires event description is not emitted in the output

/**
* @fires load - This is the first line.
* This is the second line.
*/

Does not work, the description output for both json/markdown is empty for the event.

"events": [
        {
          "name": "load"
        }
      ]

This works however:

/**
* @fires load - This is the first line. This is the second line.
*/

Support Polymer Components

I want to introduce support for analyzing Polymer components. Specifically I want to implement searching for properties/attributes when extending the PolymerElement.

Properties

A lot of this functionality is already implemented by the LitElement parsing flavor. However there are some differences between them, for example how attributes are mapped.

static get properties () {
    return {
      toggleIcon: {
        type: String
      },
      pressed: {
        type: Boolean,
        value: false,
        notify: true,
        reflectToAttribute: true
      }
    };
  }

Documentation from https://polymer-library.polymer-project.org/3.0/docs/first-element/step-3:

For this more complicated property, you supply a configuration object with several additional fields:

    value: specifies the property's default value.

    notify: tells Polymer to dispatch property change events when the property's value changes. This lets the change be observed by other nodes.

    reflectToAttribute: tells Polymer to update the corresponding attribute when the property changes. This lets you style the element using an attribute selector, like icon-toggle[pressed]. 

        Attribute names are converted to lowercase property names. For example, the attribute firstName maps to firstname.

        Attribute names with dashes are converted to camelCase property names by capitalizing the character following each dash, then removing the dashes. For example, the attribute first-name maps to firstName.

Improve output for union type aliases

Hello, we are using web-component-analyzer to generate the documentation for our design system built with lit-element. Right now we have a lot of type aliases to define that an attribute/property can have only a fixed set of values like this one:

type ButtonSize = "normal" | "small" | "large" | "none" | "extra-small";

The thing is that the output of web-component-analyzer for this case is that the attribute/property is type ButtonSize which is not really helpful for people who is reading the documentation online without reading the source.
I saw also in some repositories that maybe this is not the best behavior when the type has a lot of different options, maybe this can be enabled or disabled somehow? Any thoughts on that?

I think that maybe it would be really handy that, in the case we have an alias that is a union type composed only of strings literals, to print the value of those strings literals like: "normal" | "small" | "large" | "none" | "extra-small". This is exactly what is already happening when a union type is used but is not aliased, like in this case:

@property() size:  "normal" | "small" | "large" | "none" | "extra-small" = "normal";

What do you think about this change? Do you think it makes sense?

I know that how the value is printed is handled by ts-simple-type but for this case I think it doesn't make sense to do it there.
I was already taking a look about how to implement it and I would be willing to provide a pull request 😄

Programatic usage to integrate with storybook

Hey @runem,

This project awesome 😄

I'm using https://storybook.js.org/ to work on my components (and to showcase them). I would love to use your project to extract my JSDoc into some kind of object I could then format to HTML. This means I need direct access without the CLI part.

I tried to use your code without the CLI but I did not manage to go very far. Would you be interested to guide me so we could provide this kind of interaction?

Failed gathering comments in LitElement component

The comment is not added to the resulting markdown output.

☠ Failing usecase:
» file: src/first-compo.js

import { html, css, LitElement } from 'lit-element';

@customElement('first-compo')
export class FirstCompo extends LitElement {
  static get properties() {
    return {
      /**
       * This is my comment for the property `foo`
       */
      foo: {
        type: String,
      },
    };
  }

constructor()  {
  this.foo = "init foo";
}

✓ Working usecase 1:

  static get properties() {
    return {
      /**
       * This is my comment for the property `foo`
       */
      foo: {
        type: String,
      },
    };
  }

constructor()  {
  // initialization removed
}

Here the This is my comment for the property foo will be used

✓ Working usecase 2:

  static get properties() {
    return {
      /**
       * This is my comment for the property `foo`
       */
      foo: {
        type: String,
      },
    };
  }

constructor()  {
   /**
     * My comment for foo in the constructor
     */
   this.foo = "something"
}

Here, the My comment for foo in the constructor will be used

It seems that if

  • there is no initialization (this.foo = ...) in the constructor of a property, the comment of the properties is added correctly
  • there is the comment in the constructor, the comment from the constructor is added correctly and the comment of the properties is ignored.
  • there is a comment in the properties and an initialization without a comment, the comment is not added at all

I would suggest to use the comment from the properties instead of nothing.

[bug] No useful output from video-player.js

Including here so others can run but this is all I get as output:
command: wca analyze video-player.js --outFile custom-elements.js
output:

# 

`video-player`
`A simple responsive video player with ridiculously powerful backing`

Input file of video-player.js:

/**
 * Copyright 2018 The Pennsylvania State University
 * @license Apache-2.0, see License.md for full text.
 */
import { html } from "@polymer/polymer/polymer-element.js";
import { SimpleColors } from "@lrnwebcomponents/simple-colors/simple-colors.js";
import { A11yBehaviors } from "@lrnwebcomponents/a11y-behaviors/a11y-behaviors.js";
import "@polymer/polymer/lib/elements/dom-repeat.js";
import "@polymer/polymer/lib/elements/dom-if.js";
import { SchemaBehaviors } from "@lrnwebcomponents/schema-behaviors/schema-behaviors.js";
import { MediaBehaviorsVideo } from "@lrnwebcomponents/media-behaviors/media-behaviors.js";
import "@lrnwebcomponents/a11y-media-player/a11y-media-player.js";
/**
 * `video-player`
 * `A simple responsive video player with ridiculously powerful backing`
 *
 * @microcopy - language worth noting:
 * - `video source` - url / link to the video file
 * ```
<video-player 
  accent-color$="[[accentColor]]"                 // Optional accent color for controls, 
                                                  // using the following materialize colors: 
                                                  // red, pink, purple, deep-purple, indigo, blue, 
                                                  // light blue, cyan, teal, green, light green, lime, 
                                                  // yellow, amber, orange, deep-orange, and brown. 
                                                  // Default is null. 
  dark$="[[dark]]"                                // Is the color scheme dark? Default is light. 
  dark-transcript$="[[darkTranscript]]"           // Use dark theme on transcript? Default is false, even when player is dark.   
  disable-interactive$="[[disableInteractive]]"   // Disable interactive cues?
  height$="[[height]]"                            // The height of player
  hide-timestamps$="[[hideTimestamps]]"           // Hide cue timestamps?
  lang$="[[lang]]"                                // The language of the media
  media-title$="[[mediaTitle]]"                   // The title of the media
  source$="[[source]]"                            // The source URL of the media
  sticky-corner$="[[stickyCorner]]"               // When user scrolls a playing video off-screen, 
                                                      which corner will it stick to? Values are: 
                                                      top-right (default), top-left, bottom-left, bottom-right, 
                                                      and none (to turn sticky off)
  thumbnail-src$="[[thumbnailSrc]]"               // Optional thumbanil/cover image url
  width$="[[width]]">                              // The width of the media             
  <div slot="caption">Optional caption info.</div>
</video-player>```
 *
 * @customElement
 * @polymer
 * @demo demo/index.html
 */
class VideoPlayer extends MediaBehaviorsVideo(
  A11yBehaviors(SchemaBehaviors(SimpleColors))
) {
  // render function
  static get template() {
    return html`
      <style>
        :host {
          display: block;
          margin: 0 0 15px;
        }
        .video-caption {
          font-style: italic;
          margin: 0;
          padding: 8px;
          @apply --video-player-caption-theme;
        }
      </style>
      <style include="simple-colors-shared-styles"></style>
      <div style$="[[playerStyle]]">
        <template is="dom-if" if="[[isA11yMedia]]" restamp>
          <a11y-media-player
            accent-color$="[[accentColor]]"
            audio-only$="[[audioOnly]]"
            dark$="[[dark]]"
            dark-transcript$="[[darkTranscript]]"
            disable-interactive$="[[disableInteractive]]"
            hide-timestamps$="[[hideTimestamps]]"
            hide-transcript$="[[hideTiranscript]]"
            lang$="[[lang]]"
            media-type$="[[sourceType]]"
            preload$="[[preload]]"
            media-title$="[[mediaTitle]]"
            sources$="[[sourceData]]"
            stand-alone$="[[__standAlone]]"
            sticky-corner$="[[stickyCorner]]"
            thumbnail-src$="[[thumbnailSrc]]"
            tracks$="[[trackData]]"
            crossorigin$="[[crossorigin]]"
            youtube-id$="[[youtubeId]]"
          >
            <template
              id="sources"
              is="dom-repeat"
              items="[[sourceData]]"
              as="sd"
              restamp
            >
              <source src$="[[sd.src]]" type$="[[sd.type]]" />
            </template>
            <template
              id="tracks"
              is="dom-repeat"
              items="[[trackData]]"
              as="track"
              restamp
            >
              <track
                src$="[[track.src]]"
                kind$="[[track.kind]]"
                label$="[[track.label]]"
                srclang$="[[track.lang]]"
              />
            </template>
            <slot name="caption"></slot>
          </a11y-media-player>
        </template>
        <template is="dom-if" if="[[!isA11yMedia]]">
          <template is="dom-if" if="[[sandboxed]]">
            <div class="responsive-video-container" lang$="[[lang]]">
              <webview
                resource$="[[schemaResourceID]]-video"
                src$="[[sourceData.0.src]]"
                width$="[[width]]"
                height$="[[height]]"
                frameborder="0"
              ></webview>
            </div>
          </template>
          <template is="dom-if" if="[[!sandboxed]]">
            <template is="dom-if" if="[[iframed]]">
              <div class="responsive-video-container" lang$="[[lang]]">
                <iframe
                  resource$="[[schemaResourceID]]-video"
                  src$="[[sourceData.0.src]]"
                  width$="[[width]]"
                  height$="[[height]]"
                  frameborder="0"
                  webkitallowfullscreen=""
                  mozallowfullscreen=""
                  allowfullscreen=""
                ></iframe>
              </div>
            </template>
          </template>
          <div id="videocaption" class$="video-caption">
            <p>
              [[mediaTitle]]
              <span class="media-type print-only">(embedded media)</span>
            </p>
            <slot name="caption"></slot>
          </div>
        </template>
      </div>
    `;
  }

  // haxProperty definition
  static get haxProperties() {
    return {
      canScale: true,
      canPosition: true,
      canEditSource: false,
      gizmo: {
        title: "Video player",
        description:
          "This can present video in a highly accessible manner regardless of source.",
        icon: "av:play-circle-filled",
        color: "red",
        groups: ["Video", "Media"],
        handles: [
          {
            type: "video",
            source: "source",
            title: "caption",
            caption: "caption",
            description: "caption",
            color: "primaryColor"
          }
        ],
        meta: {
          author: "LRNWebComponents"
        }
      },
      settings: {
        quick: [
          {
            property: "accentColor",
            title: "Accent color",
            description: "Select the accent color for the player.",
            inputMethod: "colorpicker",
            icon: "editor:format-color-fill"
          },
          {
            attribute: "dark",
            title: "Dark theme",
            description: "Enable dark theme for the player.",
            inputMethod: "boolean",
            icon: "invert-colors"
          }
        ],
        configure: [
          {
            property: "source",
            title: "Source",
            description: "The URL for this video.",
            inputMethod: "haxupload",
            icon: "link",
            required: true,
            validationType: "url"
          },
          {
            property: "track",
            title: "Closed captions",
            description: "The URL for the captions file.",
            inputMethod: "textfield",
            icon: "link",
            required: true,
            validationType: "url"
          },
          {
            property: "thumbnailSrc",
            title: "Thumbnail image",
            description: "Optional. The URL for a thumbnail/poster image.",
            inputMethod: "textfield",
            icon: "link",
            required: true,
            validationType: "url"
          },
          {
            property: "mediaTitle",
            title: "Title",
            description: "Simple title for under video",
            inputMethod: "textfield",
            icon: "av:video-label",
            required: false,
            validationType: "text"
          },
          {
            property: "accentColor",
            title: "Accent color",
            description: "Select the accent color for the player.",
            inputMethod: "colorpicker",
            icon: "editor:format-color-fill"
          },
          {
            attribute: "dark",
            title: "Dark theme",
            description: "Enable dark theme for the player.",
            inputMethod: "boolean",
            icon: "invert-colors"
          }
        ],
        advanced: [
          {
            property: "darkTranscript",
            title: "Dark theme for transcript",
            description: "Enable dark theme for the transcript.",
            inputMethod: "boolean"
          },
          {
            property: "hideTimestamps",
            title: "Hide timestamps",
            description: "Hide the time stamps on the transcript.",
            inputMethod: "boolean"
          },
          {
            property: "preload",
            title: "Preload source(s).",
            description:
              "How the sources should be preloaded, i.e. auto, metadata (default), or none.",
            inputMethod: "select",
            options: {
              preload: "Preload all media",
              metadata: "Preload media metadata only",
              none: "Don't preload anything"
            }
          },
          {
            property: "stickyCorner",
            title: "Sticky Corner",
            description:
              "Set the corner where a video plays when scrolled out of range, or choose none to disable sticky video.",
            inputMethod: "select",
            options: {
              none: "none",
              "top-left": "top-left",
              "top-right": "top-right",
              "bottom-left": "bottom-left",
              "bottom-right": "bottom-right"
            }
          },
          {
            property: "sources",
            title: "Other sources",
            description: "List of other sources",
            inputMethod: "array",
            properties: [
              {
                property: "src",
                title: "Source",
                description: "The URL for this video.",
                inputMethod: "textfield"
              },
              {
                property: "type",
                title: "Type",
                description: "Media type data",
                inputMethod: "select",
                options: {
                  "audio/aac": "acc audio",
                  "audio/flac": "flac audio",
                  "audio/mp3": "mp3 audio",
                  "video/mp4": "mp4 video",
                  "video/mov": "mov video",
                  "audio/ogg": "ogg audio",
                  "video/ogg": "ogg video",
                  "audio/wav": "wav audio",
                  "audio/webm": "webm audio",
                  "video/webm": "webm video"
                }
              }
            ]
          },
          {
            property: "tracks",
            title: "Track list",
            description: "Tracks of different languages of closed captions",
            inputMethod: "array",
            properties: [
              {
                property: "kind",
                title: "Kind",
                description: "Kind of track",
                inputMethod: "select",
                options: {
                  subtitles:
                    "subtitles" /*,
              Future Features
              'description': 'description',
              'thumbnails': 'thumbnails',
              'interactive': 'interactive',
              'annotation': 'annotation'*/
                }
              },
              {
                property: "label",
                title: "Label",
                description:
                  'The human-readable name for this track, eg. "English Subtitles"',
                inputMethod: "textfield"
              },
              {
                property: "src",
                title: "Source",
                description: "Source of the track",
                inputMethod: "textfield"
              },
              {
                property: "srclang",
                title:
                  'Two letter, language code, eg. \'en\' for English, "de" for German, "es" for Spanish, etc.',
                description: "Label",
                inputMethod: "textfield"
              }
            ]
          }
        ]
      }
    };
  }
  // properties available to the custom element for data binding
  static get properties() {
    let props = {
      /**
       * Is the media an audio file only?
       */
      audioOnly: {
        type: Boolean,
        value: false
      },
      /**
       * Optional accent color for controls,
       * using the following materialize "colors":
       * red, pink, purple, deep-purple, indigo, blue,
       * light blue, cyan, teal, green, light green, lime,
       * yellow, amber, orange, deep-orange, and brown.
       * Default is null.
       */
      accentColor: {
        type: String,
        value: null,
        reflectToAttribute: true
      },
      /**
       * Cross origin flag for transcripts to load
       */
      crossorigin: {
        type: String,
        value: null,
        reflectToAttribute: true
      },
      /**
       * Enables darker player.
       */
      dark: {
        type: Boolean,
        value: false,
        reflectToAttribute: true
      },
      /**
       * Use dark theme on transcript? Default is false, even when player is dark.
       */
      darkTranscript: {
        type: Boolean,
        value: false
      },
      /**
       * disable interactive mode that makes the transcript clickable
       */
      disableInteractive: {
        type: Boolean,
        value: false
      },
      /**
       * The height of the media player for non-a11y-media.
       */
      height: {
        type: String,
        value: null
      },
      /**
       * show cue's start and end time
       */
      hideTimestamps: {
        type: Boolean,
        value: false
      },
      /**
       * hide the transcript by default
       */
      hideTranscript: {
        type: Boolean,
        value: false
      },
      /**
       * Computed if this should be in an iframe or not.
       */
      iframed: {
        type: Boolean,
        computed: "_computeIframed(sourceData, sandboxed)"
      },
      /**
       * Computed if this should be in a11y-media-player.
       */
      isA11yMedia: {
        type: Boolean,
        computed: "_computeA11yMedia(sourceType, sandboxed)"
      },
      /**
       * The type of source, i.e. "local", "vimeo", "youtube", etc.
       */
      isYoutube: {
        type: Boolean,
        computed: "_computeYoutube(sourceType)"
      },
      /**
       * The language of the media
       */
      lang: {
        type: String,
        value: "en"
      },
      /**
       * Simple caption for the video
       */
      mediaTitle: {
        type: String
      },
      /**
       * What to preload for a11y-media-player: auto, metadata (default), or none.
       */
      preload: {
        type: String,
        value: "metadata"
      },
      /* *
     * Responsive video, calculated from not-responsive.
     * /
    "responsive": {
      "type": Boolean,
      "reflectToAttribute": true,
      "value": true,
    },*/
      /**
       * Compute if this is a sandboxed system or not
       */
      sandboxed: {
        type: Boolean,
        computed: "_computeSandboxed(sourceData)"
      },
      /**
       * Source of the video
       */
      source: {
        type: String,
        value: null,
        reflectToAttribute: true
      },
      /**
       * Source of the video
       */
      sources: {
        type: Array,
        value: []
      },
      /**
       * List of source objects
       */
      sourceData: {
        type: Array,
        computed: "_getSourceData(source,sources,trackData)"
      },
      /**
       * The type of source, i.e. "local", "vimeo", "youtube", etc.
       */
      sourceType: {
        type: String,
        computed: "_computeSourceType(sourceData)"
      },
      /**
       * When playing but scrolled off screen, to which corner does it "stick":
       * top-left, top-right, bottom-left, bottom-right, or none?
       * Default is "top-right". "None" disables stickiness.
       */
      stickyCorner: {
        type: String,
        value: "top-right",
        reflectToAttribute: true
      },
      /**
       * The url for a single subtitle track
       */
      track: {
        type: String,
        value: null
      },
      /**
       * Array of text tracks
       * [{
       *  "src": "path/to/track.vtt",
       *  "label": "English",
       *  "srclang": "en",
       *  "kind": "subtitles",
       * }]
       */
      tracks: {
        type: Array,
        value: []
      },
      /**
       * Cleaned array of text tracks
       * [{
       *  "src": "path/to/track.vtt",
       *  "label": "English",
       *  "srclang": "en",
       *  "kind": "subtitles",
       * }]
       */
      trackData: {
        type: Array,
        computed: "_getTrackData(track,tracks)"
      },
      /**
       * Source of optional thumbnail image
       */
      thumbnailSrc: {
        type: String,
        value: null,
        reflectToAttribute: true
      },
      /* *
     * Calculate vimeo color based on accent color.
     * /
    "vimeoColor": {
      "type": String,
      "computed": getVimeoColor(dark,accentColor),
    }, 
    */
      /**
       * The width of the media player for non-a11y-media.
       */
      width: {
        type: String,
        value: null
      },
      /**
       * The type of source, i.e. "local", "vimeo", "youtube", etc.
       */
      youtubeId: {
        type: String,
        computed: "_computeYoutubeId(source,sourceType)"
      }
    };
    if (super.properties) {
      props = Object.assign(props, super.properties);
    }
    return props;
  }
  /**
   * Store the tag name to make it easier to obtain directly.
   * @notice function name must be here for tooling to operate correctly
   */
  static get tag() {
    return "video-player";
  }
  /**
   * life cycle, element is afixed to the DOM
   */
  connectedCallback() {
    super.connectedCallback();
  }

  /**
   * Get Youtube ID
   */
  _computeYoutubeId(source, sourceType) {
    if (source !== undefined && sourceType === "youtube") {
      return this._computeSRC(source).replace(
        /(https?:\/\/)?(www.)?youtube(-nocookie)?.com\/embed\//,
        ""
      );
    }
    return false;
  }

  /**
   * Determine if it is youtube
   */
  _computeYoutube(sourceType) {
    return sourceType === "youtube";
  }

  /**
   * Determine if it is compatible with a11y-media-player
   */
  _computeA11yMedia(sourceType, sandboxed) {
    if (!sandboxed && (sourceType == "youtube" || sourceType == "local")) {
      return true;
    }
    return false;
  }

  /**
   * Compute iframed status
   */
  _computeIframed(sourceData, sandboxed) {
    // make sure we take into account sandboxing as well
    // so that we can manage the state effectively
    if (
      sourceData.length > 0 &&
      sourceData[0] !== undefined &&
      window.MediaBehaviors.Video._sourceIsIframe(sourceData[0].src) &&
      !sandboxed
    ) {
      return true;
    }
    return false;
  }

  /**
   * Gets cleaned track list
   */
  _getTrackData(track, tracks) {
    let temp =
      typeof tracks === "string" ? JSON.parse(tracks).slice() : tracks.slice();
    if (track !== undefined && track !== null && track !== "")
      temp.push({
        src: track,
        srclang: this.lang,
        label: this.lang === "en" ? "English" : this.lang,
        kind: "subtitles"
      });
    return temp;
  }

  /**
   * Gets source and added to sources list
   */
  _getSourceData(source, sources, trackData) {
    if (typeof sources === "string") sources = JSON.parse(sources);
    let root = this,
      temp = sources.slice();
    for (let i = 0; i < temp.length; i++) {
      temp[i].type =
        temp[i].type !== undefined && temp[i].type !== null
          ? temp[i].type
          : this._computeMediaType(temp[i].src);
      temp[i].src = this._computeSRC(temp[i].src);
    }
    if (source !== null) {
      let src = this._computeSRC(source);
      this.sourceType = this._computeSourceType(src);
      if (this.sourceType !== "youtube") {
        temp.unshift({ src: src, type: this._computeMediaType(src) });
      }
    }
    this.__standAlone =
      trackData === undefined || trackData === null || trackData.length < 1;
    return temp;
  }

  /**
   * Compute media type based on source, i.e. 'audio/wav' for '.wav'
   */
  _computeMediaType(source) {
    let root = this,
      audio = ["aac", "flac", "mp3", "oga", "wav"],
      video = ["mov", "mp4", "ogv", "webm"],
      type = "",
      findType = function(text, data) {
        for (let i = 0; i < data.length; i++) {
          if (
            type === "" &&
            source !== undefined &&
            source !== null &&
            source.toLowerCase().indexOf("." + data[i]) > -1
          ) {
            if (text === "audio") root.audioOnly = true;
            type = text + "/" + data[i];
          }
        }
      };
    findType("audio", audio);
    findType("video", video);
    return type;
  }

  /**
   * Compute sandboxed status
   */
  _computeSandboxed(sourceData) {
    // we have something that would require an iframe
    // see if we have a local system that would want to sandbox instead
    if (
      sourceData.length > 0 &&
      sourceData[0] !== undefined &&
      window.MediaBehaviors.Video._sourceIsIframe(sourceData[0].src)
    ) {
      // fake the creation of a webview element to see if it's valid
      // or not.
      let test = document.createElement("webview");
      // if this function exists it means that our deploy target
      // is in a sandboxed environment and is not able to run iframe
      // content with any real stability. This is beyond edge case but
      // as this is an incredibly useful tag we want to make sure it
      // can mutate to work in chromium and android environments
      // which support such sandboxing
      if (typeof test.reload === "function") {
        return true;
      }
    }
    return false;
  }

  /**
   * Compute video type based on source
   */
  _computeSourceType(sourceData) {
    let root = this;
    if (
      sourceData.length > 0 &&
      sourceData[0] !== undefined &&
      typeof sourceData[0].src !== typeof undefined
    ) {
      return window.MediaBehaviors.Video.getVideoType(sourceData[0].src);
    } else {
      return null;
    }
  }

  /**
   * Compute src from type / source combo.
   * Type is set by source so this ensures a waterfall
   * of valid values.
   */
  _computeSRC(source) {
    if (source !== null && typeof source !== undefined) {
      let type =
        this.sourceType !== undefined
          ? this.sourceType
          : window.MediaBehaviors.Video.getVideoType(source);
      // ensure that this is a valid url / cleaned up a bit
      source = window.MediaBehaviors.Video.cleanVideoSource(source, type);
      if (type == "vimeo") {
        if (this.vimeoTitle) {
          source += "?title=1";
        } else {
          source += "?title=0";
        }
        if (this.vimeoByline) {
          source += "&byline=1";
        } else {
          source += "&byline=0";
        }
        if (this.vimeoPortrait) {
          source += "&portrait=1";
        } else {
          source += "&portrait=0";
        }
        if (typeof this.videoColor !== typeof undefined) {
          source += "&color=" + this.videoColor;
        }
      } else if (type == "dailymotion") {
        source += "&ui-start-screen-info=false";
        source += "&ui-logo=false";
        source += "&sharing-enable=false";
        source += "&endscreen-enable=false";
        if (typeof this.videoColor !== typeof undefined) {
          source += "&ui-highlight=" + this.videoColor;
        }
      }
    }
    return source;
  }
  /**
   * postProcesshaxNodeToContent - clean up so we don't have empty array data
   */
  postProcesshaxNodeToContent(content) {
    content = content.replace(' sources="[]",', "");
    content = content.replace(' tracks="[]",', "");
    return content;
  }
}
window.customElements.define(VideoPlayer.tag, VideoPlayer);
export { VideoPlayer };

Support Mixins

Right now web-component-analyzer doesn't support mixins. This issue will track the implementation of that feature.

Example 1:

const SomeMixin = (Base) => {
   return class Mixin extends Base {
        static get observedAttributes() { 
            return ["c", "d"]; 
        }
    }
}

class SomeElement extends SomeMixin(HTMLElement) {
    static get observedAttributes() { 
        return ["a", "b", ...super.observedAttributes]; 
    }
}

Analyzing the above should find "a"/"b" attributes (which it does right now) and it should find "c"/"d" attributes (which it doesn't do right now).

Example 2:

type Constructor<T = {}> = new (...args: any[]) => T;
const SomeMixin = <C extends Constructor<HTMLElement>>(Base: C) => {
    class Mixin extends Base {
        @property({ type: String }) mixinProperty: string;
    }
    return Mixin;
}

@customElement("some-element")
class SomeElement extends SomeMixin(LitElement) {
    @property({ type: String }) elementProperty: string;
}

Analyzing the above should find "elementProperty" (which it does right now) and it should find "mixinProperty" (which it doesn't do right now).

Limitations

  • Right now it doesn't analyze the absent of "super" calls, so everything returned in the inheritance chain will be included. However this limitation is in another part of the analyzer, so it should be fixed in a seperate issue.
  • I will also implement support for mixin jsdoc tags, but that should be done in another issue as well.

Sort JSON output for CSS custom properties alphabetically

There is a small room for improvement after #111 has been merged: as of now, custom CSS properties are in the reverse order (compared to how they are annotated in JSDOc).

I would recommend to either keep the original order or sort alphabetically, as the issue says.

Introduce 'debug' CLI output format

Hi !
Thanks for your tool, it's really appreciated !
I've seen a regression in version 0.1.12 : cssprop are no longer generated in json output. It's working in markdown output thought.
It was working in 0.1.11.

Thanks

Fails to parse imported JSDoc types

This file

import { svg, LitElement } from 'lit-element';

/** @typedef {import('lit-element').SVGTemplateResult} SVGTemplateResult */

/**
 * @element
 */
class FailedParse extends LitElement {
  /**
   * @param {string} name
   * @return {SVGTemplateResult}
   */
  svgTemplate(name) {
    return svg`<svg id="${name}"></svg>`;
  }
}

Fails to parse.

Removing the imported typedef allows it to parse properly.

Question: how do I document a object property "interface" in pure JSDoc?

Hey ;-)

I would really like to document all my LitElement components without using any TypeScript. I was wondering how I could document the type of an object property?

Let's consider this example:

export class TestComponent extends LitElement {

  static get properties () {
    return {
      foo: { type: Object, attribute: false },
    };
  }

}

A scan gives me this:

{
  "version": 2,
  "tags": [
    {
      "name": "test-component",
      "properties": [
        {
          "name": "foo",
          "type": "{}"
        }
      ]
    }
  ]
}

I would like wca to output "type": "MyType" and also expose the definition of MyType so it could end up in the documentation.

I tried many combinations with @type, @prop and @typedef in the JSDoc on top of the class and on top of the foo: { ...} but nothing really successful.

  1. Am I the only one to be interested in this use case?
  2. How can I output "type": "MyType"?
  3. How can I expose the definition of MyType?
  4. Can I help? ;-)

Support for Private/Internal events

I have come across a scenario where I would like to emit an event from a host element to a child element in reaction to something which is intended to be internal only and not display in the documented output.

Perhaps using the @private tag as referenced in #106

Example:

@customElement('testing')
export default class Testing extends LitElement {
  render() {
    return html`<child-element></child-element>`
  }
  firstUpdated() {
    let child = document.querySelector('child-element')
    setTimeout(() => {
      child.dispatchEvent(new CustomEvent('ping'))
    }, 1000)
  }
}

Expected:

omit eventName ping from the documentation.

Refactoring and v1.0.0

I have been spending the last couple of weeks on refactoring/improving many parts of the analyzer. Most notable, I have been improving performance, jsdoc-support and member-merging. Some of the requirements meant that I had to change core parts of the analyzer, eg. to support lazy-evaluation for performance improvements. Soon I'll be finishing up the work, push it to the branch refactor, and afterwards I will begin preparing a version 1.0.0 release.

The following is a description of what have changed. Feel free to give your feedback, thoughts or additional needs. It would be especially interesting to get some feedback on my examples of how component members are merged now.

Performance

In the past, the entire component, with all of its features, was analyzed when running the analyzer. This means, that an IDE-plugin discovering custom element tag names (eg. on startup) had to analyze all elements, which could result in performance issues. The analyzed components would often be of no use, because only a subset of components are actually used in a given source file. In order to address performance issues, I'm implementing the following 4 things in WCA:

  • Lazy evaluation of component features: Features are now only analyzed when needed.
  • Lazy evaluation of types: Types are now only returned and analyzed when needed.
  • Specify relevant features: It's now possible to specify which features should be analyzed. For example, "methods" could be excluded.
  • Caching: It's now possible to cache features per node in the inheritance tree. That means that components using the same subclass/mixin will see a big performance gain.

Library exports

In order to use WCA in the browser such as https://runem.github.io/web-component-analyzer, I will make some changes to what this library exports. Specifically, I will:

  1. Separate the CLI and analyzer logic into two separate bundles (because the CLI depends on node-specific modules which can't be loaded in the browser)
  2. Export both esm and commonjs modules
  3. Export transformers (such as json and markdown transformers) as a part of the analyzer and not the CLI

Diagnostics and metadata

In the past, WCA was able to emit both documentation and diagnostics. In order to clean up things, I'm removing the functionality to emit diagnostics. Instead, it's now possible for WCA to add flavor-specific metadata to analyzed features that can be read programmatically. For example, the content of LitElement's @property decorator will now be available to read on the result. Thus lit-analyzer will now be able to do more things with LitElement-specific information, eg. to emit diagnostics.

JSDoc

I will look into supporting @extends, @mixes, @example, @method / @function, @typedef. I haven't started working on them, but the architecture supports it now :-) Existing supported jsdoc tags will work better because of improved merging logic.

The architecture

I have been making a lot of changes to the way components are analyzed. It's now possible for flavors to hook into much more than before in a much cleaner flow. This image should give an overview of the architecture:

wca-achitecture

Analyzing and Merging

Each flavor finds features on components. Features can be "properties", "attributes", "slot", eg. Multiple features can be emitted per property (eg. if you have both a constructor assignment and a class field that reference the same property). I'm making a lot of changes to how these features are merged.

Here are some of the highlights:

  • Each feature emitted will be emitted with an priority (low, medium, high)
  • Features are sorted and merged from highest to lowest priority
  • For example, properties found in the constructor are "low" priority and class fields are "high" priority
  • A given field on a feature (such as required) prefers the value of the first non-undefined value found
  • In TS-file the type checker is preferred over the @type JSDoc. In JS-file the @type JSDoc is preferred over the type checker
  • In TS-file constructor assignments are not checked (this is more aligned with what Typescript does)
  • An attribute with same name as a property is always connected to the property (this might be unwanted behavior in some cases?)

Here are some examples so you can see how merging will work. They are a bit contrived, but that's on purpose because I want to show how overlapping fields and comments are merged.

Examples using Javascript

Example 1

/**
 * @element
 * @prop {"red"|"green"} foo - Test 1
 */
class MyElement extends HTMLElement {
    /**
     * Test 2
     */
    foo = "green";

    constructor () {
        super();

        /**
         * Test 3
         */
        this.foo = "red";
    }
}

Result

Prop Name Attr Name Type Description Default Required Visibility
foo "red" | "green" Test 1 "green" (*)
  • The default value is not 100% correct in this case.

Example 2

/**
 * @element
 * @prop {MyType} foo
 */
class MyElement extends LitElement {

    static get properties () {
        return {
            /**
             * Test 1
             * @required
             */
            foo: { type: Object, attribute: false },
        };
    }

    constructor () {
        super();

        /**
         * Test 2
         * @type {"blue"|"red"}
         * @protected
         */
        this.foo = {}
    }
}

Result

Prop Name Attr Name Type Description Default Required Visibility
foo MyType Test 1 {} yes protected

Example 3

/**
 * @element
 * @prop {("blue" | "red)} foo - Test 1
 */
class MyElement extends LitElement {
    static get properties () {
        return {
            /**
             * @private
             */
            foo: { type: String, attribute: "bar" }
        }
    }

    static get observedAttributes () {
        return ["foo"];
    }

    constructor () {
        super();

        /**
         * Test 3
         * @type {string}
         * @required
         */
        this.foo = "bar 2";
    }
}

Result

Prop Name Attr Name Type Description Default Required Visibility
foo bar string Test 1 "bar 2" yes private

Examples using Typescript

Example 1

/**
 * @element
 * @prop {"red"|"green"} foo - Test 1
 */
class MyElement extends HTMLElement {
    /**
     * Test 2
     */
    foo: MyType;

    constructor () {
        super();

        /**
         * This is ignored, because we are a TS-file
         * @required
         */
        this.foo = "red";
    }
}

Result

Prop Name Attr Name Type Description Default Required Visibility
foo MyType Test 1

Example 2

/**
 * @element
 */
class MyElement extends LitElement {

    foo: "red" | "green" = "green";

    static get properties () {
        return {
            /**
             * @private
             */
            foo: { type: String, attribute: "bar" }
        }
    }
}

Result

Prop Name Attr Name Type Description Default Required Visibility
foo bar "red" | "green" Test 2 "green" private

Example 3

/**
 * @element
 * @prop {string} foo - Test 1
 */
class MyElement extends HTMLElement {

    /**
     * Test 2
     */
    foo: number = 123;


    static get observedAttributes () {
        return ["foo"];
    }
}

Result

Prop Name Attr Name Type Description Default Required Visibility
foo foo number Test 1 123

@customElement / @element quirks in JSDocBlock

The analyzer currently seems to be looking for the static tag name as supplied in the customElements.define("what-ever", WhatEver);

If this is generated off the class itself or defined elsewhere this can cause issues causing an empty output for the tag name. Possible solutions:

  • if empty, look for .tag off of the element, convention I've seen used elsewhere even though its not part of the spec
  • support an argument being passed in via CLI that defines the tag

Referencing a commit on this to see what generated blank though I'm sure almost all our elements will since we use this convention.

Errors during `npm test`

I was going to take a stab at adding a CLI test, but I ran into issues running the existing test suite. Error log below. Is there anything other than npm install; npm test that I need to run to set up the test suite?

error log

> [email protected] test /home/user/projects/wca
> ava

✔ components › custom-element-test.ts › Custom Element: Attributes

Uncaught exception in test/snapshots/stencil-progress-ring.ts

/home/user/projects/wca/test/helpers/source-file-test.ts:30

29: console.log(_compiledResult.program.getSourceFiles().map(sf => sf.fi…
30: throw new Error(Couldn't find source file match: ${fileName});
31: } else if (sourceFiles.length > 1) {

Error: Couldn't find source file match: /progress-ring-component/dist/types/components.d.ts

getSourceFile (test/helpers/source-file-test.ts:30:9)
testResult (test/helpers/source-file-test.ts:40:8)
Object.testResultSnapshot (test/helpers/source-file-test.ts:49:2)
Object. (test/snapshots/stencil-progress-ring.ts:4:1)
Module.m._compile (node_modules/ts-node/src/index.ts:439:23)
Object.require.extensions.(anonymous function) [as .ts] (node_modules/ts-node/src/index.ts:442:12)

Uncaught exception in test/snapshots/polymer-app-layout.ts

/home/user/projects/wca/test/helpers/source-file-test.ts:30

29: console.log(_compiledResult.program.getSourceFiles().map(sf => sf.fi…
30: throw new Error(Couldn't find source file match: ${fileName});
31: } else if (sourceFiles.length > 1) {

Error: Couldn't find source file match: @polymer/app-layout/app-drawer/app-drawer.d.ts

getSourceFile (test/helpers/source-file-test.ts:30:9)
testResult (test/helpers/source-file-test.ts:40:8)
Object.testResultSnapshot (test/helpers/source-file-test.ts:49:2)
Object. (test/snapshots/polymer-app-layout.ts:4:1)
Module.m._compile (node_modules/ts-node/src/index.ts:439:23)
Object.require.extensions.(anonymous function) [as .ts] (node_modules/ts-node/src/index.ts:442:12)

Uncaught exception in test/snapshots/lit-element-mwc.ts

/home/user/projects/wca/test/helpers/source-file-test.ts:30

29: console.log(_compiledResult.program.getSourceFiles().map(sf => sf.fi…
30: throw new Error(Couldn't find source file match: ${fileName});
31: } else if (sourceFiles.length > 1) {

Error: Couldn't find source file match: @material/mwc-button/mwc-button.d.ts

getSourceFile (test/helpers/source-file-test.ts:30:9)
testResult (test/helpers/source-file-test.ts:40:8)
Object.testResultSnapshot (test/helpers/source-file-test.ts:49:2)
Object. (test/snapshots/lit-element-mwc.ts:4:1)
Module.m._compile (node_modules/ts-node/src/index.ts:439:23)
Object.require.extensions.(anonymous function) [as .ts] (node_modules/ts-node/src/index.ts:442:12)

Uncaught exception in test/snapshots/stencil-ionic.ts

/home/user/projects/wca/test/helpers/source-file-test.ts:30

29: console.log(_compiledResult.program.getSourceFiles().map(sf => sf.fi…
30: throw new Error(Couldn't find source file match: ${fileName});
31: } else if (sourceFiles.length > 1) {

Error: Couldn't find source file match: @material/mwc-button/mwc-button.d.ts

getSourceFile (test/helpers/source-file-test.ts:30:9)
testResult (test/helpers/source-file-test.ts:40:8)
Object.testResultSnapshot (test/helpers/source-file-test.ts:49:2)
Object. (test/snapshots/stencil-ionic.ts:4:1)
Module.m._compile (node_modules/ts-node/src/index.ts:439:23)
Object.require.extensions.(anonymous function) [as .ts] (node_modules/ts-node/src/index.ts:442:12)

Uncaught exception in test/snapshots/polymer-paper-button.ts

/home/user/projects/wca/test/helpers/source-file-test.ts:30

29: console.log(_compiledResult.program.getSourceFiles().map(sf => sf.fi…
30: throw new Error(Couldn't find source file match: ${fileName});
31: } else if (sourceFiles.length > 1) {

Error: Couldn't find source file match: @polymer/paper-button/paper-button.d.ts

getSourceFile (test/helpers/source-file-test.ts:30:9)
testResult (test/helpers/source-file-test.ts:40:8)
Object.testResultSnapshot (test/helpers/source-file-test.ts:49:2)
Object. (test/snapshots/polymer-paper-button.ts:4:1)
Module.m._compile (node_modules/ts-node/src/index.ts:439:23)
Object.require.extensions.(anonymous function) [as .ts] (node_modules/ts-node/src/index.ts:442:12)

Uncaught exception in test/snapshots/vanilla-masonry-layout.ts

/home/user/projects/wca/test/helpers/source-file-test.ts:30

29: console.log(_compiledResult.program.getSourceFiles().map(sf => sf.fi…
30: throw new Error(Couldn't find source file match: ${fileName});
31: } else if (sourceFiles.length > 1) {

Error: Couldn't find source file match: @appnest/masonry-layout/masonry-layout.d.ts

getSourceFile (test/helpers/source-file-test.ts:30:9)
testResult (test/helpers/source-file-test.ts:40:8)
Object.testResultSnapshot (test/helpers/source-file-test.ts:49:2)
Object. (test/snapshots/vanilla-masonry-layout.ts:4:1)
Module.m._compile (node_modules/ts-node/src/index.ts:439:23)
Object.require.extensions.(anonymous function) [as .ts] (node_modules/ts-node/src/index.ts:442:12)

✖ test/snapshots/stencil-progress-ring.ts exited with a non-zero exit code: 1
✖ test/snapshots/vanilla-masonry-layout.ts exited with a non-zero exit code: 1
✖ test/snapshots/polymer-app-layout.ts exited with a non-zero exit code: 1
✖ test/snapshots/polymer-paper-button.ts exited with a non-zero exit code: 1
✖ test/snapshots/lit-element-mwc.ts exited with a non-zero exit code: 1
✖ test/snapshots/stencil-ionic.ts exited with a non-zero exit code: 1

1 test passed
6 uncaught exceptions

Crash when analyzing a custom element with a field that's a mixin

Analyzing this code:

type Constructor<T> = {
  new (...args: any[]): T
};
type ConstructorOne = Constructor<{someMethod(): void;}>;
type ConstructorTwo = Constructor<{someMethod(): void;}>;

type Combined = ConstructorOne&ConstructorTwo;
declare const Combined: Combined;

declare class ExtendsCombined extends Combined {}

export class C extends HTMLElement {
  field!: ExtendsCombined;
}
customElements.define('s-e', C);

results in the following crash:

TypeError: Cannot read property 'kind' of undefined
    at Object.isBindingElement (/Users/rictic/open/fuck/repro-mixin-simple-type/node_modules/typescript/lib/typescript.js:13739:21)
    at getCombinedFlags (/Users/rictic/open/fuck/repro-mixin-simple-type/node_modules/typescript/lib/typescript.js:13081:16)
    at Object.getCombinedModifierFlags (/Users/rictic/open/fuck/repro-mixin-simple-type/node_modules/typescript/lib/typescript.js:13098:16)
    at getModifiersFromDeclaration (/Users/rictic/open/fuck/repro-mixin-simple-type/node_modules/ts-simple-type/lib/index.cjs.js:230:37)
    at /Users/rictic/open/fuck/repro-mixin-simple-type/node_modules/ts-simple-type/lib/index.cjs.js:499:28
    at Array.map (<anonymous>)
    at toSimpleTypeInternal (/Users/rictic/open/fuck/repro-mixin-simple-type/node_modules/ts-simple-type/lib/index.cjs.js:497:63)
    at toSimpleTypeInternalCaching (/Users/rictic/open/fuck/repro-mixin-simple-type/node_modules/ts-simple-type/lib/index.cjs.js:291:33)
    at toSimpleType (/Users/rictic/open/fuck/repro-mixin-simple-type/node_modules/ts-simple-type/lib/index.cjs.js:257:12)
    at Object.isAssignableToSimpleTypeKind (/Users/rictic/open/fuck/repro-mixin-simple-type/node_modules/ts-simple-type/lib/index.cjs.js:654:45)

This is a minimal reduction of a bug from a LitElement class that had a field that was an array of PolymerElements, whose type is defined by application of a number of class mixins.

Analyze seems to pick up variables inside my LitElement code (in ternary operators)

Hey, I'm not sure what I'm doing wrong but I tried to reduce the test case:

import { html, LitElement } from 'lit-element';

export class TestComponent extends LitElement {
  render () {
    const someBoolean = (42 > 2);
    return html`<div>${someBoolean ? 'yes' : 'no'}</div>`;
  }
}

window.customElements.define('test-component', TestComponent);

When I run wca, the JSON output picks up the internal variable someBoolean.

{
  "version": 2,
  "tags": [
    {
      "name": "test-component",
      "properties": [
        {
          "name": "someBoolean",
          "type": "\"yes\" | \"no\""
        }
      ]
    }
  ]
}
  1. Why is this doing this?
  2. Is this a bug?
  3. Can I help? ;-)

Extraction of some doc tags is failing

Thanks for creating this great project! I am able to use it for some of our api doc extraction already.

I have hit two problems in my Typescript/LitElement project:

  1. If I add extra doc tags for things that are not automatically picked up, e.g. @fires change, those seem to be ignored
  2. Some of the tags from your examples trigger errors when scanning.

I am seeing errors like this:

Fatal error:  Invalid regular expression: /^(\s*?\{(?<type>.*)\})?(((?<name1>.+)(\s\-\s)(?<comment1>.+))|(\s?\-\s)(?<comment2>.+)|(?<name2>.*?))$/: Invalid group
SyntaxError: Invalid regular expression: /^(\s*?\{(?<type>.*)\})?(((?<name1>.+)(\s\-\s)(?<comment1>.+))|(\s?\-\s)(?<comment2>.+)|(?<name2>.*?))$/: Invalid group
    at parseJsDocTag (/Users/mitaylor/Scratch/wc-analyze-test/node_modules/web-component-analyzer/lib/index.cjs.js:241:17)

I have attached a simple test project that demonstrates this (wc-analyze-test.zip).

  1. Unzip the directory
  2. cd wc-analyze-test
  3. npm install
  4. npm run analyze

If I delete some of the tags from component.ts, it runs, but the @fires is ignored. Edit the JSDoc comment to look like:

/**
 * Here is a description of my web component.
 * 
 * @element my-element
 * 
 * @fires change - This jsdoc tag makes it possible to document events.
 * @fires submit
 */

Then try npm analyze again. It should run, but you won't see any mention of the change event in the output.

Interfaces that extend imported interfaces don't get extended fields

Consider a project like:

// base.ts
export interface Checked {
  checked: boolean;
}
// main.ts
import {Checked} from './base';

interface CheckableElement extends HTMLElement, Checked {}

declare global {
  interface HTMLElementTagNameMap {
    'checkable-element': CheckableElement;
  }
}

web-component-analyzer appears not to know that <checkable-element> has a .checked property. The issue seems to be specific to interfaces that extend imported interfaces. Specifically, I checked these cases:

  • ✅class extending another class declared in the same file
  • ✅class extending another class imported from another file
  • ✅interface extending an interface declared in the same file
  • ❌interface extending an interface imported from another file

Standalone repro at https://github.com/rictic/repro-wca-import-interface-issue

Multiple elements -> Existing README

It would be nice if wca had an option to concatenate its output to an existing README.md, while bumping up it's header levels.

Input:

/**
 * @element 'super-man'
 * @attr {String} [weakness="kryptonite"] - supe's ultimate weakness
 *
 * ### Usage
 * ```html
 * <super-man weakness="lois-lane"></super-man>
 * ```
 */
class SuperMan extends HTMLElement {}

/**
 * @element 'spider-man'
 * @attr {'classic'|'alien'} [suit="classic"] - type of spidey suit
 *
 * ### Usage
 * ```html
 * <spider-man suit="classic"></spider-man>
 * ```
 */
class SpiderMan extends HTMLElement {}

Existing README.md

# Super Elements
Super elements for super developers.
Here's a bunch of emoji
[![made with open-wc](https://img.shields.io/badge/made%20with-open--wc-%23217ff9)](https://open-wc.org)
[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](collection/super-elements/smart-elements-community)

Output:

# Super Elements
Super elements for super developers.
Here's a bunch of emoji
[![made with open-wc](https://img.shields.io/badge/made%20with-open--wc-%23217ff9)](https://open-wc.org)
[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](collection/super-elements/smart-elements-community)

## super-man

### Usage
```html
<super-man weakness="lois-lane"></super-man>
```

| Property   | Attribute  | Modifiers | Type     | Default      | Description              |
|------------|------------|-----------|----------|--------------|--------------------------|
| `weakness` | `weakness` |           | `String` | 'kryptonite' | supe's ultimate weakness |

## spider-man

### Usage
```html
<spider-man suit="classic"></spider-man>
```

| Property | Attribute | Modifiers | Type             | Default   | Description         |
|----------|-----------|-----------|------------------|-----------|---------------------|
| `suit`   | `suit`    |           | `classic|alien`  | 'classic' | type of spidey suit |

This could be accomplished by the user with a little bash scripting and some regex, but it would be nice to have some --merge flag that defaults to README.md

Custom interfaces for ts/tsx

Hello,
Thank you for your effort on building this useful tool 👍

I have a question regarding name of the interface that is used during the analyze of ts files:
Would be feasible to customize the name of an interface for analysis. For instance, HTMLElementTagNameMap to IntrinsicElements?

What do you think?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.