Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 123 additions & 48 deletions schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
"description": "The name of the field this attribute is associated with, if any.",
"type": "string"
},
"inheritedFrom": {
"$ref": "#/definitions/Reference"
},
"name": {
"type": "string"
},
Expand Down Expand Up @@ -216,39 +219,13 @@
],
"type": "object"
},
"CustomElement": {
"description": "Description of a custom element class.\n\nCustom elements are JavaScript classes, so this extends from\n`ClassDeclaration` and adds custom-element-specific features like\nattributes, events, and slots.\n\nNote that `tagName` in this interface is optional. Tag names are not\nneccessarily part of a custom element class, but belong to the definition\n(often called the \"registration\") or the `customElements.define()` call.\n\nBecause classes and tag anmes can only be registered once, there's a\none-to-one relationship between classes and tag names. For ease of use,\nwe allow the tag name here.\n\nSome packages define and register custom elements in separate modules. In\nthese cases one `Module` should contain the `CustomElement` without a\ntagName, and another `Module` should contain the\n`CustomElement`.",
"CustomElementDeclaration": {
"description": "A description of a custom element class.\n\nCustom elements are JavaScript classes, so this extends from\n`ClassDeclaration` and adds custom-element-specific features like\nattributes, events, and slots.\n\nNote that `tagName` in this interface is optional. Tag names are not\nneccessarily part of a custom element class, but belong to the definition\n(often called the \"registration\") or the `customElements.define()` call.\n\nBecause classes and tag anmes can only be registered once, there's a\none-to-one relationship between classes and tag names. For ease of use,\nwe allow the tag name here.\n\nSome packages define and register custom elements in separate modules. In\nthese cases one `Module` should contain the `CustomElement` without a\ntagName, and another `Module` should contain the\n`CustomElement`.",
"properties": {
"attributes": {
"description": "The attributes that this element is known to understand.",
"items": {
"$ref": "#/definitions/Attribute"
},
"type": "array"
},
"cssProperties": {
"items": {
"$ref": "#/definitions/CssCustomProperty"
},
"type": "array"
},
"demos": {
"items": {
"$ref": "#/definitions/Demo"
},
"type": "array"
},
"description": {
"description": "A markdown description of the class.",
"type": "string"
},
"events": {
"description": "The events that this element fires.",
"items": {
"$ref": "#/definitions/Event"
},
"type": "array"
},
"kind": {
"enum": [
"class"
Expand Down Expand Up @@ -277,29 +254,12 @@
"name": {
"type": "string"
},
"parts": {
"items": {
"$ref": "#/definitions/CssPart"
},
"type": "array"
},
"slots": {
"description": "The shadow dom content slots that this element accepts.",
"items": {
"$ref": "#/definitions/Slot"
},
"type": "array"
},
"summary": {
"description": "A markdown summary suitable for display in a listing.",
"type": "string"
},
"superclass": {
"$ref": "#/definitions/Reference"
},
"tagName": {
"description": "An optional tag name that should be specified if this is a\nself-registering element.\n\nSelf-registering elements must also include a CustomElementExport\nin the module's exports.",
"type": "string"
}
},
"required": [
Expand Down Expand Up @@ -355,6 +315,9 @@
"description": "A markdown description.",
"type": "string"
},
"inheritedFrom": {
"$ref": "#/definitions/Reference"
},
"name": {
"type": "string"
},
Expand Down Expand Up @@ -452,11 +415,14 @@
{
"$ref": "#/definitions/FunctionDeclaration"
},
{
"$ref": "#/definitions/MixinDeclaration"
},
{
"$ref": "#/definitions/VariableDeclaration"
},
{
"$ref": "#/definitions/CustomElement"
"$ref": "#/definitions/CustomElementDeclaration"
}
]
},
Expand Down Expand Up @@ -501,6 +467,115 @@
],
"type": "object"
},
"MixinDeclaration": {
"description": "A description of a mixin.\n\nMixins are functions which generate a new subclass of a given superclass.\nThis interfaces describes the class and custom element features that\nare added by the mixin. As such, it extends the CustomElement interface and\nClassLike interface.\n\nSince mixins are functions, it also extends the FunctionLike interface. This\nmeans a mixin is callable, and has parameters and a return type.\n\nThe return type is often hard or impossible to accurately describe in type\nsystems like TypeScript. It requires generics and an `extends` operator\nthat TypeScript lacks. Therefore it's recommended that the return type is\nleft empty. The most common form of a mixin function takes a single\nargument, so consumers of this interface should assume that the return type\nis the single argument subclassed by this declaration.\n\nA mixin should only have a superclass if it composes another mixin, and the\nsuperclass should reference that mixin.",
"properties": {
"attributes": {
"description": "The attributes that this element is known to understand.",
"items": {
"$ref": "#/definitions/Attribute"
},
"type": "array"
},
"cssParts": {
"items": {
"$ref": "#/definitions/CssPart"
},
"type": "array"
},
"cssProperties": {
"items": {
"$ref": "#/definitions/CssCustomProperty"
},
"type": "array"
},
"demos": {
"items": {
"$ref": "#/definitions/Demo"
},
"type": "array"
},
"description": {
"description": "A markdown description of the class.",
"type": "string"
},
"events": {
"description": "The events that this element fires.",
"items": {
"$ref": "#/definitions/Event"
},
"type": "array"
},
"kind": {
"enum": [
"mixin"
],
"type": "string"
},
"members": {
"items": {
"anyOf": [
{
"$ref": "#/definitions/ClassField"
},
{
"$ref": "#/definitions/ClassMethod"
}
]
},
"type": "array"
},
"mixins": {
"items": {
"$ref": "#/definitions/Reference"
},
"type": "array"
},
"name": {
"type": "string"
},
"parameters": {
"items": {
"$ref": "#/definitions/Parameter"
},
"type": "array"
},
"return": {
"properties": {
"description": {
"type": "string"
},
"type": {
"$ref": "#/definitions/Type"
}
},
"type": "object"
},
"slots": {
"description": "The shadow dom content slots that this element accepts.",
"items": {
"$ref": "#/definitions/Slot"
},
"type": "array"
},
"summary": {
"description": "A markdown summary suitable for display in a listing.",
"type": "string"
},
"superclass": {
"$ref": "#/definitions/Reference"
},
"tagName": {
"description": "An optional tag name that should be specified if this is a\nself-registering element.\n\nSelf-registering elements must also include a CustomElementExport\nin the module's exports.",
"type": "string"
}
},
"required": [
"kind",
"name"
],
"type": "object"
},
"Parameter": {
"properties": {
"default": {
Expand Down Expand Up @@ -652,7 +727,7 @@
"type": "object"
}
},
"description": "The top-level interface of a custom-elements.json file.\n\nBecause custom elements are JavaScript classes, describing a custom element\nmay require describing arbitrary JavaScript concepts like modules, classes,\nfunctions, etc. So custom-elements.json documents are capable of documenting\nthe elements in a package, as well as those JavaScript concepts.\n\nThe modules described in a package should be the public entrypoints that\nother packages may import from. Multiple modules may export the same object\nvia re-exports, but in most cases a package should document the single\ncanonical export that should be used.",
"description": "The top-level interface of a custom elements manifest file.\n\nBecause custom elements are JavaScript classes, describing a custom element\nmay require describing arbitrary JavaScript concepts like modules, classes,\nfunctions, etc. So custom elements manifests are capable of documenting\nthe elements in a package, as well as those JavaScript concepts.\n\nThe modules described in a package should be the public entrypoints that\nother packages may import from. Multiple modules may export the same object\nvia re-exports, but in most cases a package should document the single\ncanonical export that should be used.",
"properties": {
"modules": {
"description": "An array of the modules this package contains.",
Expand All @@ -666,7 +741,7 @@
"type": "string"
},
"schemaVersion": {
"description": "The version of the custom-elements.json schema used in this file.",
"description": "The version of the schema used in this file.",
"type": "string"
}
},
Expand Down
61 changes: 56 additions & 5 deletions schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export type Declaration =
| FunctionDeclaration
| MixinDeclaration
| VariableDeclaration
| CustomElement;
| CustomElementDeclaration;

/**
* A reference to an export of a module.
Expand All @@ -149,7 +149,7 @@ export interface Reference {
}

/**
* Description of a custom element class.
* A description of a custom element class.
*
* Custom elements are JavaScript classes, so this extends from
* `ClassDeclaration` and adds custom-element-specific features like
Expand All @@ -168,7 +168,12 @@ export interface Reference {
* tagName, and another `Module` should contain the
* `CustomElement`.
*/
export interface CustomElement extends ClassDeclaration {
export interface CustomElementDeclaration extends ClassDeclaration {}

/**
* The additional fields that a custom element adds to classes and mixins.
*/
export interface CustomElement extends ClassLike {
/**
* An optional tag name that should be specified if this is a
* self-registering element.
Expand All @@ -193,7 +198,7 @@ export interface CustomElement extends ClassDeclaration {
*/
slots?: Slot[];

parts?: CssPart[];
cssParts?: CssPart[];

cssProperties?: CssCustomProperty[];

Expand Down Expand Up @@ -404,9 +409,55 @@ export interface ClassMethod extends FunctionLike {
}

/**
* A description of a mixin.
*
* Mixins are functions which generate a new subclass of a given superclass.
* This interfaces describes the class and custom element features that
* are added by the mixin. As such, it extends the CustomElement interface and
* ClassLike interface.
Comment thread
justinfagnani marked this conversation as resolved.
*
* Since mixins are functions, it also extends the FunctionLike interface. This
* means a mixin is callable, and has parameters and a return type.
*
* The return type is often hard or impossible to accurately describe in type
* systems like TypeScript. It requires generics and an `extends` operator
* that TypeScript lacks. Therefore it's recommended that the return type is
* left empty. The most common form of a mixin function takes a single
* argument, so consumers of this interface should assume that the return type
* is the single argument subclassed by this declaration.
*
* A mixin should only have a superclass if it composes another mixin, and the
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Im not sure I understand.

Given:

export const MixinA = base => class extends MixinB(base){}

Here the MixinB would be "superclass"?

What in the case of:

export const MixinA = base => class extends MixinC(MixinB(base)){}

?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was updated to say "a mixins should not have a superclass". In your examples the other mixins would be included in the mixins field of MixinA.

* superclass should reference that mixin.
*
* @example
*
* This JavaScript mixin declaration:
* ```javascript
* const MyMixin = (base) => class extends base {
* foo() { ... }
* }
* ```
*
* Is described by this JSON:
* ```json
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it still have a "superclass" field that has as value base here? Thats how I currently handle it in @custom-elements-manifest/analyzer, but it might be redundant I guess?

Copy link
Copy Markdown
Collaborator Author

@justinfagnani justinfagnani Nov 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No - mixins (usually) don't have superclasses until they are applied to a specific superclass.

When mixins do have a superclass, it's because of mixin composition:

const MixinA = (base) => class extends base {
  fieldA;
}

const MixinB = (base) class extends MixinA(base) {
  fieldB;
}

class C extends MixinB(S) {
}
const c = new C();
c.fieldA; // exists
c.fieldB; // exists

So here the superclass of MixinB would be MixinA.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a short mention of this in the docs

Copy link
Copy Markdown
Collaborator

@thepassle thepassle Nov 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a mixin has nested mixins, though, are those nested mixins then documented as being mixins, or superclasses? E.g.:

const MixinA = (base) => class extends base {
  fieldA;
}

const MixinB = (base) => class extends MixinA(base) {
  fieldB;
}

Would the mixin declaration look like this:

        {
          "kind": "mixin",
          "description": "",
          "name": "MixinB",
          "mixins": [
            {
              "name": "MixinA"
            }
          ],
         "parameters": [
             {
               "name": "base"
             }
           ],
          "members": ["//etc"]
        }

If we consider MixinA to be the superclass in that case, we have to change superclass to be superclasses and be an array, because there can be multiple mixins applied.

Intuitively, I feel like I'd prefer to name "nested mixins" just mixins in this case though

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point, I'll change the comments to says composed mixins should go in the mixins field.

* {
* "kind": "mixin",
* "name": "MyMixin",
* "parameters": [
* {
* "name": "base",
* }
* ],
* "members": [
* {
* "kind": "method",
* "name": "foo",
* }
* ]
* }
* ```
*/
export interface MixinDeclaration extends ClassLike, FunctionLike {
export interface MixinDeclaration extends CustomElement, FunctionLike {
kind: 'mixin';
}

Expand Down