From 190f61231f65d34db13857c65593986c47431ed7 Mon Sep 17 00:00:00 2001 From: RiN Date: Tue, 7 Sep 2021 11:33:44 +0800 Subject: [PATCH] feat(CLI): support prefix, suffix, and typescript annotation in `` component (#4) * feat: typscript support NOTES: In the ouput, it will be with typeAnnotation for and only component * feat(CLI): add options for prefix and suffix --- index.bin.js | 176 ++++++++++++++++++++++++++++++++------------------ lib/ast.js | 16 ++++- lib/chakra.js | 19 +++++- lib/utils.js | 40 +++++++++++- 4 files changed, 182 insertions(+), 69 deletions(-) diff --git a/index.bin.js b/index.bin.js index 01edf6d..92fecb5 100755 --- a/index.bin.js +++ b/index.bin.js @@ -14,12 +14,7 @@ const { exit, stderr: error, } = require("process"); -const { - pascalCase: PascalCase, - camelCase, - snakeCase: snake_case, - constantCase: CONSTANT_CASE, -} = require("change-case"); +const { stringToCase, compose } = require("./lib/utils"); const encoding = "utf-8"; if (input.isTTY) { @@ -28,78 +23,67 @@ if (input.isTTY) { input.setEncoding(encoding); input.on("data", function (data) { if (data) { - const name = argv.name || argv.n || "Unamed"; - const exportNameCase = argv.C || argv.case; - const source = createCode(name, { + const { + name, + exportNameCase, + exportNameSuffix, + exportNamePrefix, + isTypescript, + outputFile, + } = getCommonOptions(argv); + const exportNamed = createExportNamed( + exportNameCase, + exportNamePrefix, + exportNameSuffix + ); + const source = createCode({ source: data, - displayName: stringToCase(name, exportNameCase), + displayName: exportNamed(name), + isTypescript, + exportNameSuffix, + exportNamePrefix, }); - output.write(source); + return outputFile + ? Fs.writeFile(Path.resolve(outputFile), source, (err) => { + if (err) { + error.write(err, () => exit(1)); + } + }) + : output.write(source); } }); } -function createCode(...sources) { - const icon = createChakraIcon(...sources); - return BabelGenerator(icon).code; -} - -function stringToCase(str, _case) { - return { - [true]: PascalCase(str), - [_case === "pascal"]: PascalCase(str), - [_case === "camel"]: camelCase(str), - [_case === "constant"]: CONSTANT_CASE(str), - [_case === "snake"]: snake_case(str), - }[true]; -} - -function stringToInput({ displayName, exportNameCase, encoding }) { - return function (acc, str) { - if (Fs.existsSync(str)) { - if (Fs.lstatSync(str).isDirectory()) { - const pathResolved = Path.resolve(str); - acc.push( - ...Fs.readdirSync(pathResolved) - .filter((f) => f.split(".")[1] === "svg") - .map((f) => Path.join(pathResolved, f)) - .map((source) => ({ - displayName: stringToCase( - Path.basename(source).split(".")[0], - exportNameCase - ), - source: Fs.readFileSync(source, encoding), - })) - ); - } else { - acc.push({ - displayName: stringToCase(displayName, exportNameCase), - source: Fs.readFileSync(str, encoding), - }); - } - } - return acc; - }; -} -// :: [Object] -> () *Effect* function main(args) { - const inputs = (args.i && [args.i]) || (args.input && [args.input]) || args._; + const { + inputs, + outputFile, + name, + exportNameCase, + exportNameSuffix, + exportNamePrefix, + isTypescript, + } = getCommonOptions(args); const version = args.V || args.version; - const outFile = args.o || args.output; - const name = args.name || args.n || "Unamed"; - const exportNameCase = args.C || args.case; if (inputs.length > 0) { // make code const source = createCode( ...inputs.reduce( - stringToInput({ displayName: name, exportNameCase, encoding }), + stringToInput({ + displayName: name, + exportNameCase, + encoding, + isTypescript, + exportNameSuffix, + exportNamePrefix, + }), [] ) ); // write output in output - return outFile - ? Fs.writeFile(Path.resolve(outFile), source, (err) => { + return outputFile + ? Fs.writeFile(Path.resolve(outputFile), source, (err) => { if (err) { error.write(err, () => exit(1)); } @@ -129,9 +113,12 @@ OPTIONS: -C, --case Sets for case [snake|camel|constant|pascal] in export named declaration output. [default: pascal] + -S, --suffix Sets for suffix in export named declaration. + -P, --prefix Sets for prefix in export named declaration. + [e.g.: -S "Icon"] - --ts, --typescript Sets output as TypeScript code. (UNAVAILABLE, SOON). + --ts, --typescript Sets output as TypeScript code. [INPUT]: This option for read the input from PATH of FILE or DIRECTORIES. @@ -140,3 +127,68 @@ OPTIONS: ${packageName} (version: ${packageVersion}) `); } + +function getCommonOptions(args) { + return { + inputs: (args.i && [args.i]) || (args.input && [args.input]) || args._, + outputFile: args.o || args.output, + name: args.name || args.n || "Unamed", + isTypescript: args.ts || args.typescript || false, + exportNameCase: args.C || args.case, + exportNameSuffix: args.S || args.suffix || "", + exportNamePrefix: args.P || args.prefix || "", + }; +} + +function createExportNamed(exportNameCase, exportNamePrefix, exportNameSuffix) { + return compose( + (str) => stringToCase(str, exportNameCase), + (str) => `${str}${exportNameSuffix}`, + (str) => `${exportNamePrefix}${str}` + ); +} + +function stringToInput({ + displayName, + exportNameCase, + exportNamePrefix, + exportNameSuffix, + encoding, + isTypescript, +}) { + const exportNamed = createExportNamed( + exportNameCase, + exportNamePrefix, + exportNameSuffix + ); + + return function (acc, str) { + if (Fs.existsSync(str)) { + if (Fs.lstatSync(str).isDirectory()) { + const pathResolved = Path.resolve(str); + acc.push( + ...Fs.readdirSync(pathResolved) + .filter((f) => f.split(".")[1] === "svg") + .map((f) => Path.join(pathResolved, f)) + .map((source) => ({ + displayName: exportNamed(Path.basename(source).split(".")[0]), + source: Fs.readFileSync(source, encoding), + isTypescript, + })) + ); + } else { + acc.push({ + displayName: exportNamed(displayName), + source: Fs.readFileSync(str, encoding), + isTypescript, + }); + } + } + return acc; + }; +} + +function createCode(...sources) { + const icon = createChakraIcon(...sources); + return BabelGenerator(icon).code; +} diff --git a/lib/ast.js b/lib/ast.js index 26d8e28..7d92afd 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -224,10 +224,13 @@ const hastToJSXProperties = ({ /** * @memberof ast * @name jsxPropertiesToComponent - * @param {Object} + * @param {Object} hast + * @param {Object} options + * @param {Boolean} options.isTypescript + * @param {String} options.displayName * @returns {Object} */ -const hastToComponent = (hast, displayName) => { +const hastToComponent = (hast, { displayName, isTypescript }) => { const svgTagToIcon = ({ properties: { viewBox }, ...others }) => ({ ...others, properties: { viewBox }, @@ -259,8 +262,15 @@ const hastToComponent = (hast, displayName) => { iconElement ); // @see {https://babeljs.io/docs/en/babel-types#variabledeclarator} + const variableIdentifier = t.identifier(displayName); + + if (isTypescript) { + variableIdentifier.typeAnnotation = t.tsTypeAnnotation( + t.tsTypeReference(t.identifier("IconProps")) + ); + } const variableDeclarator = t.variableDeclarator( - t.identifier(displayName), + variableIdentifier, arrowFunctionIcon ); // @see {https://babeljs.io/docs/en/babel-types#variabledeclaration} diff --git a/lib/chakra.js b/lib/chakra.js index 9118e16..7f3bf56 100644 --- a/lib/chakra.js +++ b/lib/chakra.js @@ -7,16 +7,28 @@ const ast = require("./ast"); /** * @memberof chakra * @name createChakraIcon - * @param {String} svg - * @param {String} displayName + * @param {Object[]} svg * @returns {Object} + * @example + * createChakraIcon({ + * isTypescript: false, + * displayName: "Hei", + * source: ` + * + * + * `, + * }) */ const createChakraIcon = (...sources) => { + const isTypescript = sources.some(({ isTypescript }) => isTypescript); const perFileCode = ({ source: svg, displayName }) => { const hast = SvgParser.parse(svg); if (ast.hastChildrenLength(hast) > 1) { - return ast.hastToComponent(hast, displayName); + return ast.hastToComponent(hast, { displayName, isTypescript }); } const properties = ast.hastToProperties(hast); @@ -56,6 +68,7 @@ const createChakraIcon = (...sources) => { const isNotEmptyString = (str) => str !== ""; // usage for generate import module const imports = [ + isTypescript ? "IconProps" : "", svgCodes.some(hasArrowFunctionExpression) ? "Icon" : "", svgCodes.some(hasCallExpression) ? "createIcon" : "", ].filter(isNotEmptyString); diff --git a/lib/utils.js b/lib/utils.js index 8c57199..e21d46f 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -2,6 +2,13 @@ * @module utils * @description provided utility function. */ +const { + pascalCase, + camelCase, + snakeCase, + constantCase, +} = require("change-case"); + /** * @memberof utils * @name compose @@ -29,8 +36,19 @@ const objectToPair = (object) => objectToPairs(object)[0]; /** * @memberof utils * @name pairsToObject - * @param {Array} + * @param {[String,Any][]} pairs * @returns {Object} + * @example + * const pairs = [ + * ["name", "ninja"], + * ["from", "japan"], + * ]; + * + * const ninjaObject = pairsToObject(pairs) + * // { + * // name: "ninja", + * // from: "japan", + * // } */ const pairsToObject = (pairs) => Object.fromEntries(pairs); /** @@ -40,6 +58,25 @@ const pairsToObject = (pairs) => Object.fromEntries(pairs); * @returns {Array} */ const objectToPairs = (object) => Object.entries(object); +/** + * + * @memberof utils + * @name stringToCase + * @param {String} str + * @param {String} [_case="pascal"] - case style "camel" | "constant" | "snake" + * @returns {String} + * @example + * const str = "Hei" + * stringToCase(str, "constant") + * // HEI + */ +const stringToCase = (str, _case) => + ({ + [true]: pascalCase(str), + [_case === "camel"]: camelCase(str), + [_case === "constant"]: constantCase(str), + [_case === "snake"]: snakeCase(str), + }[true]); module.exports = { pairToObject, @@ -47,4 +84,5 @@ module.exports = { pairsToObject, objectToPairs, compose, + stringToCase, };