open-wc / custom-elements-manifest Goto Github PK
View Code? Open in Web Editor NEWCustom Elements Manifest is a file format that describes custom elements in your project.
Home Page: https://custom-elements-manifest.open-wc.org/
Custom Elements Manifest is a file format that describes custom elements in your project.
Home Page: https://custom-elements-manifest.open-wc.org/
web-component-analyzer is allowing to drill down to node_modules
folder, it's essential functionality for my project and it's a show stopper for this tool.
As the reproduction showcases the analyzer will fail if the dispatched custom event has no type argument. There are occasionally cases where no type is required, see this example.
Error thrown location: ERROR
Reproduction Link: REPRODUCTION
Preferably the analyzer will go one step beyond and analyze the event if it's a CustomEvent
. This includes collecting documentation from the class declaration, and checking the super(...)
call for a type
argument.
This function accidentally gives events privacy. In the schema, events are specced not to have privacy, so we should remove it.
Quick fix:
delete event.privacy;
But double check to see if we handle events anywhere else in the code, and remove the privacy there also
Currently If you have a class with private class fields, cem outputs those fields.
class Foo {
#bar = 'bar';
}
Example cem output (from another lit element web component):
{
"kind": "field",
"name": "#initials",
"privacy": "private",
"type": {
"text": "string | undefined"
},
"description": "The actual initials shown in the UI. We keep this as a separate variable to determine\nwhether the initials are derived from the displayName or explicitly set via the initials property."
},
Unlike typescript private
keyword fields, private class fields are really private. So trying to access foo.#bar
results in a syntax error.
Since you cannot access these private class fields from outside the class, should cem really output these fields?
Checklist
--dev
flag to get more information?Completing the items above will greatly improve triaging time of your issue.
Expected behavior
When I use a mixin for retrieving a Custom Element class, and extend using a mixin. I expect the attributes from the mixin I'm extending on te become available.
Some code to demonstrate:
class MyElementA extends getClassA(HTMLElement){}
class MyElementAB extends getClassAB(MyElementA){};
function getClassA(superClass = HTMLElement){
return class extends superClass {
static get observedAttributes() {
return ['a'];
}
set a(val) {
this._a = val;
}
get a() {
return this._a;
}
}
}
function getClassAB(superClass = getClassA()){
return class extends superClass {
static get observedAttributes() {
return ['b'];
}
set b(val) {
this._b = val;
}
get b() {
return this._b;
}
}
}
customElements.define('my-mixin-element-a', MyElementA);
customElements.define('my-mixin-element-ab', MyElementAB);
//customElements.define('my-element-a', getClassA);
//customElements.define('my-element-ab', getClassAB);
The result has no attributes at all:
{
"schemaVersion": "1.0.0",
"readme": "",
"modules": [
{
"kind": "javascript-module",
"path": "src/my-element.js",
"declarations": [
{
"kind": "class",
"description": "",
"name": "MyElementA",
"mixins": [
{
"name": "getClassA",
"module": "src/my-element.js"
}
],
"superclass": {
"name": "HTMLElement"
},
"tagName": "my-mixin-element-a",
"customElement": true
},
{
"kind": "class",
"description": "",
"name": "MyElementAB",
"mixins": [
{
"name": "getClassAB",
"module": "src/my-element.js"
}
],
"superclass": {
"name": "MyElementA",
"module": "src/my-element.js"
},
"tagName": "my-mixin-element-ab",
"customElement": true
}
],
"exports": [
{
"kind": "custom-element-definition",
"name": "my-mixin-element-a",
"declaration": {
"name": "MyElementA",
"module": "src/my-element.js"
}
},
{
"kind": "custom-element-definition",
"name": "my-mixin-element-ab",
"declaration": {
"name": "MyElementAB",
"module": "src/my-element.js"
}
}
]
}
]
}
When I uncomment the last two lines of the example code, the result changes to this (and has the expected attributes)
{
"schemaVersion": "1.0.0",
"readme": "",
"modules": [
{
"kind": "javascript-module",
"path": "src/my-element.js",
"declarations": [
{
"kind": "class",
"description": "",
"name": "MyElementA",
"mixins": [
{
"name": "getClassA",
"module": "src/my-element.js"
}
],
"superclass": {
"name": "HTMLElement"
},
"tagName": "my-mixin-element-a",
"customElement": true,
"attributes": [
{
"name": "a",
"inheritedFrom": {
"name": "getClassA",
"module": "src/my-element.js"
}
}
],
"members": [
{
"kind": "field",
"name": "a",
"inheritedFrom": {
"name": "getClassA",
"module": "src/my-element.js"
}
}
]
},
{
"kind": "class",
"description": "",
"name": "MyElementAB",
"mixins": [
{
"name": "getClassAB",
"module": "src/my-element.js"
}
],
"superclass": {
"name": "MyElementA",
"module": "src/my-element.js"
},
"tagName": "my-mixin-element-ab",
"customElement": true,
"attributes": [
{
"name": "b",
"inheritedFrom": {
"name": "getClassAB",
"module": "src/my-element.js"
}
},
{
"name": "a",
"inheritedFrom": {
"name": "MyElementA",
"module": "src/my-element.js"
}
}
],
"members": [
{
"kind": "field",
"name": "b",
"inheritedFrom": {
"name": "getClassAB",
"module": "src/my-element.js"
}
},
{
"kind": "field",
"name": "a",
"inheritedFrom": {
"name": "MyElementA",
"module": "src/my-element.js"
}
}
]
},
{
"kind": "mixin",
"description": "",
"name": "getClassA",
"members": [
{
"kind": "field",
"name": "a"
}
],
"attributes": [
{
"name": "a"
}
],
"parameters": [
{
"name": "superClass",
"default": "HTMLElement"
}
]
},
{
"kind": "mixin",
"description": "",
"name": "getClassAB",
"members": [
{
"kind": "field",
"name": "b"
}
],
"attributes": [
{
"name": "b"
}
],
"parameters": [
{
"name": "superClass",
"default": "getClassA()"
}
]
}
],
"exports": [
{
"kind": "custom-element-definition",
"name": "my-mixin-element-a",
"declaration": {
"name": "MyElementA",
"module": "src/my-element.js"
}
},
{
"kind": "custom-element-definition",
"name": "my-mixin-element-ab",
"declaration": {
"name": "MyElementAB",
"module": "src/my-element.js"
}
},
{
"kind": "custom-element-definition",
"name": "my-element-a",
"declaration": {
"name": "getClassA",
"module": "src/my-element.js"
}
},
{
"kind": "custom-element-definition",
"name": "my-element-ab",
"declaration": {
"name": "getClassAB",
"module": "src/my-element.js"
}
}
]
}
]
}
The above result is only retrieved when all 4 lines are uncommented:
customElements.define('my-mixin-element-a', MyElementA);
customElements.define('my-mixin-element-ab', MyElementAB);
customElements.define('my-element-a', getClassA);
customElements.define('my-element-ab', getClassAB);
Hey team ๐
I tried using outdir
in my custom-elements-manifest.config.mjs
but the file still gets generated at the default location. Using the CLI param works.
export default {
globs: ['src/**/cc-*.js'],
// Has no effect for now
outdir: 'dist',
plugins: [
],
};
Hi! ๐
Firstly, thanks for your work on this project! ๐
Today I used patch-package to patch @custom-elements-manifest/[email protected]
for the project I'm working on.
This should work, the only thing that I'm not sure about is the type imports from the custom-elements-manifest
package, which didn't appear to work for me, so please check.
Here is the diff that solved my problem:
diff --git a/node_modules/@custom-elements-manifest/analyzer/index.d.ts b/node_modules/@custom-elements-manifest/analyzer/index.d.ts
new file mode 100644
index 0000000..994d363
--- /dev/null
+++ b/node_modules/@custom-elements-manifest/analyzer/index.d.ts
@@ -0,0 +1,103 @@
+import * as TS from 'typescript'
+import { Module, Package } from 'custom-elements-manifest';
+
+/** Plugin execution context. Pass arbitrary data here. */
+export type Context = Record<string, unknown>;
+
+export interface CollectPhaseParams {
+ /**
+ * TypeScript API
+ */
+ ts: TS;
+
+ /**
+ * The current TypeScript AST Node
+ */
+ node: TS.Node;
+
+ /**
+ * Plugin execution context. Pass arbitrary data here.
+ */
+ context: Context;
+}
+
+export interface AnalyzePhaseParams {
+ /**
+ * TypeScript API
+ */
+ ts: TS;
+
+ /**
+ * The current TypeScript AST Node
+ */
+ node: TS.Node;
+
+ /**
+ * The current state of the current module's manifest
+ */
+ moduleDoc: Partial<Module>;
+
+ /**
+ * Plugin execution context. Pass arbitrary data here.
+ */
+ context: Context;
+}
+
+export interface ModuleLinkPhaseParams {
+ /**
+ * The completed manifest, i.e. the output of the analyze phase
+ */
+ moduleDoc: Module;
+
+ /**
+ * Plugin execution context. Pass arbitrary data here.
+ */
+ context: Context;
+}
+
+export interface PackageLinkPhaseParams {
+ /**
+ * The completed manifest, i.e. the output of the analyze phase
+ */
+ customElementsManifest: Package;
+
+ /**
+ * Plugin execution context. Pass arbitrary data here.
+ */
+ context: Context;
+}
+
+/**
+ * A Custom Elements Manifest Analyzer plugin
+ */
+export interface Plugin {
+ /**
+ * @summary Plugin hook that runs in the analyze phase.
+ *
+ * Runs for all modules in a project, before continuing to the `analyzePhase`
+ */
+ collectPhase(params: CollectPhaseParams): void;
+
+ /**
+ * @summary Plugin hook that runs in the analyze phase.
+ *
+ * Runs for each AST node in each module.
+ * You can use this phase to access a module's AST nodes and mutate the manifest.
+ */
+ analyzePhase(params: AnalyzePhaseParams): void;
+
+ /**
+ * @summary Plugin hook that runs in the module-link phase.
+ *
+ * Post-processing hook that runs for each module, after analyzing.
+ * All information about your module should now be available.
+ */
+ moduleLinkPhase(params: ModuleLinkPhaseParams): void;
+
+ /**
+ * @summary Plugin hook that runs in the package-link phase.
+ *
+ * Runs once per package, after modules have been parsed and after per-module post-processing
+ */
+ packageLinkPhase(params: PackageLinkPhaseParams): void;
+}
This issue body was partially generated by patch-package.
custom-elements-manifest/analyzer
automatically outputs the schema file custom-elements.json
into whichever directory you run the analyzer, but I'd like to be able to specify a path for the file to output into.
For example, I have a package called foo-button
. When analyzing files inside /foo-button/src/
, I want custom-elements.json
to output into /foo-button/dist/
instead of in the current directory /foo-button/
:
foo-button/
โโโ dist/
โ โโโ custom-elements.json
โโโ src/
โ โโโ foo-button.ts
โ โโโ foo-button-dropdown.ts
โ โโโ foo-button-group.ts
โ โโโ index.ts
โโโ package.json
Here's the script I'm using to analyze files in /foo-button/
:
cem analyze --globs "./src/foo-*.ts" --litelement
Would like a flag like this to specify output path:
cem analyze --globs "./src/foo-*.ts" --litelement --out "./dist/"
Thanks!
Checklist
--dev
flag to get more information?In writing a custom element to render custom elements manifest in a friendly format, we've encountered an issue when trying to generate the custom element manifest for this component.
I've included a txt file at the end so I could identify the cyclical items in the object that is being passed to JSON.stringify
We're using latest of lit (2.0.0-rc.2) and TypeScript 4.3.4.
TypeError: Converting circular structure to JSON
--> starting at object with constructor 'SourceFileObject'
| property 'statements' -> object with constructor 'Array'
| index 0 -> object with constructor 'NodeObject'
--- property 'parent' closes the circle
at JSON.stringify ()
at run (file:///C:/code/zywave/custom-element-viewer/node_modules/@custom-elements-manifest/analyzer/index.js:78:83)
at processTicksAndRejections (node:internal/process/task_queues:93:5)
at async file:///C:/code/zywave/custom-element-viewer/node_modules/@custom-elements-manifest/analyzer/index.js:85:5
Expected behavior
I'd expect for the custom element manifest JSON file to generate.
Reproduction
Repro
Expected behavior
Running the analyzer will only write to my package.json
when I want it to.
CLI/config entry options*
So as not to make this change breaking, we likely want a negation style flag here. Some options for review:
cem analyze --no-package-json
cem analyze --no-linking
cem analyze --create-only
cem analyze --packagejson=false // defaulting to true
... more?
... better?
With my naive though that these kabob case flags would move to camel case in a config file.
Thoughts?
Got the tool to work with my project that uses LitElement components. I don't see anything that documents how to generate descriptions though. Can someone point me in the right direction here?
Checklist
--dev
flag to get more information?Bug
I set the output directory for my manifest file to /dist/
, but it is not reflected in package.json
after running cem analyze
script.
Expected behavior
I expect that if I set --outdir
to dist
in the script, --outdir
will be reflected in package.json
:
Command:
cem analyze --globs src/foo-*.ts --litelement --outdir dist
package.json
:
{
"name": "foo-button",
"customElements": "dist/custom-elements.json"
}
comment-parser
seems to break in the browser. Need to figure out why.
Only used in handlers.js
(handleAttrJsDoc()
) and class-jsdoc.js
.
Hey looks good, liking the vscode colours you've got going too.
I ran the analyser tests on windows and they were failing due to path issues:
globby
wants forward slashes so added an explicitly posix version of the path
const packagePath = path.join(fixturesDir, `${testCase}/package`);
const packagePathPosix = packagePath.split(path.sep).join(path.posix.sep);
//...
const globs = await globby(packagePathPosix);
changed relativeModulePath to use the current separator instead of /
:
const relativeModulePath = `.${path.sep}${path.relative(process.cwd(), glob)}`;
Importing the manifest using a local file url instead of an absolute path (absolute import paths not working on windows):
const manifestPathFileURL = pathToFileURL(`${packagePath}/custom-elements-manifest.config.js`).href;
try {
const config = await import(manifestPathFileURL);
plugins = [...config.default.plugins];
} catch {}
I've also added a .gitattributes
file to the analyser folder to tell git on windows to always check out the fixtures output.json files with LF endings (was originally checking out those files as CLRF then after running tests they changed to LF)
# Auto detect text files and perform LF normalization
* text=auto
# Declare files that will always have LF line endings on checkout.
/fixtures/**/output.json text eol=lf
All tests pass for me on windows and mac after these changes, will make a pull request shortly.
Hi! ๐
Firstly, thanks for your work on this project! ๐
Today I used patch-package to patch @custom-elements-manifest/[email protected]
for the project I'm working on.
Here is the diff that solved my problem:
diff --git a/node_modules/@custom-elements-manifest/analyzer/src/features/analyse-phase/class-jsdoc.js b/node_modules/@custom-elements-manifest/analyzer/src/features/analyse-phase/class-jsdoc.js
index 8f67cbc..8d564d1 100644
--- a/node_modules/@custom-elements-manifest/analyzer/src/features/analyse-phase/class-jsdoc.js
+++ b/node_modules/@custom-elements-manifest/analyzer/src/features/analyse-phase/class-jsdoc.js
@@ -1,4 +1,4 @@
-import parse from 'comment-parser';
+import { parse } from 'comment-parser';
import { handleJsDocType, normalizeDescription } from '../../utils/jsdoc.js';
import { safe } from '../../utils/index.js';
@@ -25,7 +25,7 @@ export function classJsDocPlugin() {
* Checks to see if the item is already in the classDoc, and if so merge and overwrite (JSDoc takes precedence)
*/
node?.jsDoc?.forEach(jsDoc => {
- const parsed = parse.parse(jsDoc?.getFullText());
+ const parsed = parse(jsDoc?.getFullText());
parsed?.forEach(parsedJsDoc => {
/**
diff --git a/node_modules/@custom-elements-manifest/analyzer/src/features/analyse-phase/creators/handlers.js b/node_modules/@custom-elements-manifest/analyzer/src/features/analyse-phase/creators/handlers.js
index 21ca2ca..99d220b 100644
--- a/node_modules/@custom-elements-manifest/analyzer/src/features/analyse-phase/creators/handlers.js
+++ b/node_modules/@custom-elements-manifest/analyzer/src/features/analyse-phase/creators/handlers.js
@@ -1,9 +1,9 @@
import ts from 'typescript';
-import parse from 'comment-parser';
+import { parse } from 'comment-parser';
import { has, resolveModuleOrPackageSpecifier, safe } from '../../../utils/index.js';
import { handleJsDocType, normalizeDescription } from '../../../utils/jsdoc.js';
-import { isPrimitive, isWellKnownType } from '../../../utils/ast-helpers.js';
+import { isWellKnownType } from '../../../utils/ast-helpers.js';
/**
* @example static foo;
@@ -197,7 +197,7 @@ export function handleHeritage(classTemplate, moduleDoc, context, node) {
*/
export function handleAttrJsDoc(node, doc) {
node?.jsDoc?.forEach(jsDoc => {
- const docs = parse.parse(jsDoc?.getFullText())?.find(doc => doc?.tags?.some(({tag}) => tag === 'attr'));
+ const docs = parse(jsDoc?.getFullText())?.find(doc => doc?.tags?.some(({tag}) => tag === 'attr'));
const attrTag = docs?.tags?.find(({tag}) => tag === 'attr');
if(attrTag?.name) {
This issue body was partially generated by patch-package.
Checklist
--dev
flag to get more information?Completing the items above will greatly improve triaging time of your issue.
Expected behavior
When using the ?
operator in TypeScript to indicate an optional property, I would expect the type to be propType | undefined
. In this playground example I would expect the type for foo
to be string | undefined
instead of string
.
Checklist
--dev
flag to get more information?Completing the items above will greatly improve triaging time of your issue.
Expected behavior
Properties of a lit component that get initialized from an imported module (here's an example) do not have a type or default value defined in the manifest (in this case it would be string
and 3.5rem
). This was working correctly with web-component-analyzer
.
import { DesignSystem } from "@microsoft/fast-foundation"
import { fastAnchor } from '@microsoft/fast-components';
DesignSystem.getOrCreate().register(
fastAnchor()
);
import { DesignSystem } from "@microsoft/fast-foundation"
import { allComponents } from '@microsoft/fast-components';
DesignSystem.getOrCreate().register(
Object.values(allComponents).map(definition => definition())
);
export const fastAnchor = Anchor.compose({
baseName: "anchor",
template,
styles,
shadowOptions: {
delegatesFocus: true,
},
});
DesignSystem.getOrCreate().register(fastAnchor({prefix: "fluent"}))
defines the anchor as<fluent-anchor></fluent-anchor>
Checklist
--dev
flag to get more information?Completing the items above will greatly improve triaging time of your issue.
Expected behavior
When importing a module from another file to initialize a property, the resulting resolveInitializer
field with module
path does not match the path of the actual module resulting in the property's default
value not getting extracted from the module. Here is a simple repo reproducing the issue. The prop here is initialized to a value from an imported module. In the generated manifest, the property's default
field is set to ColorRed
instead of the expected '#FF0000'
. This is because module
in resolveInitializer
found here is set to /src/tokens/colors
while the module's path in the manifest is equal to src/tokens/colors.ts
found here and so the expected default value is not extracted.
Checklist
--dev
flag to get more information?Completing the items above will greatly improve triaging time of your issue.
Expected behavior
If a subclass initializes its own value for an inherited field I would expect that value to be shown as the default
in the schema. Here is a reproduction of the issue. I would expect default
for foo
under MySecondElement
to be "bye" instead of "hello."
Goal: Plug in to analyzer's module resolution so I can resolve across monorepo packages
Relevant Code:
custom-elements-manifest/packages/analyzer/src/utils/index.js
Lines 18 to 34 in d8bb5a6
Given input like:
// @apollo-elements/core/index.js
export { ApolloQueryController } from './apollo-query-controller.js';
// @apollo-elements/fast/apollo-query-behavior.js
import { ApolloQueryController } from '@apollo-elements/core/apollo-query-controller';
export class ApolloQueryBehavior extends ApolloQueryController {...}
Analyser will identify '@apollo-elements/core/apollo-query-controller';
as a bare specifier and quit.
The final manifest for @apollo-elements/fast
will contain the members of ApolloQueryBehavior
, but not the members of its ancestor ApolloQueryController
.
I'd like to write a small plugin to hook into the module resolution so that I can point local monorepo imports to their relevant source files.
I think I could write a large plugin to collate manifest members across packages in packageLinkPhase
, but it would be nicer to just use the core's code to do that stuff rather than replicate what analyzer does anyways.
I have a feature request for type generation for events.
I would like to write the following code:
/**
* Some extra info
*/
this.dispatchEvent(new CustomEvent<{ bar: string }>('foo', {
detail: {
bar: 'baz'
}
}));
Would it be possible to infer the type of the event foo
to the type argument { bar: 'baz' }
?
This would save me from duplicating the information to a comment, which can get out of sync with the code. This also allows TypeScript to check if my event detail has the correct type.
What I have to write right now:
/**
* Some extra info
*
* @type {{ bar: string }}
*/
this.dispatchEvent(new CustomEvent('foo', {
detail: {
bar: 'baz'
}
}));
The simular package web-component-analyzer does this (there are other reasons why I am not using that package). See the example of event typing
my deps
node_modules/base/MyBase.js
node_modules/base/package.json
node_modules/base/custom-elements-manifst.json
๐ node_modules/base/MyBase.js
import { LitElement } from 'lit-element';
export class MyElement extends LitElement {
static get properties() {
return {
awesome: { type: Boolean }
}
}
constructor() {
super();
this.awesome = true;
}
}
๐ node_modules/base/custom-elements-manifst.json
// ...
"members": [
{
"kind": "field",
"name": "awesome",
"type": {
"text": "boolean"
},
"default": "true",
"privacy": "public",
"attribute": "awesome"
}
],
// ...
my element
class MyEl extends MyBase {
static get properties() {
return {
rainbow: { type: Boolean }
}
}
constructor() {
super();
this.rainbow = true;
}
}
Generated CEM or docs for MyEl lists only rainbow
as a member but not awesome
.
// ...
"members": [
{
"kind": "field",
"name": "rainbow",
"type": {
"text": "boolean"
},
"default": "true",
"privacy": "public",
"attribute": "rainbow"
}
],
// ...
Generated CEM or docs for MyEl should list rainbow
and awesome
as a member.
// ...
"members": [
{
"kind": "field",
"name": "rainbow",
"type": {
"text": "boolean"
},
"default": "true",
"privacy": "public",
"attribute": "rainbow"
},
{
"kind": "field",
"name": "awesome",
"type": {
"text": "boolean"
},
"default": "true",
"privacy": "public",
"attribute": "awesome",
"inheritedFrom": {
"name": "MyBase",
"module": "base/MyBase.js"
}
}
],
// ...
Hey there @thepassle! This tool is looking amazing for CEM. Is there anything I can do to help improve the Stencil compatibility, either within this code or within Stencil?
I can't quite tell if analyzer code is parsing TypeScript or if you're parsing final compiled files. We do have an output target format that doesn't include the lazy loading which splits definitions out into their own files - dist-custom-elements
. Would targeting that help?
I'm not sure it this is intentional or not, but inherited events don't have an inherited type like methods and fields - it would be useful for events to also have the type.
Also, CSS properties aren't inherited - not sure if this is intentional either.
I'm not sure if this is intentional or not, but given this example:
that has the following attributes defined in the comments:
* @attr {string} label - Label of the button
* @attr {boolean} primary - primary
* @attr {string} size - Size of the button, can be "small", "medium" or "large"; default is "medium".
* @attr {string} background-color - Color of the button's background
The custom elements analyzer spits out:
"attributes": [
{
"type": {
"text": "string"
},
"description": "Label of the button",
"name": "label"
},
{
"type": {
"text": "boolean"
},
"description": "primary",
"name": "primary"
},
{
"type": {
"text": "string"
},
"description": "Size of the button, can be \"small\", \"medium\" or \"large\"; default is \"medium\".",
"name": "size"
},
{
"type": {
"text": "string"
},
"description": "Color of the button's background",
"name": "background-color"
},
{
"name": "label",
"fieldName": "label"
},
{
"name": "primary",
"fieldName": "primary"
},
{
"name": "size",
"fieldName": "size"
},
{
"name": "background-color",
"fieldName": "backgroundColor"
}
],
This is probably because it parses the jsdoc but still parses the Lit properties?
I would expect it to merge them like:
...
{
"type": {
"text": "string"
},
"description": "Label of the button",
"name": "label",
"fieldName": "label"
},
...
Is this a bug or intentional?
I'm just running the standard npx command with the litelement flag.
Checklist
--dev
flag to get more information?Source
export class A extends HTMLElement {
#private = 'hello';
private priv = 'hello';
}
export class B extends A {
}
Expected behavior
#private
and priv
should not inherit to B, the first because it's a runtime error, the second because it's a TypeScript error.
Actual
{
"kind": "class",
"description": "",
"name": "B",
"superclass": {
"name": "A",
"module": "src/my-element.js"
},
"members": [
{
"kind": "field",
"name": "#private",
"privacy": "private",
"type": {
"text": "string"
},
"default": "'hello'",
"inheritedFrom": {
"name": "A",
"module": "src/my-element.js"
}
},
{
"kind": "field",
"name": "priv",
"type": {
"text": "string"
},
"privacy": "private",
"default": "'hello'",
"inheritedFrom": {
"name": "A",
"module": "src/my-element.js"
}
}
]
}
@reflect
on classfields for this
See:
the reason is that we only check for @property
decorators inside ClassDeclarations
:
Hi! ๐
Firstly, thanks for your work on this project! ๐
Today I used patch-package to patch @custom-elements-manifest/[email protected]
for the project I'm working on.
class A {
static a: A;
a: A;
}
expected:
"members": [
{
"kind": "field",
"name": "a",
"type": {
"text": "A"
},
"static": true
},
{
"kind": "field",
"name": "a",
"type": {
"text": "A"
}
}
],
actual:
"members": [
{
"kind": "field",
"name": "a",
"type": {
"text": "A"
},
"static": true
}
],
Here is the diff that solved my problem:
diff --git a/node_modules/@custom-elements-manifest/analyzer/src/features/analyse-phase/creators/createClass.js b/node_modules/@custom-elements-manifest/analyzer/src/features/analyse-phase/creators/createClass.js
index 0d60361..8c76e1b 100644
--- a/node_modules/@custom-elements-manifest/analyzer/src/features/analyse-phase/creators/createClass.js
+++ b/node_modules/@custom-elements-manifest/analyzer/src/features/analyse-phase/creators/createClass.js
@@ -4,7 +4,7 @@ import { createAttribute, createAttributeFromField } from './createAttribute.js'
import { createField } from './createClassField.js';
import { handleHeritage, handleJsDoc, handleAttrJsDoc } from './handlers.js';
import { hasDefaultModifier } from '../../../utils/exports.js';
-import { isProperty, isDispatchEvent, hasAttrAnnotation, isReturnStatement, isPrimitive } from '../../../utils/ast-helpers.js';
+import { isProperty, isStaticMember, isDispatchEvent, hasAttrAnnotation, isReturnStatement, isPrimitive } from '../../../utils/ast-helpers.js';
/**
@@ -26,6 +26,7 @@ export function createClass(node, moduleDoc) {
};
node?.members?.forEach(member => {
+
/**
* Handle attributes
*/
@@ -79,13 +80,16 @@ export function createClass(node, moduleDoc) {
* Handle fields
*/
if (isProperty(member)) {
- if (gettersAndSetters.includes(member?.name?.getText())) {
- return;
- } else {
- gettersAndSetters.push(member?.name?.getText());
+ if (!isStaticMember(member)) {
+ if (gettersAndSetters.includes(member?.name?.getText())) {
+ return;
+ } else {
+ gettersAndSetters.push(member?.name?.getText());
+ }
}
const field = createField(member);
+
classTemplate.members.push(field);
/**
diff --git a/node_modules/@custom-elements-manifest/analyzer/src/utils/ast-helpers.js b/node_modules/@custom-elements-manifest/analyzer/src/utils/ast-helpers.js
index df87c86..73cb792 100644
--- a/node_modules/@custom-elements-manifest/analyzer/src/utils/ast-helpers.js
+++ b/node_modules/@custom-elements-manifest/analyzer/src/utils/ast-helpers.js
@@ -85,3 +85,6 @@ export function getElementNameFromDecorator(decorator) {
* @example @attr({attribute: 'my-el'})
*/
export const getOptionsObject = decorator => decorator?.expression?.arguments?.find(arg => arg.kind === ts.SyntaxKind.ObjectLiteralExpression);
+
+export const isStaticMember = member =>
+ member.modifiers?.some?.(x => x.kind === ts.SyntaxKind.StaticKeyword);
diff --git a/node_modules/@custom-elements-manifest/analyzer/src/utils/mixins.js b/node_modules/@custom-elements-manifest/analyzer/src/utils/mixins.js
index 2361e7d..d12375a 100644
--- a/node_modules/@custom-elements-manifest/analyzer/src/utils/mixins.js
+++ b/node_modules/@custom-elements-manifest/analyzer/src/utils/mixins.js
@@ -62,15 +62,20 @@ export function extractMixinNodes(node) {
const classDeclaration = node.body.statements.find(statement => ts.isClassDeclaration(statement));
const returnStatement = node.body.statements.find(statement => ts.isReturnStatement(statement));
+ /** Avoid undefined === undefined */
+ if(!(classDeclaration && returnStatement))
+ return;
+
+ const classDeclarationName = classDeclaration.name?.getText?.();
+ const returnValue =
+ returnStatement.expression?.kind === ts.SyntaxKind.AsExpression ? returnStatement.expression.expression.getText()
+ : returnStatement.expression?.getText();
+
/**
* If the classDeclaration inside the function body has the same name as whats being
* returned from the function, consider it a mixin
*/
- if(
- /** Avoid undefined === undefined */
- (classDeclaration && returnStatement) &&
- (classDeclaration?.name?.getText() === returnStatement?.expression?.getText())
- ) {
+ if (classDeclarationName === returnValue) {
return {
mixinFunction: node,
mixinClass: classDeclaration
This issue body was partially generated by patch-package.
https://jsdoc.app/tags-ignore.html
The @ignore tag indicates that a symbol in your code should never appear in the documentation. This tag takes precedence over all others.
Given this source:
const priv = Symbol();
export class MyEl extends HTMLElement {
f() { return this[priv](); }
/** @ignore */
[priv]() { return 'Hi, Pascal!'; }
}
I expect this output:
{
"schemaVersion": "0.1.0",
"readme": "",
"modules": [
{
"kind": "javascript-module",
"path": "src/my-element.js",
"declarations": [
{
"kind": "class",
"description": "",
"name": "MyEl",
"members": [
{
"kind": "method",
"name": "f"
}
],
"superclass": {
"name": "HTMLElement"
},
"customElement": true
}
],
"exports": [
{
"kind": "js",
"name": "MyEl",
"declaration": {
"name": "MyEl",
"module": "src/my-element.js"
}
}
]
}
]
}
instead actual result is:
{
"schemaVersion": "0.1.0",
"readme": "",
"modules": [
{
"kind": "javascript-module",
"path": "src/my-element.js",
"declarations": [
{
"kind": "class",
"description": "",
"name": "MyEl",
"members": [
{
"kind": "method",
"name": "f"
},
{
"kind": "method",
"name": "[priv]"
}
],
"superclass": {
"name": "HTMLElement"
},
"customElement": true
}
],
"exports": [
{
"kind": "js",
"name": "MyEl",
"declaration": {
"name": "MyEl",
"module": "src/my-element.js"
}
}
]
}
]
}
Playground:
Doesn't work with refactored code:
customElements.define('my-element', class extends HTMLElement {
static get observedAttributes() {
return ['disabled'];
}
set disabled(val) {
this.__disabled = val;
}
get disabled() {
return this.__disabled;
}
fire() {
this.dispatchEvent(new Event('disabled-changed'));
}
}
);
Checklist
--dev
flag to get more information?Completing the items above will greatly improve triaging time of your issue.
Expected behavior
If a subclass initializes its own value for an inherited field I would expect that field to include the type value that it inherited. Here is a reproduction of the issue. I would expect type
for foo under members
for MySecondElement to be Greeting
instead of string
. It is shown correctly under attributes
, though the default
is incorrect and shows "hello" instead of the expected value "bye" (as seen under members).
Now that class fields are implemented in Chrome (and other browsers), combined with the latest Typescript compiler that can compile to this standard, I recently discovered something that caused me to reevaluate how I define vanilla web components.
If I define a custom element like this:
export class MyCustomElement extends HTMLElement{
myField;
}
customElements.define('my-custom-element', MyCustomElement);
This allowed Typescript consumers of the class to recognize that there is a field/property called myField, with minimal fuss, and also the custom elements manifest analyzer would add it to custom-elements.json. The problem is, during runtime, if the property value of myField is passed to an my-custom-element instance before the custom element class is registered, so that at that point it is an unknown element, the value passed gets lost when the class is registered. That non-assigned field, placed there to get Typescript support, as well as automatic inclusion in custom-elements.json, is causing asynchronous loading to stop working properly.
So the work around I've found, with Typescript, is to define an interface in a separate types.d.ts file (condensed here for simplicty):
export class MyCustomElement extends HTMLElement{
}
customElements.define('my-custom-element', MyCustomElement);
export interface MyCustomElementProps{
/**
* @prop {string} [myField=someDefaultValue]
*/
myField: string;
}
export interface MyCustomElement extends MyCustomElementProps {}
Now, thanks to that strange last line, I get the compile time / autocomplete help I was getting before (with the simpler class field) from TypeScript, but without the damage to asynchronous loading. But the custom-element analyzer doesn't seem to pick up the associated property/field information from the extended interface.
Not sure if that's been looked at and not something the Typescript AST libraries pick up, so it is really difficult to implement? Or does any plugin pick it up already?
Hi all!
I'm trying to use the to-markdown
package in nodejs as commonjs. and getting the following stack trace:
$ node ./tasks/build-readmes.js
internal/modules/cjs/loader.js:892
throw err;
^
Error: Cannot find module './lib/fp.js'
Require stack:
- /Users/<ME>/Documents/Repos/<REPO>/node_modules/@custom-elements-manifest/to-markdown/dist/index.cjs
- /Users/<ME>/Documents/Repos/<REPO>/tasks/build-readmes.js
at Function.Module._resolveFilename (internal/modules/cjs/loader.js:889:15)
at Function.Module._load (internal/modules/cjs/loader.js:745:27)
at Module.require (internal/modules/cjs/loader.js:961:19)
at require (internal/modules/cjs/helpers.js:92:18)
at Object.<anonymous> (/Users/<ME>/Documents/Repos/<REPO>/node_modules/@custom-elements-manifest/to-markdown/dist/index.cjs:28:28)
at Module._compile (internal/modules/cjs/loader.js:1072:14)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1101:10)
at Module.load (internal/modules/cjs/loader.js:937:32)
at Function.Module._load (internal/modules/cjs/loader.js:778:12)
at Module.require (internal/modules/cjs/loader.js:961:19) {
code: 'MODULE_NOT_FOUND',
requireStack: [
'/Users/<ME>/Documents/Repos/<REPO>/node_modules/@custom-elements-manifest/to-markdown/dist/index.cjs',
'/Users/<ME>/Documents/Repos/<REPO>/tasks/build-readmes.js'
]
}
I basically just copied the import from the docs and switched it to commonjs/require notation so im importing like:
const { customElementsManifestToMarkdown } = require('@custom-elements-manifest/to-markdown');
and using like:
const manifest = JSON.parse(fs.readFileSync(ceJsonLocation, 'utf-8'));
const markdown = customElementsManifestToMarkdown(manifest);
I checked the exports in package.json and its correctly pointing at dist/index.cjs
, but the path in that file to fp.js
is pointing at ./lib/fp.js
but since the index.cjs
is in dist
, i think that path should be ../lib/fp.js
maybe?
Checklist
--dev
flag to get more information?Completing the items above will greatly improve triaging time of your issue.
Expected behavior
If a subclass initializes its own value for an inherited field I would expect that field to include the privacy
value that it inherited. Here is a reproduction of the issue. I would expect privacy
for foo under MySecondElement to be public
just as bar (which is also inherited and doesn't get overridden) has privacy set.
As seen in the playground above. If the element is default exported, a crash occurs. If you remove the default export, it goes through smoothly.
It seems to crash because there is no conditional chaining for some of the static property mappings in the lit parsers
The following code:
/**
* @prop {boolean} primary - Set button in primary mode
*/
class MyEl extends LitElement {
static get properties() {
return {
primary: {type: Boolean}
}
}
}
customElements.define('my-el', MyEl)
Results in:
{
"kind": "class",
"description": "",
"name": "MyEl",
"members": [
{
"type": {
"text": "boolean"
},
"description": "- Set button in primary mode",
"name": "primary",
"kind": "field"
},
{
"kind": "field",
"name": "primary",
"privacy": "public"
}
],
"attributes": [
{
"name": "primary",
"fieldName": "primary"
}
],
"superclass": {
"name": "LitElement"
},
"tagName": "my-el",
"customElement": true
}
The two properties in the CEM should be merged instead
EDIT: This is not limited to lit, but also regular class fields
Hey team ๐,
I'm still using the _
prefix convention for private properties and methods. I know #
is coming but I don't want to transpile and while I wait for better support, I'm OK with the _
prefix convention.
WCA recognize _
as a private prefix and it also recognize #
as a private prefix I think.
If I use those _
prefix with CEM analyzer:
_
or #
have a "privacy": "public"
._
or #
don't have a privacy
field./** @private */
on methods, we get a "privacy": "private"
./** @private */
on properties, it doesn't change anything.Examples with /** @private */
:
_
and #
to be handled as private.What do you think about those?
Checklist
--dev
flag to get more information?In the playground (or command line), use this custom element:
class MyElement extends HTMLElement {}
customElements.define("my-element", MyElement)
You'll get this JSON:
{
"schemaVersion": "1.0.0",
"readme": "",
"modules": [
{
"kind": "javascript-module",
"path": "src/my-element.js",
"declarations": [
{
"kind": "class",
"description": "",
"name": "MyElement",
"superclass": {
"name": "HTMLElement"
},
"tagName": "my-element",
"customElement": true
}
],
"exports": [
{
"kind": "custom-element-definition",
"name": "my-element",
"declaration": {
"name": "MyElement",
"module": "src/my-element.js"
}
}
]
}
]
}
If you compare this to the schema, it doesn't match because of tagName
on the ClassDeclaration
.
You can repro using a simple TypeScript file:
import { Package } from 'custom-elements-manifest/schema'
const json: Package = {
"schemaVersion": "1.0.0",
"readme": "",
"modules": [
{
"kind": "javascript-module",
"path": "index.js",
"declarations": [
{
"kind": "class",
"description": "",
"name": "MyElement",
"superclass": {
"name": "HTMLElement"
},
"tagName": "my-element",
"customElement": true
}
],
"exports": [
{
"kind": "custom-element-definition",
"name": "my-element",
"declaration": {
"name": "MyElement",
"module": "index.js"
}
}
]
}
]
}
Then run:
mkdir test
cd test
npm init --yes
npm i --save [email protected] [email protected]
npx tsc index.ts
You'll see the error:
test.ts:18:11 - error TS2322: Type '{ kind: "class"; description: string; name: string; superclass: { name: string; }; tagName: string; customElement: true; }' is not assignable to type 'Declaration'.
Object literal may only specify known properties, and '"tagName"' does not exist in type 'ClassDeclaration'.
18 "tagName": "my-element",
~~~~~~~~~~~~~~~~~~~~~~~
Found 1 error.
Expected behavior
The output schema should match the one from custom-elements-manifest
.
I'm not sure if this is a problem in the analyzer or in the custom-elements-manifest
schema itself, but I'm just reporting it.
cem --help
works fine
cem analyze --help
doesn't:
(node:33622) UnhandledPromiseRejectionWarning: UNKNOWN_OPTION: Unknown option: --help
at commandLineArgs (/Users/jeroenz/Projects/dna/core/node_modules/command-line-args/dist/index.js:1374:21)
at getCliConfig (file:///Users/jeroenz/Projects/dna/core/node_modules/@custom-elements-manifest/analyzer/src/utils/cli.js:73:10)
Not just --help
, but any property cem doesn't know about, so cem analyze --silent
:
(node:33623) UnhandledPromiseRejectionWarning: UNKNOWN_OPTION: Unknown option: --silent
at commandLineArgs (/Users/jeroenz/Projects/dna/core/node_modules/command-line-args/dist/index.js:1374:21)
at getCliConfig (file:///Users/jeroenz/Projects/dna/core/node_modules/@custom-elements-manifest/analyzer/src/utils/cli.js:73:10)
Hey team ๐,
I'm used to write my JSDoc like this:
@prop {String} foobar - My awesome description
This is how it's documented on JSDoc and how I used it with WCA.
As you can see in this:
CEM generates a JSON with:
"members": [
{
"type": {
"text": "String"
},
"description": "- My awesome description",
"name": "foobar",
"kind": "field",
"privacy": "public"
}
]
I think I expected it not to take the -
.
Checklist
--dev
flag to get more information?Playground with minimal reproduction
An empty return;
statement at the end of a function has no use (or at least I don't know of any), but it also shouldn't break the parsing/analyzing.
Expected behavior
Analyzer should ignore the empty return;
statement.
Hi all-
I took a look through the docs and didnt see anything like this, so I figured I'd request it.
I think it would be really useful to support a option/flag in the to-markdown
package and the CEM plugin for markdown that lets consumers decide which sections to include in the final MD output, similar to the --features
option in the wca analyzer tool so that the final markdown output could be customized according to use case.
My first question is whether or not there's currently a way to do this, as I'm trying to use the generated markdown for technical docs for my design system, but Im not a fan of the file info sections that the markdown lib currently creates. Its making me flip flop about deciding which analyzer/markdown tool to use, this one or wca
. I predict that wca
may end up deprecated as the push for switching to the new manifest.json format is pushed forward, so i'd rather use this one, but I definitely would like to be able to customize the output a bit more
My second question is how similar in implementations are the two analyzers? is it a case of porting the feature over, or is there more to it because the implementations are drastically different?
I would be down to help if i can, but i'm not that familiar with ASTs and such.
Edit:
Looking through, could it be as simple as how the returned array in makeModuleDoc
gets filled.
https://github.com/open-wc/custom-elements-manifest/blob/master/packages/to-markdown/index.js#L103
Thanks!
This issue tracks some nice-to-have enhancements for type detection.
The analyzer does great picking up on explicitly typed members:
function XMixin<B extends Constructor<{ b: B }>(superclass: B) {
return class XL<A> extends superclass {
x: X<A> = new X<A>(this.b);
bool: boolean = true;
}
}
Here,
B
x
field will be correctly typed as X<A>
bool
field will be typed as boolean
However, it would be nice if for the following code:
function XMixin<B extends Constructor>(superclass: B) {
return class XL<A> extends superclass {
x = new X<A>();
bool = true;
}
}
Would give:
B extends Constructor<{ b: B }>
x
field as X<A>
(even without an explicit typing)bool
field as boolean
(even without an explicit typing)I'm guessing that the TypeScript API has the chops to infer those things, given a SourceFile
AST, but I haven't looked into it just yet.
It would be a nice user experience if, on error, as we get provided the playground link, it would have the parameters so that the code part is pre-filled with the contents of the file that caused the error.
This would make it easier and less cumbersome to submit bug reports.
It would mean that we need to add the source to the withErrorHandling function, and then parse the text
property of the object.
What might make this ticket harder is if all of the use cases can't access the file responsible. But in these cases we could maybe just not generate it?
export default class Foo {}
Should be:
{
"schemaVersion": "1.0.0",
"readme": "",
"modules": [
{
"kind": "javascript-module",
"path": "src/my-element.js",
"declarations": [
{
"kind": "class",
"description": "",
+ "name": "Foo"
}
],
"exports": [
{
"kind": "js",
+ "name": "default",
"declaration": {
+ "name": "Foo",
"module": "src/my-element.js"
}
}
]
}
]
}
Instead of (currently):
{
"schemaVersion": "1.0.0",
"readme": "",
"modules": [
{
"kind": "javascript-module",
"path": "src/my-element.js",
"declarations": [
{
"kind": "class",
"description": "",
+ "name": "default"
}
],
"exports": [
{
"kind": "js",
+ "name": "default",
"declaration": {
+ "name": "default",
"module": "src/my-element.js"
}
}
]
}
]
}
See also: webcomponents/custom-elements-manifest#72
Notes:
_tempName
hack thenChecklist
--dev
flag to get more information?Completing the items above will greatly improve triaging time of your issue.
Expected behavior
The link-class-to-tagname.js
post process should link tagnames if they are not documented.
However theres a bug in the system currently which names the class "default" if it's default exported
e.g.
export default class MyComponent extends HTMLElement {
}
Which therefore breaks the linking process
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.