Skip to content

Commit

Permalink
feat(CLI): support prefix, suffix, and typescript annotation in `<Ico…
Browse files Browse the repository at this point in the history
…n />` component (#4)

* feat: typscript support

NOTES:
In the ouput, it will be with typeAnnotation for and only <Icon /> component

* feat(CLI): add options for prefix and suffix
  • Loading branch information
r17x authored Sep 7, 2021
1 parent 5df56be commit 190f612
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 69 deletions.
176 changes: 114 additions & 62 deletions index.bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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));
}
Expand Down Expand Up @@ -129,9 +113,12 @@ OPTIONS:
-C, --case <snake|camel|constant|pascal>
Sets for case [snake|camel|constant|pascal] in export named declaration
output. [default: pascal]
-S, --suffix <STRING> Sets for suffix in export named declaration.
-P, --prefix <STRING> 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.
Expand All @@ -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;
}
16 changes: 13 additions & 3 deletions lib/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -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}
Expand Down
19 changes: 16 additions & 3 deletions lib/chakra.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: `
* <svg viewBox=\"0 0 200 200\">
* <path
* fill="#666"
* d="M 100, 100 m -75, 0 a 75,75 0 1,0 150,0 a 75,75 0 1,0 -150,0"
* />
* </svg>`,
* })
*/
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);
Expand Down Expand Up @@ -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);
Expand Down
40 changes: 39 additions & 1 deletion lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
* @module utils
* @description provided utility function.
*/
const {
pascalCase,
camelCase,
snakeCase,
constantCase,
} = require("change-case");

/**
* @memberof utils
* @name compose
Expand Down Expand Up @@ -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);
/**
Expand All @@ -40,11 +58,31 @@ 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,
objectToPair,
pairsToObject,
objectToPairs,
compose,
stringToCase,
};

0 comments on commit 190f612

Please sign in to comment.