Skip to content

Refactoring and v1.0.0 #125

@runem

Description

@runem

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions