From d8700650a4e5991862e2076cc2c93482011c4c90 Mon Sep 17 00:00:00 2001 From: Stoyan <88034608+hinzzx@users.noreply.github.com> Date: Wed, 8 Mar 2023 09:08:22 +0200 Subject: [PATCH] feat: enhance create-ui5-element command to create component in TS (#6609) * feat: enhance create-ui5-element command to create component in Typescript * feat: add second argument as an option to create-ui5-element command for specifying the language * feat: add third argument as an option for specifying a custom tag --- .../tools/lib/create-new-component/index.js | 213 +++++++++--------- .../jsFileContentTemplate.js | 77 +++++++ .../tsFileContentTemplate.js | 84 +++++++ 3 files changed, 266 insertions(+), 108 deletions(-) create mode 100644 packages/tools/lib/create-new-component/jsFileContentTemplate.js create mode 100644 packages/tools/lib/create-new-component/tsFileContentTemplate.js diff --git a/packages/tools/lib/create-new-component/index.js b/packages/tools/lib/create-new-component/index.js index c937bdbcc988..44d21f646f19 100644 --- a/packages/tools/lib/create-new-component/index.js +++ b/packages/tools/lib/create-new-component/index.js @@ -1,80 +1,7 @@ const fs = require("fs"); - -const jsFileContentTemplate = componentName => { - return `import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; -import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js"; -import ${componentName}Template from "./generated/templates/${componentName}Template.lit.js"; - -// Styles -import ${componentName}Css from "./generated/themes/${componentName}.css.js"; - -/** - * @public - */ -const metadata = { - tag: "${tagName}", - properties: /** @lends sap.ui.webc.${library}.${componentName}.prototype */ { - // - }, - slots: /** @lends sap.ui.webc.${library}.${componentName}.prototype */ { - // - }, - events: /** @lends sap.ui.webc.${library}.${componentName}.prototype */ { - // - }, -}; - -/** - * @class - * - *

Overview

- * - * - *

Usage

- * - * For the ${tagName} - *

ES6 Module Import

- * - * import ${packageName}/dist/${componentName}.js"; - * - * @constructor - * @author SAP SE - * @alias sap.ui.webc.${library}.${componentName} - * @extends sap.ui.webc.base.UI5Element - * @tagname ${tagName} - * @public - */ -class ${componentName} extends UI5Element { - static get metadata() { - return metadata; - } - - static get render() { - return litRender; - } - - static get styles() { - return ${componentName}Css; - } - - static get template() { - return ${componentName}Template; - } - - static get dependencies() { - return []; - } - - static async onDefine() { - - } -} - -${componentName}.define(); - -export default ${componentName}; -`; -}; +const prompts = require("prompts"); +const jsFileContentTemplate = require("./jsFileContentTemplate.js"); +const tsFileContentTemplate = require("./tsFileContentTemplate.js"); const getPackageName = () => { if (!fs.existsSync("./package.json")) { @@ -108,47 +35,117 @@ const getLibraryName = packageName => { return packageName.substr("webcomponents-".length); }; -const camelToKebabCase = string => string.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); +// String manipulation +const capitalizeFirstLetter = string => string.charAt(0).toUpperCase() + string.slice(1); + +// Validation of user input +const isNameValid = name => typeof name === "string" && name.match(/^[a-zA-Z][a-zA-Z0-9_-]*$/); +const isTagNameValid = tagName => tagName.match(/^([a-z][a-z0-9]*-)([a-z0-9]+(-[a-z0-9]+)*)$/); + +const generateFiles = (componentName, tagName, library, packageName, isTypeScript) => { + componentName = capitalizeFirstLetter(componentName); + const filePaths = { + "main": isTypeScript + ? `./src/${componentName}.ts` + : `./src/${componentName}.js`, + "css": `./src/themes/${componentName}.css`, + "template": `./src/${componentName}.hbs`, + }; -const packageName = getPackageName(); -const library = getLibraryName(packageName); + const FileContentTemplate = isTypeScript + ? tsFileContentTemplate(componentName, tagName, library, packageName) + : jsFileContentTemplate(componentName, tagName, library, packageName); -const consoleArguments = process.argv.slice(2); -const componentName = consoleArguments[0]; + fs.writeFileSync(filePaths.main, FileContentTemplate, { flag: "wx+" }); + fs.writeFileSync(filePaths.css, "", { flag: "wx+" }); + fs.writeFileSync(filePaths.template, "
Hello World
", { flag: "wx+" }); -if (!componentName){ - console.error("Please enter component name."); - return; + console.log(`Successfully generated ${filePaths.main}`); + console.log(`Successfully generated ${filePaths.css}`); + console.log(`Successfully generated ${filePaths.template}`); + + // Change the color of the output + console.warn('\x1b[33m%s\x1b[0m', ` + Make sure to import the component in your bundle by using: + import ${componentName} from "./dist/${componentName}.js";`); } -const tagName = `ui5-${camelToKebabCase(componentName)}`; +// Main function +const createWebComponent = async () => { + const packageName = getPackageName(); + const library = getLibraryName(packageName); -const filePaths = { - "js": `./src/${componentName}.js`, - "css": `./src/themes/${componentName}.css`, - "hbs": `./src/${componentName}.hbs`, -}; -const sJsFileContentTemplate = jsFileContentTemplate(componentName); + const consoleArguments = process.argv.slice(2); + let componentName = consoleArguments[0]; + let tagName = consoleArguments[1]; + let language = consoleArguments[2]; + let isTypeScript; -fs.writeFileSync(filePaths.js, sJsFileContentTemplate, { flag: "wx+" }); -fs.writeFileSync(filePaths.css, "", { flag: "wx+" }); -fs.writeFileSync(filePaths.hbs, "
Hello World
", { flag: "wx+" }); + if (componentName && !isNameValid(componentName)) { + throw new Error("Invalid component name. Please use only letters, numbers, dashes and underscores. The first character must be a letter."); + } -console.log(`Successfully generated ${componentName}.js`); -console.log(`Successfully generated ${componentName}.css`); -console.log(`Successfully generated ${componentName}.hbs`); + if (tagName && !isTagNameValid(tagName)) { + throw new Error("Invalid tag name. The tag name should only contain lowercase letters, numbers, dashes, and underscores. The first character must be a letter, and it should follow the pattern 'tag-name'."); + } -const bundleLogger = fs.createWriteStream("./bundle.common.js", { - flags: "a" // appending -}); + if (language && language !== "typescript" && language !== "ts" && language !== "javascript" && language !== "js") { + throw new Error("Invalid language. Please use 'typescript','javascript' or their respective 'ts','js'."); + } -bundleLogger.write(` -// TODO: Move this line in order to keep the file sorted alphabetically -import ${componentName} from "./dist/${componentName}.js";`); + if (!componentName) { + const response = await prompts({ + type: "text", + name: "componentName", + message: "Please enter a component name:", + validate: (value) => isNameValid(value), + }); + componentName = response.componentName; + + if (!componentName) { + process.exit(); + } + } -// Change the color of the output - console.warn('\x1b[33m%s\x1b[0m', ` -Component is imported in bundle.common.js. -Do NOT forget to sort the file in alphabeticall order. -`); \ No newline at end of file + if (!tagName) { + const response = await prompts({ + type: "text", + name: "tagName", + message: "Please enter a tag name:", + validate: (value) => isTagNameValid(value), + }); + tagName = response.tagName; + + if (!tagName) { + process.exit(); + } + } + + if (!language) { + const response = await prompts({ + type: "select", + name: "isTypeScript", + message: "Please select a language:", + choices: [ + { + title: "TypeScript (recommended)", + value: true, + }, + { + title: "JavaScript", + value: false, + }, + ], + }); + isTypeScript = response.isTypeScript; + } else if (language === "typescript" || language === "ts") { + isTypeScript = true; + } else { + isTypeScript = false; + } + + generateFiles(componentName, tagName, library, packageName, isTypeScript); +}; + +createWebComponent(); \ No newline at end of file diff --git a/packages/tools/lib/create-new-component/jsFileContentTemplate.js b/packages/tools/lib/create-new-component/jsFileContentTemplate.js new file mode 100644 index 000000000000..363933ccf318 --- /dev/null +++ b/packages/tools/lib/create-new-component/jsFileContentTemplate.js @@ -0,0 +1,77 @@ +const jsFileContentTemplate = (componentName, tagName, library, packageName) => { + return `import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; +import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js"; +import ${componentName}Template from "./generated/templates/${componentName}Template.lit.js"; + +// Styles +import ${componentName}Css from "./generated/themes/${componentName}.css.js"; + +/** + * @public + */ +const metadata = { + tag: "${tagName}", + properties: /** @lends sap.ui.webc.${library}.${componentName}.prototype */ { + // + }, + slots: /** @lends sap.ui.webc.${library}.${componentName}.prototype */ { + // + }, + events: /** @lends sap.ui.webc.${library}.${componentName}.prototype */ { + // + }, +}; + +/** + * @class + * + *

Overview

+ * + * + *

Usage

+ * + * For the ${tagName} + *

ES6 Module Import

+ * + * import ${packageName}/dist/${componentName}.js"; + * + * @constructor + * @author SAP SE + * @alias sap.ui.webc.${library}.${componentName} + * @extends sap.ui.webc.base.UI5Element + * @tagname ${tagName} + * @public + */ +class ${componentName} extends UI5Element { + static get metadata() { + return metadata; + } + + static get render() { + return litRender; + } + + static get styles() { + return ${componentName}Css; + } + + static get template() { + return ${componentName}Template; + } + + static get dependencies() { + return []; + } + + static async onDefine() { + + } +} + +${componentName}.define(); + +export default ${componentName}; +`; +}; + +module.exports = jsFileContentTemplate; diff --git a/packages/tools/lib/create-new-component/tsFileContentTemplate.js b/packages/tools/lib/create-new-component/tsFileContentTemplate.js new file mode 100644 index 000000000000..169a41d59485 --- /dev/null +++ b/packages/tools/lib/create-new-component/tsFileContentTemplate.js @@ -0,0 +1,84 @@ +const tsFileContentTemplate = (componentName, tagName, library, packageName) => { + return `import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; +import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; +import property from "@ui5/webcomponents-base/dist/decorators/property.js"; +import slot from "@ui5/webcomponents-base/dist/decorators/slot.js"; +import event from "@ui5/webcomponents-base/dist/decorators/event.js"; +import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js"; + +import ${componentName}Template from "./generated/templates/${componentName}Template.lit.js"; + +// Styles +import ${componentName}Css from "./generated/themes/${componentName}.css.js"; + +/** + * @class + * + *

Overview

+ * + * + *

Usage

+ * + * For the ${tagName} + *

ES6 Module Import

+ * + * import ${packageName}/dist/${componentName}.js"; + * + * @constructor + * @author SAP SE + * @alias sap.ui.webc.${library}.${componentName} + * @extends sap.ui.webc.base.UI5Element + * @tagname ${tagName} + * @public + */ +@customElement({ + tag: "${tagName}", + renderer: litRender, + styles: ${componentName}Css, + template: ${componentName}Template, + dependencies: [], +}) + +/** + * Example custom event. + * Please keep in mind that all public events should be documented in the API Reference as shown below. + * + * @event sap.ui.webc.${library}.${componentName}#interact + * @public + */ +@event("interact", { detail: { /* event payload ( optional ) */ } }) +class ${componentName} extends UI5Element { + /** + * Defines the value of the component. + * + * @type {string} + * @name sap.ui.webc.${library}.${componentName}.prototype.value + * @defaultvalue "" + * @public + */ + @property() + value!: string; + + /** + * Defines the text of the component. + * + * @type {Node[]} + * @name sap.ui.webc.${library}.${componentName}.prototype.default + * @slot + * @public + */ + @slot({ type: Node, "default": true }) + text!: Array; + + static async onDefine() { + + } +} + +${componentName}.define(); + +export default ${componentName}; +`; +}; + +module.exports = tsFileContentTemplate;