diff --git a/custom-elements-manifest.config.mjs b/custom-elements-manifest.config.mjs new file mode 100644 index 00000000..d72a9a96 --- /dev/null +++ b/custom-elements-manifest.config.mjs @@ -0,0 +1,266 @@ +function skipInternals () { + // These class members (and also all private ones) are internal and should not be included in the manifest file + const fields = ["innerHTML", "_el", "props", "events"]; + const methods = ["render", "handleEvent", "propChangedCallback"]; + + let members = [...fields, ...methods]; + + return { + name: "color-elements-skip-internals-plugin", + + // Runs for each color element module; after the analysis phase, all information about the module is now available + moduleLinkPhase ({ moduleDoc }) { + let classes = moduleDoc.declarations?.filter(declaration => declaration.kind === "class") ?? []; + + for (let Class of classes) { + if (!Class.members) { + continue; + } + + Class.members = Class.members.filter(member => member.privacy !== "private" && !members.includes(member.name)); + } + }, + }; +} + +function addTagName () { + return { + name: "color-elements-tag-name-plugin", + + // Runs for each color element module + analyzePhase ({ ts, node, moduleDoc, context }) { + if (node.kind === ts.SyntaxKind.ClassDeclaration) { + let className = node.name.getText(); + let Class = moduleDoc.declarations.find(declaration => declaration.name === className); + + if (!Class) { + return; + } + + for (let member of node.members) { + let name = member.name?.getText(); + + if (name === "tagName") { // static tagName = "..." + let tagName = member.initializer?.text; + if (tagName) { + Class.tagName = tagName; + } + } + } + } + }, + }; +} + +function defineProps () { + return { + name: "color-elements-define-props-plugin", + + // Runs for each color element module + analyzePhase ({ ts, node, moduleDoc, context }) { + if (node.kind === ts.SyntaxKind.ClassDeclaration) { + let className = node.name.getText(); + let Class = moduleDoc.declarations.find(declaration => declaration.name === className); + + if (!Class) { + return; + } + + for (let member of node.members) { + let name = member.name?.getText(); + + if (name === "props") { // static props = { ... } + let classProps = []; + + let props = member.initializer?.properties ?? []; + props = props.filter(prop => prop.jsDoc?.length); // we are interested in props with JSDoc only (it's a flag that they should be added to the manifest) + + for (let prop of props) { + let propName = prop.name.getText(); + let spec = prop.initializer?.properties ?? []; + let jsDoc = prop.jsDoc[0]; // all info about the prop should be in the first JSDoc block + let tags = jsDoc.tags; + let description = jsDoc.comment ?? ""; // the prop description is provided without a tag + + let ret = { kind: "field", name: propName, description }; + + // === Type === + // Find the prop type (if any) in the @type tag + let type = tags?.find(tag => tag.tagName.getText() === "type")?.typeExpression?.type.getText() ?? ""; + if (!type) { + // If the @type tag is not provided, try to get the type from the prop's spec + let typeProp = spec.find(prop => prop.name.getText() === "type"); + type = typeProp?.initializer?.getText() ?? ""; + } + + if (type) { + ret.type = { text: type }; + } + + // === Default === + // Find the prop default value (if any) in the @default tag + let defaultValue = tags?.find(tag => tag.tagName.getText() === "default")?.comment ?? ""; + if (!defaultValue) { + // If the @default tag is not provided, try to get the default value from the prop's spec (if it's not a function) + let defaultProp = spec.find(prop => prop.name.getText() === "default" && + (prop.initializer?.kind !== ts.SyntaxKind.ArrowFunction || prop.initializer?.kind !== ts.SyntaxKind.MethodDeclaration)); + + let kind = defaultProp?.initializer?.kind; + if (kind === ts.SyntaxKind.NumericLiteral) { + defaultValue = Number(defaultProp?.initializer?.text); + } + else if (kind === ts.SyntaxKind.StringLiteral) { + defaultValue = defaultProp?.initializer?.text ?? ""; + } + else { + defaultValue = defaultProp?.initializer?.getText() ?? ""; + } + } + + if (defaultValue) { + ret.default = defaultValue; + } + + // === Reflect === + let reflect; + let reflectProp = spec.find(prop => prop.name.getText() === "reflect"); + + // By default, reflect is true unless get is also specified, in which case it defaults to false + if (!reflectProp && !spec.find(prop => prop.name.getText() === "get")) { + reflect = true; + } + else if (reflectProp) { + let kind = reflectProp.initializer?.kind; + if (kind === ts.SyntaxKind.TrueKeyword) { + // Reflect to/from an attribute with the same name as the prop + reflect = true; + } + else if (kind === ts.SyntaxKind.StringLiteral) { + // Reflect to/from an attribute with the given name + reflect = reflectProp.initializer.text; + } + else if (kind === ts.SyntaxKind.ObjectLiteralExpression) { + let from = reflectProp.initializer.properties.find(prop => prop.name.getText() === "from"); + let fromKind = from.initializer.kind; + if (fromKind === ts.SyntaxKind.StringLiteral) { + // Reflect from an attribute with the given name + reflect = from.initializer.text; + } + else if (fromKind === ts.SyntaxKind.TrueKeyword) { + // Reflect from an attribute with the same name as the prop + reflect = true; + } + } + } + + if (reflect) { + ret.reflects = true; + ret.attribute = typeof reflect === "boolean" ? propName : reflect; + } + + classProps.push(ret); + } + + if (classProps.length) { + (Class.members ??= []).push(...classProps); + } + } + } + } + }, + }; +} + +function defineEvents () { + return { + name: "color-elements-define-events-plugin", + + // Runs for each color element module + analyzePhase ({ ts, node, moduleDoc, context }) { + if (node.kind === ts.SyntaxKind.ClassDeclaration) { + let className = node.name.getText(); + let Class = moduleDoc.declarations.find(declaration => declaration.name === className); + + if (!Class) { + return; + } + + for (let member of node.members) { + let name = member.name?.getText(); + + if (name === "events") { // static events = { ... } + let events = member.initializer?.properties ?? []; + let classEvents = []; + + for (let event of events) { + let eventName = event.name.getText(); + let jsDoc = event.jsDoc; + + if (!jsDoc?.length) { + // Event doesn't have JSDoc, so we can't get any information about it, except its name + classEvents.push({ name: eventName }); + continue; + } + + jsDoc = jsDoc[0]; // all info about the event should be in the first JSDoc block + let description = jsDoc?.comment ?? ""; // the event description is provided without a tag + + // Find the event type (if any) in the @type tag + let tags = jsDoc.tags; + let type = tags?.find(tag => tag.tagName.getText() === "type")?.typeExpression?.type.getText() ?? ""; + + let ret = { name: eventName, description }; + if (type) { + ret.type = { text: type }; + } + classEvents.push(ret); + } + + if (classEvents.length) { + // We define the events described in the events property only. + // We can use the comment line below if we are interested in all events (including those dispatched with this.dispatchEvent). + // (Class.events ??= []).push(...classEvents); + Class.events = classEvents; + } + } + } + } + }, + }; +} + +function defineAttributes () { + return { + name: "color-elements-define-attributes-plugin", + + // Runs for each color element module; after the analysis phase, all information about the module is now available + moduleLinkPhase ({ moduleDoc }) { + let classes = moduleDoc.declarations?.filter(declaration => declaration.kind === "class") ?? []; + + for (let Class of classes) { + if (!Class.members) { + continue; + } + + // We define attributes for all props that have the reflects flag set to true + let props = Class.members.filter(member => member.reflects); + for (let prop of props) { + let {attribute: name, default: defaultValue, name: fieldName} = prop; + let ret = { name, type: {text: "string"}, fieldName }; + if (defaultValue !== undefined) { + ret.default = defaultValue; + } + + (Class.attributes ??= []).push(ret); + } + } + }, + }; +} + +export default { + globs: ["src/**/*.js"], + exclude: ["src/common", "src/*.njk"], + outdir: ".", + plugins: [addTagName(), defineProps(), defineEvents(), skipInternals(), defineAttributes()], +}; diff --git a/package-lock.json b/package-lock.json index 278639cb..59623c6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ }, "devDependencies": { "@11ty/eleventy": "^3.0.0-alpha.6", + "@custom-elements-manifest/analyzer": "^0.10.2", "@stylistic/eslint-plugin": "latest", "eslint": "latest", "globals": "latest", @@ -226,6 +227,96 @@ "node": ">=6.9.0" } }, + "node_modules/@custom-elements-manifest/analyzer": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@custom-elements-manifest/analyzer/-/analyzer-0.10.2.tgz", + "integrity": "sha512-YkfAfaNGSulnXxyIAHU3K8Z7bYGmIU2MlPvEaQPXnWUaIFMo0p3VEVYvByvENnVCQKPPDyjlkCm73u/zRnRvMA==", + "dev": true, + "dependencies": { + "@custom-elements-manifest/find-dependencies": "^0.0.5", + "@github/catalyst": "^1.6.0", + "@web/config-loader": "0.1.3", + "chokidar": "3.5.2", + "command-line-args": "5.1.2", + "comment-parser": "1.2.4", + "custom-elements-manifest": "1.0.0", + "debounce": "1.2.1", + "globby": "11.0.4", + "typescript": "~5.4.2" + }, + "bin": { + "cem": "cem.js", + "custom-elements-manifest": "cem.js" + } + }, + "node_modules/@custom-elements-manifest/analyzer/node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@custom-elements-manifest/analyzer/node_modules/chokidar": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/@custom-elements-manifest/analyzer/node_modules/globby": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", + "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@custom-elements-manifest/analyzer/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@custom-elements-manifest/find-dependencies": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@custom-elements-manifest/find-dependencies/-/find-dependencies-0.0.5.tgz", + "integrity": "sha512-fKIMMZCDFSoL2ySUoz8knWgpV4jpb0lUXgLOvdZQMQFHxgxz1PqOJpUIypwvEVyKk3nEHRY4f10gNol02HjeCg==", + "dev": true, + "dependencies": { + "es-module-lexer": "^0.9.3" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -361,6 +452,12 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@github/catalyst": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@github/catalyst/-/catalyst-1.6.0.tgz", + "integrity": "sha512-u8A+DameixqpeyHzvnJWTGj+wfiskQOYHzSiJscCWVfMkIT3rxnbHMtGh3lMthaRY21nbUOK71WcsCnCrXhBJQ==", + "dev": true + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -1096,6 +1193,18 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/@web/config-loader": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@web/config-loader/-/config-loader-0.1.3.tgz", + "integrity": "sha512-XVKH79pk4d3EHRhofete8eAnqto1e8mCRAqPV00KLNFzCWSe8sWmLnqKCqkPNARC6nksMaGrATnA5sPDRllMpQ==", + "dev": true, + "dependencies": { + "semver": "^7.3.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/a-sync-waterfall": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", @@ -1227,6 +1336,15 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/array-back": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", + "integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==", + "dev": true, + "engines": { + "node": ">=12.17" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", @@ -1878,6 +1996,21 @@ "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.0.tgz", "integrity": "sha512-qekjTiBLM3F/sXKks/ih5aWaHIGu+Ftel0yKEvmpbKvmxpNOhojKgha5uiWEUOqEpRjC1Tq3nJRT7WgdBOxIGg==" }, + "node_modules/command-line-args": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.1.2.tgz", + "integrity": "sha512-fytTsbndLbl+pPWtS0CxLV3BEWw9wJayB8NnU2cbQqVPsNdYezQeT+uIQv009m+GShnMNyuoBrRo8DTmuTfSCA==", + "dev": true, + "dependencies": { + "array-back": "^6.1.2", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/commander": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", @@ -1887,6 +2020,15 @@ "node": ">=14" } }, + "node_modules/comment-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.2.4.tgz", + "integrity": "sha512-pm0b+qv+CkWNriSTMsfnjChF9kH0kxz55y44Wo5le9qLxMj5xDQAaEd9ZN1ovSuk9CsrncWaFwgpOMg7ClJwkw==", + "dev": true, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2031,6 +2173,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/custom-elements-manifest": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/custom-elements-manifest/-/custom-elements-manifest-1.0.0.tgz", + "integrity": "sha512-j59k0ExGCKA8T6Mzaq+7axc+KVHwpEphEERU7VZ99260npu/p/9kd+Db+I3cGKxHkM5y6q5gnlXn00mzRQkX2A==", + "dev": true + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -2091,6 +2239,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2574,6 +2728,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-module-lexer": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "dev": true + }, "node_modules/es-object-atoms": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", @@ -3198,6 +3358,27 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "dev": true, + "dependencies": { + "array-back": "^3.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/find-replace/node_modules/array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -4982,6 +5163,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, "node_modules/lodash.capitalize": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz", @@ -7559,7 +7746,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7568,6 +7754,15 @@ "node": ">=14.17" } }, + "node_modules/typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/uc.micro": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", diff --git a/package.json b/package.json index d34b715d..48878ee7 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "A set of web components for working with color. A Color.js project.", "main": "index.js", "type": "module", + "customElements": "custom-elements.json", "scripts": { "eslint": "npx eslint .", "eslint:fix": "npx eslint . --fix", @@ -37,6 +38,7 @@ }, "devDependencies": { "@11ty/eleventy": "^3.0.0-alpha.6", + "@custom-elements-manifest/analyzer": "^0.10.2", "@stylistic/eslint-plugin": "latest", "eslint": "latest", "globals": "latest",