diff --git a/.gitignore b/.gitignore
index 52551e30..4e4e95e8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,5 @@ node_modules
dist
lib
*.tgz
+yarn.lock
diff --git a/README.md b/README.md
index bd256153..1a9996c9 100644
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@ The analyze command analyses an optional `` and emits the output to
| Option | Type | Description |
| --------------------------- | -------------------------------- | ---------------------------------------------------------------------------- |
-| `--format ` | `markdown` \| `json` \| `vscode` | Specify output format. Default is `markdown`. |
+| `--format ` | `markdown` \| `json` \| `vscode` \| `webtypes` | Specify output format. Default is `markdown`. |
| `--outDir ` | `directory path` | Direct output to a directory where each file corresponds to a web component. |
| `--outFile ` | `file path` | Concatenate and emit output to a single file. |
| `--outFiles ` | `file path with pattern` | Emit output to multiple files using a pattern. Available substitutions:
**{dir}**: The directory of the component
**{filename}**: The filename (without ext) of the component
**{tagname}**: The element's tag name |
@@ -107,6 +107,33 @@ wca analyze src --format vscode --outFile vscode-html-custom-data.json
VSCode supports a JSON format called [vscode custom data](https://github.com/microsoft/vscode-custom-data) for the built in html editor which is set using `html.customData` vscode setting. Web Component Analyzer can output this format.
+### webtypes
+
+
+```bash
+wca analyze src --format webtypes --outFile web-types-custom.json
+```
+
+To configure web-types (name, version, etc.) add this section to your `package.json`. `name` and `version` fields are required if wca is not ran from npm `package.json` `scripts` section.
+Otherwise it will take `package.json` project `name` and `version`.
+
+You can use the `wca-config` section to configure other options as well.
+
+```json
+"wca": {
+ "webtypesConfig": {
+ "name": "your-project-name",
+ "version": "0.0.1",
+ "framework": "lit",
+ "description-markup": "markdown"
+ }
+}
+```
+
+Web-types format is a description for IDE completion, see [web-types](https://github.com/JetBrains/web-types/tree/master/packages)
+
+See [web-component-analyzer web-types dedicated page](./doc/web-types.md) for project setup.
+
[](#how-does-this-tool-analyze-my-components)
[](#how-to-document-your-components-using-jsdoc)
diff --git a/doc/web-types.md b/doc/web-types.md
new file mode 100644
index 00000000..f96d49ed
--- /dev/null
+++ b/doc/web-types.md
@@ -0,0 +1,153 @@
+# Web-types
+
+## web-component-analyzer usage
+
+### With package.json configuration
+
+You can add a `wca` section in your `package.json` to configure wca for web-types build.
+
+```json
+"wca": {
+ "webtypesConfig": {
+ "name": "your-project-name",
+ "version": "0.0.1",
+ "framework": "lit",
+ "description-markup": "markdown"
+ }
+}
+```
+
+To run wca then use command:
+
+```bash
+wca analyze src --format webtypes --outFile web-types-custom.json
+```
+
+If you run `wca` from project npm `scripts` section of `package.json`, you can omit `name` and `version` property
+in `webtypesConfig`, this will take package `name` and `version` by default.
+
+### With command line only
+
+You can also avoid updating `package.json` `wca` section and use only command line by providing a json configuration to `--webtypesConfig` argument:
+
+```bash
+wca analyze src --format webtypes --outFile web-types-custom.json --webtypesConfig='{"name": "your-project-name", "version": "0.0.1", "framework": "lit", "description-markup": "markdown"}'
+```
+
+If you run `wca` from project npm `scripts` section of `package.json`, you can omit `name` and `version` property
+in `webtypesConfig`, this will take package `name` and `version` by default.
+
+## Project setup
+
+### For Lit
+
+Generated web-types are working with the generic `@web-types/lit` library. You need to add it on your project.
+
+
+```bash
+npm i @web-types/lit -D
+```
+
+Add `wca` section in your `package.json`:
+
+```json
+"wca": {
+ "webtypesConfig": {
+ "framework": "lit",
+ "description-markup": "markdown"
+ }
+}
+```
+
+Working with lit, `framework` must have `lit` value. See [Web types config parameters](#web-types-config-parameters) documentation section for more info.
+
+Add `scripts` section in your `package.json`:
+
+```json
+"scripts": {
+ "web-types": "wca analyze src --format webtypes --outFile web-types.json"
+}
+```
+
+Generate your components descriptions with wca
+
+
+```bash
+npm run web-types
+```
+
+If your web-types is not named `web-types.json` and placed at root of your project, you need to declare it in your `package.json`
+
+```json
+"web-types": [
+ "..../web-types-custom.json"
+]
+```
+
+After the first setup on Intellij, IDE restart might be needed to enable components completion.
+
+### For Polymer
+
+Generated web-types are working with the generic `polymer-web-types` library. You need to add it on your project.
+
+
+```bash
+npm i polymer-web-types -D
+```
+
+Add `wca` section in your `package.json`:
+
+```json
+"wca": {
+ "webtypesConfig": {
+ "framework": "@polymer/polymer",
+ "description-markup": "markdown"
+ }
+}
+```
+
+Working with lit, `framework` must have `@polymer/polymer` value. See [Web types config parameters](#web-types-config-parameters) documentation section for more info.
+
+Add `scripts` section in your `package.json`:
+
+```json
+"scripts": {
+ "web-types": "wca analyze src --format webtypes --outFile web-types.json"
+}
+```
+
+Generate your components descriptions with wca
+
+
+```bash
+npm run web-types
+```
+
+If your web-types is not named `web-types.json` and placed at root of your project, you need to declare it in your `package.json`
+
+```json
+"web-types": [
+ "..../web-types-custom.json"
+]
+```
+
+After the first setup on Intellij, IDE restart might be needed to enable components completion.
+
+## Web types config parameters
+
+`webtypesConfig` parameter is a json object containing web-types file root parameter.
+
+Available parameters:
+
+| Name | Description |
+| ------------------ | ------------------------------------------------------------------------------------------------------------- |
+| name | Name of library |
+| version | Version of the library, for which Web-Types are provided |
+| framework | See [http://json.schemastore.org/web-types](http://json.schemastore.org/web-types) framework section |
+| js-types-syntax | See [http://json.schemastore.org/web-types](http://json.schemastore.org/web-types) js-types-syntax section |
+| description-markup | See [http://json.schemastore.org/web-types](http://json.schemastore.org/web-types) description-markup section |
+| framework-config | See [http://json.schemastore.org/web-types](http://json.schemastore.org/web-types) framework-config section |
+| default-icon | See [http://json.schemastore.org/web-types](http://json.schemastore.org/web-types) default-icon section |
+| path-as-absolute | Consider paths as absolute: don't add './' in front of paths |
+
+See [web-types project:](https://github.com/JetBrains/web-types) for more info.
diff --git a/package.json b/package.json
index fab85b93..9cb6f698 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,8 @@
"keywords": [
"web components",
"web",
- "components"
+ "components",
+ "web-types"
],
"contributors": [
{
diff --git a/src/cli/analyze/analyze-cli-command.ts b/src/cli/analyze/analyze-cli-command.ts
index 419861a2..0fdd5e06 100644
--- a/src/cli/analyze/analyze-cli-command.ts
+++ b/src/cli/analyze/analyze-cli-command.ts
@@ -23,7 +23,7 @@ export const analyzeCliCommand: CliCommand = async (config: AnalyzerCliConfig):
const inputGlobs = config.glob || [];
// Log warning for experimental json format
- if (config.format === "json" || config.format === "json2" || config.outFile?.endsWith(".json")) {
+ if (config.format === "json" || config.format === "json2" || (config.outFile?.endsWith(".json") && config.format !== "webtypes")) {
log(
`
!!!!!!!!!!!!! WARNING !!!!!!!!!!!!!
@@ -37,6 +37,43 @@ Please follow and contribute to the discussion at:
);
}
+ if (config.format === "webtypes") {
+ if (!config.webtypesConfig) throw makeCliError("Missing webtypes-config configuration");
+
+ // Allow object being passed as JSON from command line
+ let cleanedConfiguration;
+ if (typeof config.webtypesConfig === "string") {
+ try {
+ cleanedConfiguration = JSON.parse(config.webtypesConfig);
+ } catch (e) {
+ const message = e instanceof Error ? e.message : e;
+ throw makeCliError("webtypes-config JSON format issue: " + message + "\nReceived value: " + config.webtypesConfig);
+ }
+ } else {
+ cleanedConfiguration = config.webtypesConfig;
+ }
+
+ if (!cleanedConfiguration.name) {
+ // Take package name if ran from npm script
+ if (process.env.npm_package_name) {
+ cleanedConfiguration.name = process.env.npm_package_name;
+ } else {
+ throw makeCliError('Missing webtypes-config "name" property');
+ }
+ }
+
+ if (!cleanedConfiguration.version) {
+ // Take package version if ran from npm script
+ if (process.env.npm_package_version) {
+ cleanedConfiguration.version = process.env.npm_package_version;
+ } else {
+ throw makeCliError('Missing webtypes-config "version" property');
+ }
+ }
+
+ config.parsedWebtypesConfig = cleanedConfiguration;
+ }
+
// If no "out" is specified, output to console
const outStrategy: OutStrategy = (() => {
if (config.outDir == null && config.outFile == null && config.outFiles == null) {
@@ -124,6 +161,9 @@ function transformResults(results: AnalyzerResult[] | AnalyzerResult, program: P
markdown: config.markdown,
cwd: config.cwd
};
+ if (format == "webtypes") {
+ transformerConfig.webTypes = config.parsedWebtypesConfig;
+ }
return transformAnalyzerResult(format, results, program, transformerConfig);
}
@@ -227,6 +267,7 @@ function formatToExtension(kind: TransformerKind): string {
switch (kind) {
case "json":
case "vscode":
+ case "webtypes":
return ".json";
case "md":
case "markdown":
diff --git a/src/cli/analyzer-cli-config.ts b/src/cli/analyzer-cli-config.ts
index 9b8012da..f0a2770d 100644
--- a/src/cli/analyzer-cli-config.ts
+++ b/src/cli/analyzer-cli-config.ts
@@ -2,6 +2,7 @@ import * as tsModule from "typescript";
import { ComponentFeature } from "../analyze/types/features/component-feature";
import { VisibilityKind } from "../analyze/types/visibility-kind";
import { TransformerKind } from "../transformers/transformer-kind";
+import { WebTypesTransformerConfig } from "../transformers";
export interface AnalyzerCliConfig {
glob?: string[];
@@ -30,4 +31,7 @@ export interface AnalyzerCliConfig {
ts?: typeof tsModule;
cwd?: string;
+
+ webtypesConfig?: string | WebTypesTransformerConfig;
+ parsedWebtypesConfig?: WebTypesTransformerConfig;
}
diff --git a/src/cli/cli.ts b/src/cli/cli.ts
index 97719d65..ebf8fe8f 100644
--- a/src/cli/cli.ts
+++ b/src/cli/cli.ts
@@ -9,6 +9,7 @@ import { log } from "./util/log";
*/
export function cli(): void {
const argv = yargs
+ .pkgConf("wca")
.usage("Usage: $0 [glob..] [options]")
.command({
command: ["analyze [glob..]", "$0"],
@@ -50,7 +51,7 @@ o {tagname}: The element's tag name`,
})
.option("format", {
describe: `Specify output format`,
- choices: ["md", "markdown", "json", "json2", "vscode"],
+ choices: ["md", "markdown", "json", "json2", "vscode", "webtypes"],
nargs: 1,
alias: "f"
})
@@ -103,6 +104,10 @@ o {tagname}: The element's tag name`,
string: true,
hidden: true
})
+ .option("webtypesConfig", {
+ describe: "WebTypes header configuration",
+ string: true
+ })
.alias("v", "version")
.help("h")
diff --git a/src/transformers/transform-analyzer-result.ts b/src/transformers/transform-analyzer-result.ts
index d2b06b9d..c5a13c91 100644
--- a/src/transformers/transform-analyzer-result.ts
+++ b/src/transformers/transform-analyzer-result.ts
@@ -8,6 +8,7 @@ import { TransformerConfig } from "./transformer-config";
import { TransformerFunction } from "./transformer-function";
import { TransformerKind } from "./transformer-kind";
import { vscodeTransformer } from "./vscode/vscode-transformer";
+import { webtypesTransformer } from "./webtypes/webtypes-transformer";
const transformerFunctionMap: Record = {
debug: debugJsonTransformer,
@@ -15,7 +16,8 @@ const transformerFunctionMap: Record = {
json2: json2Transformer,
markdown: markdownTransformer,
md: markdownTransformer,
- vscode: vscodeTransformer
+ vscode: vscodeTransformer,
+ webtypes: webtypesTransformer
};
/**
diff --git a/src/transformers/transformer-config.ts b/src/transformers/transformer-config.ts
index 00c67e9b..caa23fe4 100644
--- a/src/transformers/transformer-config.ts
+++ b/src/transformers/transformer-config.ts
@@ -1,4 +1,5 @@
import { VisibilityKind } from "../analyze/types/visibility-kind";
+import { GenericContributions } from "./webtypes/webtypes-schema";
export interface TransformerConfig {
cwd?: string;
@@ -8,4 +9,20 @@ export interface TransformerConfig {
headerLevel?: number;
};
inlineTypes?: boolean;
+ webTypes?: WebTypesTransformerConfig;
+}
+
+export interface WebTypesTransformerConfig {
+ name: string;
+ version: string;
+ "default-icon"?: string;
+ "js-types-syntax"?: "typescript";
+ framework?: string;
+ "framework-config"?: WebTypesFrameworkConfig;
+ "description-markup"?: "html" | "markdown" | "none";
+ pathAsAbsolute?: boolean;
+}
+
+export interface WebTypesFrameworkConfig {
+ [k: string]: GenericContributions;
}
diff --git a/src/transformers/transformer-kind.ts b/src/transformers/transformer-kind.ts
index c87a2f3a..22e5a190 100644
--- a/src/transformers/transformer-kind.ts
+++ b/src/transformers/transformer-kind.ts
@@ -1 +1 @@
-export type TransformerKind = "md" | "markdown" | "json" | "vscode" | "debug" | "json2";
+export type TransformerKind = "md" | "markdown" | "json" | "vscode" | "debug" | "json2" | "webtypes";
diff --git a/src/transformers/webtypes/webtypes-schema.ts b/src/transformers/webtypes/webtypes-schema.ts
new file mode 100644
index 00000000..3e8adc18
--- /dev/null
+++ b/src/transformers/webtypes/webtypes-schema.ts
@@ -0,0 +1,224 @@
+// converted from JSON schema with https://transform.tools/json-schema-to-typescript
+// just renamed JSONSchemaForWebTypesPreviewOfVersion20OfTheStandard to WebtypesSchema
+
+/**
+ * Language in which JavaScript objects types are specified.
+ */
+export type JsTypesSyntax = "typescript";
+/**
+ * Markup language in which descriptions are formatted
+ */
+export type DescriptionMarkup = "html" | "markdown" | "none";
+/**
+ * A RegEx pattern to match whole content. Syntax should work with at least ECMA, Java and Python implementations.
+ */
+export type Pattern =
+ | string
+ | {
+ regex?: string;
+ "case-sensitive"?: boolean;
+ [k: string]: unknown;
+ };
+export type NameConverter = "as-is" | "PascalCase" | "camelCase" | "lowercase" | "UPPERCASE" | "kebab-case" | "snake_case";
+export type NameConverters = NameConverter[];
+
+export type Priority = "lowest" | "low" | "normal" | "high" | "highest";
+/**
+ * Relative path to icon
+ */
+export type Icon = string;
+export type GenericContributions = GenericContributionOrProperty[] | GenericContributionOrProperty;
+export type GenericContributionOrProperty = string | number | boolean | object | GenericContribution | Source;
+export type GenericContribution = TypedContribution;
+
+export interface TypedContribution extends BaseContribution {
+ type?: Type | Type[];
+}
+
+export interface WebtypesSchema {
+ $schema?: string;
+ /**
+ * Framework, for which the components are provided by the library
+ */
+ framework?: string;
+ /**
+ * Name of the library
+ */
+ name: string;
+ /**
+ * Version of the library, for which web-types are provided
+ */
+ version: string;
+ "js-types-syntax"?: JsTypesSyntax;
+ "description-markup"?: DescriptionMarkup;
+ "framework-config"?: FrameworkConfig;
+ "default-icon"?: Icon;
+ contributions?: {
+ html?: Html;
+ css?: Css;
+ js?: Js;
+ };
+}
+export interface FrameworkConfig {
+ /**
+ * Specify rules for enabling web framework support.
+ */
+ "enable-when"?: {
+ /**
+ * Node.js package names, which enable framework support within the folder containing the package.json.
+ */
+ "node-packages"?: string[];
+ /**
+ * RegExps to match script URLs, which enable framework support within a particular HTML.
+ */
+ "script-url-patterns"?: Pattern[];
+ /**
+ * Extensions of files, which should have the framework support enabled
+ */
+ "file-extensions"?: string[];
+ /**
+ * RegExp patterns to match file names, which should have the framework support enabled
+ */
+ "file-name-patterns"?: Pattern[];
+ /**
+ * Global JavaScript libraries names enabled within the IDE, which enable framework support in the whole project
+ */
+ "ide-libraries"?: string[];
+ };
+ /**
+ * Specify rules for disabling web framework support. These rules take precedence over enable-when rules.
+ */
+ "disable-when"?: {
+ /**
+ * Extensions of files, which should have the framework support disabled
+ */
+ "file-extensions"?: string[];
+ /**
+ * RegExp patterns to match file names, which should have the framework support disabled
+ */
+ "file-name-patterns"?: Pattern[];
+ };
+ "canonical-names"?: {
+ [k: string]: NameConverter;
+ };
+ "name-variants"?: {
+ [k: string]: NameConverters;
+ };
+}
+
+export interface SourceFile {
+ file: string;
+ offset: number;
+}
+
+export interface SourceModule {
+ module: string;
+ symbol: string;
+}
+
+export type Source = SourceFile | SourceModule;
+
+export type Html = HtmlContributionHost;
+
+export interface HtmlElement extends BaseContribution, HtmlContributionHost {}
+
+export interface BaseContribution {
+ name?: string;
+ description?: string;
+ // "description-sections"?: ;
+ "doc-url"?: string;
+ icon?: Icon;
+ source?: Source;
+ deprecated?: boolean;
+ experimental?: boolean;
+ priority?: Priority;
+ proximity?: number;
+ virtual?: boolean;
+ abstract?: boolean;
+ extension?: boolean;
+ extends?: Reference;
+ // pattern?: NamePatternRoot;
+ html?: Html;
+ css?: Css;
+ js?: Js;
+ // "exclusive-contributions"?: ;
+}
+
+export interface HtmlContributionHost {
+ elements?: HtmlElement[];
+ attributes?: HtmlAttribute[];
+ slots?: SlotAttribute[];
+}
+
+export interface HtmlAttribute extends BaseContribution, HtmlContributionHost {
+ value?: HtmlAttributeValue;
+ default?: string;
+ required?: boolean;
+}
+
+export interface HtmlAttributeValue {
+ type?: Type | Type[];
+ required?: boolean;
+ default?: string;
+ kind?: HtmlAttributeType;
+}
+
+export interface SlotAttribute extends BaseContribution {}
+
+export interface CssPartAttribute extends BaseContribution {}
+
+export interface TypeReference {
+ module?: string;
+ name: string;
+}
+
+export type Type = string | TypeReference;
+
+export type HtmlAttributeType = "no-value" | "plain" | "expression";
+
+export type Reference = string | ReferenceWithProps;
+
+export interface ReferenceWithProps {
+ path: string;
+ includeVirtual?: boolean;
+ includeAbstract?: boolean;
+ filter?: string;
+}
+
+export type Js = JsContributionsHost;
+
+export interface JsContributionsHost {
+ events?: GenericJsContribution[];
+ properties?: GenericJsContribution[];
+}
+
+export interface GenericJsContribution extends GenericContribution, JsContributionsHost {
+ value?: HtmlAttributeValue;
+ default?: string;
+ required?: boolean;
+}
+
+export type Css = CssContributionsHost;
+
+export interface CssContributionsHost {
+ properties?: CssProperty[];
+ "pseudo-elements"?: CssPseudoElement[];
+ "pseudo-classes"?: CssPseudoClass[];
+ functions?: CssGenericItem[];
+ classes?: CssGenericItem[];
+ parts?: CssPartAttribute[];
+}
+
+export interface CssProperty extends BaseContribution, CssContributionsHost {
+ values?: string[];
+}
+
+export interface CssPseudoElement extends BaseContribution, CssContributionsHost {
+ arguments?: boolean;
+}
+
+export interface CssPseudoClass extends BaseContribution, CssContributionsHost {
+ arguments?: boolean;
+}
+
+export interface CssGenericItem extends BaseContribution, CssContributionsHost {}
diff --git a/src/transformers/webtypes/webtypes-transformer.ts b/src/transformers/webtypes/webtypes-transformer.ts
new file mode 100644
index 00000000..3e57a561
--- /dev/null
+++ b/src/transformers/webtypes/webtypes-transformer.ts
@@ -0,0 +1,237 @@
+import { getTypeHintFromType } from "../../util/get-type-hint-from-type";
+import { Program, TypeChecker } from "typescript";
+import { AnalyzerResult } from "../../analyze/types/analyzer-result";
+import { ComponentDefinition } from "../../analyze/types/component-definition";
+import { ComponentEvent } from "../../analyze/types/features/component-event";
+import { ComponentCssProperty } from "../../analyze/types/features/component-css-property";
+import { ComponentMember } from "../../analyze/types/features/component-member";
+import { arrayDefined } from "../../util/array-util";
+import { TransformerConfig } from "../transformer-config";
+import { TransformerFunction } from "../transformer-function";
+import {
+ HtmlAttribute,
+ WebtypesSchema,
+ HtmlElement,
+ BaseContribution,
+ Js,
+ GenericJsContribution,
+ CssProperty,
+ HtmlAttributeValue,
+ SlotAttribute,
+ CssContributionsHost
+} from "./webtypes-schema";
+import { getFirst } from "../../util/set-util";
+import { relative } from "path";
+
+interface SourceDescription {
+ module: string;
+ className: string;
+}
+
+/**
+ * Transforms results to json.
+ * @param results
+ * @param program
+ * @param config
+ */
+export const webtypesTransformer: TransformerFunction = (results: AnalyzerResult[], program: Program, config: TransformerConfig): string => {
+ const checker = program.getTypeChecker();
+
+ // Grab all definitions
+ const definitions = results.map(res => res.componentDefinitions).reduce((acc, cur) => [...acc, ...cur], []);
+
+ // Transform all definitions into "tags"
+ const elements = definitions.map(d => definitionToHTMLElement(d, checker, config));
+ const cssProperties = definitions.map(d => definitionToCssProperties(d)).reduce((acc, cur) => [...acc, ...cur], []);
+
+ const webTypeConfig = config.webTypes;
+
+ const webtypesJson: WebtypesSchema = {
+ $schema: "http://json.schemastore.org/web-types",
+ name: webTypeConfig?.name ?? "",
+ version: webTypeConfig?.version ?? "",
+ ...(webTypeConfig?.framework ? { framework: webTypeConfig?.framework } : {}),
+ ...(webTypeConfig?.["js-types-syntax"] ? { "js-types-syntax": webTypeConfig?.["js-types-syntax"] } : {}),
+ ...(webTypeConfig?.["default-icon"] ? { "default-icon": webTypeConfig?.["default-icon"] } : {}),
+ ...(webTypeConfig?.["framework-config"] ? { "framework-config": webTypeConfig?.["framework-config"] } : {}),
+ ...(webTypeConfig?.["description-markup"] ? { "description-markup": webTypeConfig?.["description-markup"] } : {}),
+ contributions: {
+ html: {
+ elements: elements
+ },
+ css: {
+ properties: cssProperties
+ }
+ }
+ };
+
+ return JSON.stringify(webtypesJson, null, 4);
+};
+
+function definitionToCssProperties(definition: ComponentDefinition): CssProperty[] {
+ if (!definition.declaration) return [];
+
+ return arrayDefined(definition.declaration.cssProperties.map(e => componentCssPropertiesToAttr(e)));
+}
+
+function definitionToHTMLElement(definition: ComponentDefinition, checker: TypeChecker, config: TransformerConfig): HtmlElement {
+ const declaration = definition.declaration;
+
+ if (declaration == null) {
+ return {
+ name: definition.tagName,
+ attributes: []
+ };
+ }
+
+ const build: HtmlElement = {
+ name: definition.tagName,
+ ...(declaration.deprecated !== undefined ? { deprecated: true } : {})
+ };
+
+ // Build description
+ if (declaration?.jsDoc?.description) build.description = declaration.jsDoc.description;
+
+ // Build source section
+ const node = getFirst(definition.identifierNodes);
+ const path = getRelativePath(node?.getSourceFile().fileName, config);
+
+ let sourceDescription: SourceDescription | null = null;
+ if (node?.getText() && path) {
+ build.source = {
+ module: path,
+ symbol: node.getText()
+ };
+ sourceDescription = {
+ module: path,
+ className: node.getText()
+ };
+ }
+
+ // Build attributes
+ const customElementAttributes = arrayDefined(
+ declaration.members.map(d => componentMemberToAttr(d.attrName, d, sourceDescription, checker, config))
+ );
+ if (customElementAttributes.length > 0) build.attributes = customElementAttributes;
+
+ const js: Js = {};
+
+ // Build properties
+ const customElementProperties = arrayDefined(
+ declaration.members.map(d => componentMemberToAttr(d.propName, d, sourceDescription, checker, config))
+ );
+ if (customElementProperties.length > 0) js.properties = customElementProperties;
+
+ // Build events
+ const eventAttributes = arrayDefined(declaration.events.map(e => componentEventToAttr(e)));
+ if (eventAttributes.length > 0) js.events = eventAttributes;
+
+ if (js.properties || js.events) build.js = js;
+
+ // Build slots
+ const slots: SlotAttribute[] = declaration.slots?.map(slot => ({
+ name: slot.name || "",
+ description: slot.jsDoc?.description || ""
+ }));
+ if (slots && slots.length > 0) {
+ slots.forEach(slot => {
+ if (slot.name == "") slot.priority = "low";
+ });
+ build.slots = slots;
+ }
+
+ // Build component CSS
+ const css: CssContributionsHost = {};
+ if (declaration.cssParts && declaration.cssParts.length > 0) {
+ css.parts = declaration.cssParts?.map(part => ({
+ name: part.name,
+ description: part.jsDoc?.description || ""
+ }));
+ }
+
+ if (css.parts) build.css = css;
+
+ return build;
+}
+
+function getRelativePath(fileName: string | undefined, config: TransformerConfig): string | undefined {
+ return fileName != null && config.cwd != null
+ ? `${config.webTypes?.pathAsAbsolute ? "" : "./"}${relative(config.cwd, fileName)}`.replaceAll("\\", "/")
+ : undefined;
+}
+
+function componentEventToAttr(event: ComponentEvent): GenericJsContribution {
+ const builtEvent: GenericJsContribution = {
+ name: event.name
+ };
+
+ if (event?.jsDoc?.description) builtEvent.description = event.jsDoc.description;
+
+ return builtEvent;
+}
+
+function componentCssPropertiesToAttr(cssProperty: ComponentCssProperty): CssProperty {
+ const builtCssProp: CssProperty = {
+ name: cssProperty.name
+ };
+
+ if (cssProperty?.jsDoc?.description) builtCssProp.description = cssProperty.jsDoc.description;
+ if (cssProperty.default) {
+ if (builtCssProp.description) builtCssProp.description += "\n\n**Default:** " + cssProperty.default;
+ else builtCssProp.description = "**Default:** " + cssProperty.default;
+ }
+
+ return builtCssProp;
+}
+
+function componentMemberToAttr(
+ propName: string | undefined,
+ member: ComponentMember,
+ sourceDescription: SourceDescription | null,
+ checker: TypeChecker,
+ config: TransformerConfig
+): BaseContribution | undefined {
+ if (propName == null) {
+ return undefined;
+ }
+
+ const types: string[] | string = getTypeHintFromType(member.typeHint ?? member.type?.(), checker, config)?.split(" | ") ?? [];
+ const isPlainEnum = types.every(t => t == "null" || t == "undefined" || t.trim().match(/^["'].*["']$/));
+ const typeValues: Partial = isPlainEnum
+ ? {
+ kind: "plain",
+ type: types.join(" | ")
+ }
+ : {
+ type: types && Array.isArray(types) && types.length == 1 ? types[0] : types
+ };
+
+ const attr: HtmlAttribute = {
+ name: propName,
+ required: !!member.required,
+ priority: member.visibility == "private" || member.visibility == "protected" ? "lowest" : "normal",
+ value: {
+ ...typeValues,
+ required: !isBoolean(types),
+ ...(member.default !== undefined ? { default: JSON.stringify(member.default) } : {})
+ },
+ ...(member.deprecated !== undefined ? { deprecated: true } : {})
+ };
+
+ if (member?.jsDoc?.description) attr.description = member.jsDoc.description;
+
+ if (sourceDescription !== null) {
+ attr.source = {
+ module: sourceDescription.module,
+ symbol: sourceDescription.className + "." + member.propName
+ };
+ }
+
+ return attr;
+}
+
+function isBoolean(type: string | string[]): boolean {
+ if (Array.isArray(type)) return type.some(t => t && t.toLowerCase().includes("boolean"));
+
+ return type ? type.toLowerCase().includes("boolean") : false;
+}
diff --git a/test/helpers/webtypes-test-utils.ts b/test/helpers/webtypes-test-utils.ts
new file mode 100644
index 00000000..e383b34d
--- /dev/null
+++ b/test/helpers/webtypes-test-utils.ts
@@ -0,0 +1,43 @@
+import { analyzeTextWithCurrentTsModule } from "./analyze-text-with-current-ts-module";
+import { VirtualSourceFile } from "../../src/analyze";
+import { System } from "typescript";
+import { getCurrentTsModule } from "./ts-test";
+import { transformAnalyzerResult, TransformerConfig, WebTypesTransformerConfig } from "../../src/transformers";
+import { GenericJsContribution, HtmlAttribute, HtmlElement, WebtypesSchema } from "../../src/transformers/webtypes/webtypes-schema";
+
+export function runWebtypesBuild(source: VirtualSourceFile[] | VirtualSourceFile, config: Partial = {}): string {
+ const parseResult = analyzeTextWithCurrentTsModule(source);
+
+ const system: System = getCurrentTsModule().sys;
+ const transformerConfig: TransformerConfig = {
+ inlineTypes: false,
+ visibility: "public",
+ cwd: system.getCurrentDirectory(),
+ webTypes: {
+ name: "test",
+ version: "1.0.0",
+ ...config
+ }
+ };
+
+ return transformAnalyzerResult("webtypes", parseResult.results, parseResult.program, transformerConfig);
+}
+
+export function runAndParseWebtypesBuild(
+ source: VirtualSourceFile[] | VirtualSourceFile,
+ config: Partial = {}
+): WebtypesSchema {
+ return JSON.parse(runWebtypesBuild(source, config));
+}
+
+export function findHtmlElementOfName(schema: WebtypesSchema, name: string): HtmlElement | undefined {
+ return schema.contributions?.html?.elements?.find(el => el.name == name);
+}
+
+export function findAttributeOfName(element: HtmlElement | undefined, name: string): HtmlAttribute | undefined {
+ return element?.attributes?.find(el => el.name == name);
+}
+
+export function findPropertyOfName(element: HtmlElement | undefined, name: string): GenericJsContribution | undefined {
+ return element?.js?.properties?.find(el => el.name == name);
+}
diff --git a/test/transformers/webtypes/attributes-and-properties-test.ts b/test/transformers/webtypes/attributes-and-properties-test.ts
new file mode 100644
index 00000000..5c266ce5
--- /dev/null
+++ b/test/transformers/webtypes/attributes-and-properties-test.ts
@@ -0,0 +1,177 @@
+import { tsTest } from "../../helpers/ts-test";
+import { findAttributeOfName, findHtmlElementOfName, findPropertyOfName, runAndParseWebtypesBuild } from "../../helpers/webtypes-test-utils";
+import { Type } from "../../../src/transformers/webtypes/webtypes-schema";
+
+tsTest("Transformer: Webtypes: Attributes and properties only present if needed", t => {
+ const res = runAndParseWebtypesBuild(`
+ @customElement('my-element')
+ class MyElement extends HTMLElement {
+ @property({type: String, attribute: "my-prop"}) myProp: string;
+ @property({type: String, attribute: false}) myProp2: string;
+ }
+ `);
+
+ const myElement = findHtmlElementOfName(res, "my-element");
+ t.truthy(myElement);
+ t.is(myElement?.attributes?.length, 1);
+ t.is(myElement?.js?.properties?.length, 2);
+ t.truthy(findAttributeOfName(myElement, "my-prop"));
+ t.truthy(findPropertyOfName(myElement, "myProp"));
+ t.truthy(findPropertyOfName(myElement, "myProp2"));
+});
+
+tsTest("Transformer: Webtypes: Attributes and properties default value", t => {
+ const res = runAndParseWebtypesBuild(`
+ @customElement('my-element')
+ class MyElement extends HTMLElement {
+ @property({type: String, attribute: "my-prop"}) myProp: string;
+ @property({type: String, attribute: "my-prop2"}) myProp2: string = "testValue";
+ }
+ `);
+
+ const myElement = findHtmlElementOfName(res, "my-element");
+ t.truthy(myElement);
+ t.is(myElement?.attributes?.length, 2);
+ t.is(myElement?.js?.properties?.length, 2);
+ const att1 = findAttributeOfName(myElement, "my-prop");
+ const att2 = findAttributeOfName(myElement, "my-prop2");
+ const prop1 = findPropertyOfName(myElement, "myProp");
+ const prop2 = findPropertyOfName(myElement, "myProp2");
+
+ t.is(att1?.value?.type, "string");
+ t.false(att1?.required);
+ t.true(att1?.value?.required);
+ t.is(att1?.value?.default, undefined);
+
+ t.is(att2?.value?.type, "string");
+ t.false(att2?.required);
+ t.true(att2?.value?.required);
+ t.is(att2?.value?.default, '"testValue"');
+
+ t.is(prop1?.value?.type, "string");
+ t.false(prop1?.required);
+ t.true(prop1?.value?.required);
+ t.is(prop1?.value?.default, undefined);
+
+ t.is(prop2?.value?.type, "string");
+ t.false(prop2?.required);
+ t.true(prop2?.value?.required);
+ t.is(prop2?.value?.default, '"testValue"');
+});
+
+tsTest("Transformer: Webtypes: Boolean value not required", t => {
+ const res = runAndParseWebtypesBuild(`
+ @customElement('my-element')
+ class MyElement extends HTMLElement {
+ @property({type: Boolean, attribute: "my-prop"}) myProp: boolean;
+ @property({type: Boolean, attribute: "my-prop2"}) myProp2: boolean | undefined = false;
+ }
+ `);
+
+ const myElement = findHtmlElementOfName(res, "my-element");
+ t.truthy(myElement);
+ t.is(myElement?.attributes?.length, 2);
+ t.is(myElement?.js?.properties?.length, 2);
+ const att1 = findAttributeOfName(myElement, "my-prop");
+ const att2 = findAttributeOfName(myElement, "my-prop2");
+ const prop1 = findPropertyOfName(myElement, "myProp");
+ const prop2 = findPropertyOfName(myElement, "myProp2");
+
+ t.is(att1?.value?.type, "boolean");
+ t.false(att1?.required);
+ t.false(att1?.value?.required);
+ t.is(att1?.value?.default, undefined);
+
+ t.is((att2?.value?.type as Array)?.length, 2);
+ t.true((att2?.value?.type as Array)?.includes("boolean"));
+ t.true((att2?.value?.type as Array)?.includes("undefined"));
+ t.false(att2?.required);
+ t.false(att2?.value?.required);
+ t.is(att2?.value?.default, "false");
+
+ t.is(prop1?.value?.type, "boolean");
+ t.false(prop1?.required);
+ t.false(prop1?.value?.required);
+ t.is(prop1?.value?.default, undefined);
+
+ t.is((prop2?.value?.type as Array)?.length, 2);
+ t.true((prop2?.value?.type as Array)?.includes("boolean"));
+ t.true((prop2?.value?.type as Array)?.includes("undefined"));
+ t.false(prop2?.required);
+ t.false(prop2?.value?.required);
+ t.is(prop2?.value?.default, "false");
+});
+
+tsTest("Transformer: Webtypes: Enum values", t => {
+ const res = runAndParseWebtypesBuild(`
+ @customElement('my-element')
+ class MyElement extends HTMLElement {
+ /**
+ * @type {'foo' | 'bar'}
+ */
+ @property({type: String, attribute: "my-prop"}) myProp = "foo";
+ }
+ `);
+
+ const myElement = findHtmlElementOfName(res, "my-element");
+ t.truthy(myElement);
+
+ t.is(myElement?.attributes?.length, 1);
+ t.is(myElement?.js?.properties?.length, 1);
+ const att1 = findAttributeOfName(myElement, "my-prop");
+ const prop1 = findPropertyOfName(myElement, "myProp");
+
+ t.is(att1?.value?.type, "'foo' | 'bar'");
+ t.false(att1?.required);
+ t.is(att1?.value?.kind, "plain");
+ t.true(att1?.value?.required);
+ t.is(att1?.value?.default, '"foo"');
+
+ t.is(prop1?.value?.type, "'foo' | 'bar'");
+ t.false(prop1?.required);
+ t.is(prop1?.value?.kind, "plain");
+ t.true(prop1?.value?.required);
+ t.is(prop1?.value?.default, '"foo"');
+});
+
+tsTest("Transformer: Webtypes: Slots values", t => {
+ const res = runAndParseWebtypesBuild(`
+ /**
+ * @slot - Default slot desc
+ * @slot named-slot - Named slot desc
+ */
+ @customElement('my-element')
+ class MyElement extends HTMLElement {}
+ `);
+
+ const myElement = findHtmlElementOfName(res, "my-element");
+ t.truthy(myElement);
+
+ t.is(myElement?.slots?.length, 2);
+ const defaultSlot = myElement?.slots?.find(slot => slot.name == "");
+ const namedSlot = myElement?.slots?.find(slot => slot.name == "named-slot");
+
+ t.is(defaultSlot?.description, "Default slot desc");
+ t.is(namedSlot?.description, "Named slot desc");
+});
+
+tsTest("Transformer: Webtypes: CssParts values", t => {
+ const res = runAndParseWebtypesBuild(`
+ /**
+ * @csspart part1 - Part 1 desc
+ * @csspart part-2 - Part 2 desc
+ */
+ @customElement('my-element')
+ class MyElement extends HTMLElement {}
+ `);
+
+ const myElement = findHtmlElementOfName(res, "my-element");
+ t.truthy(myElement);
+
+ t.is(myElement?.css?.parts?.length, 2);
+ const defaultSlot = myElement?.css?.parts?.find(slot => slot.name == "part1");
+ const namedSlot = myElement?.css?.parts?.find(slot => slot.name == "part-2");
+
+ t.is(defaultSlot?.description, "Part 1 desc");
+ t.is(namedSlot?.description, "Part 2 desc");
+});
diff --git a/test/transformers/webtypes/basic-test.ts b/test/transformers/webtypes/basic-test.ts
new file mode 100644
index 00000000..d3cf5d62
--- /dev/null
+++ b/test/transformers/webtypes/basic-test.ts
@@ -0,0 +1,74 @@
+import { tsTest } from "../../helpers/ts-test";
+import { findAttributeOfName, findHtmlElementOfName, findPropertyOfName, runAndParseWebtypesBuild } from "../../helpers/webtypes-test-utils";
+import { WebTypesTransformerConfig } from "../../../src/transformers";
+import { SourceModule } from "../../../src/transformers/webtypes/webtypes-schema";
+
+tsTest("Transformer: Webtypes: File header test", t => {
+ const config: WebTypesTransformerConfig = {
+ name: "PkgTest",
+ version: "1.2.3-test",
+ "default-icon": "path/foo.png",
+ "framework-config": {
+ "enable-when": {
+ "node-packages": ["lit", "lit-html"]
+ }
+ },
+ framework: "test-fw",
+ "description-markup": "html"
+ };
+ const res = runAndParseWebtypesBuild(
+ `
+ @customElement('my-element')
+ class MyElement extends HTMLElement {}
+ `,
+ config
+ );
+
+ t.is(res.name, config.name);
+ t.is(res.version, config.version);
+ t.is(res.framework, config.framework);
+ t.is(res["default-icon"], config["default-icon"]);
+ t.is(res["description-markup"], config["description-markup"]);
+ t.deepEqual(res["framework-config"], config["framework-config"]);
+ t.is(res.contributions?.html?.elements?.length, 1);
+});
+
+tsTest("Transformer: Webtypes: Basic content test", t => {
+ const res = runAndParseWebtypesBuild([
+ `
+ @customElement('my-element')
+ class MyElement extends HTMLElement {
+ @property({type: String, attribute: "my-prop"}) myProp: string;
+ @property({type: String, attribute: "my-prop2"}) myProp2: string;
+ }
+ `,
+ `
+ @customElement('my-element2')
+ class MyElement2 extends HTMLElement {
+ @property({type: String, attribute: "my-prop"}) myProp: string;
+ }
+ `
+ ]);
+
+ t.is(res.contributions?.html?.elements?.length, 2);
+
+ const myElement = findHtmlElementOfName(res, "my-element");
+ t.truthy(myElement);
+ t.is((myElement?.source as SourceModule)?.symbol, "MyElement");
+ // Test element contains expected attributes
+ t.is(myElement?.attributes?.length, 2);
+ t.truthy(findAttributeOfName(myElement, "my-prop"));
+ t.truthy(findAttributeOfName(myElement, "my-prop2"));
+ // Test element contains expected properties
+ t.is(myElement?.js?.properties?.length, 2);
+ t.truthy(findPropertyOfName(myElement, "myProp"));
+ t.truthy(findPropertyOfName(myElement, "myProp2"));
+
+ const myElement2 = findHtmlElementOfName(res, "my-element2");
+ t.truthy(myElement2);
+ t.is((myElement2?.source as SourceModule)?.symbol, "MyElement2");
+ t.is(myElement2?.attributes?.length, 1);
+ t.truthy(findAttributeOfName(myElement2, "my-prop"));
+ t.is(myElement2?.js?.properties?.length, 1);
+ t.truthy(findPropertyOfName(myElement2, "myProp"));
+});
diff --git a/test/transformers/webtypes/css-test.ts b/test/transformers/webtypes/css-test.ts
new file mode 100644
index 00000000..de13afcc
--- /dev/null
+++ b/test/transformers/webtypes/css-test.ts
@@ -0,0 +1,26 @@
+import { tsTest } from "../../helpers/ts-test";
+import { runAndParseWebtypesBuild } from "../../helpers/webtypes-test-utils";
+
+tsTest("Transformer: Webtypes: CSS properties test", t => {
+ const res = runAndParseWebtypesBuild(`
+ /**
+ * @cssprop [--var-test] - Var test desc
+ * @cssprop [--var-test2=48px] - Var test desc with default value
+ */
+ @customElement('my-element')
+ class MyElement extends HTMLElement {}
+ `);
+
+ const cssProps = res?.contributions?.css?.properties;
+ t.truthy(cssProps);
+ t.is(cssProps?.length, 2);
+
+ const varTest = cssProps?.find(cp => cp.name == "--var-test");
+ t.truthy(varTest);
+ t.is(varTest?.description, "Var test desc");
+
+ const varTest2 = cssProps?.find(cp => cp.name == "--var-test2");
+ t.truthy(varTest2);
+ t.is(varTest2?.description, "Var test desc with default value\n\n**Default:** 48px");
+ // default value not put in CSS
+});
diff --git a/test/transformers/webtypes/event-test.ts b/test/transformers/webtypes/event-test.ts
new file mode 100644
index 00000000..040e8f51
--- /dev/null
+++ b/test/transformers/webtypes/event-test.ts
@@ -0,0 +1,29 @@
+import { tsTest } from "../../helpers/ts-test";
+import { findHtmlElementOfName, runAndParseWebtypesBuild } from "../../helpers/webtypes-test-utils";
+
+tsTest("Transformer: Webtypes: Element events test", t => {
+ const res = runAndParseWebtypesBuild(`
+ /**
+ * @fires test-event - Test event desc
+ * @fires test-event2 {CustomEvent<{index: int, name: string}>} - Test event desc with typing
+ */
+ @customElement('my-element')
+ class MyElement extends HTMLElement {}
+ `);
+
+ const myElement = findHtmlElementOfName(res, "my-element");
+ t.truthy(myElement);
+
+ const events = myElement?.js?.events;
+ t.truthy(events);
+ t.is(events?.length, 2);
+
+ const testEvent = events?.find(cp => cp.name == "test-event");
+ t.truthy(testEvent);
+ t.is(testEvent?.description, "Test event desc");
+
+ const testEvent2 = events?.find(cp => cp.name == "test-event2");
+ t.truthy(testEvent2);
+ t.is(testEvent2?.description, "Test event desc with typing");
+ // typing value not put in event
+});