@kadconsulting/dry
Version:
KAD Reusable Component Library
266 lines (248 loc) • 11.3 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateIcon = void 0;
const format_1 = require('./format.cjs');
// Declare a variable for inquirer but don't initialize it here
const inquirer_1 = require("inquirer");
const fs = require("fs/promises");
const child_process_1 = require("child_process");
const indexTemplate = (pathComponentName) => `export { ${pathComponentName} } from './${pathComponentName}';`;
const pathTemplate = (pathComponentName, untitledUIIconName, paths) => `import type { PathProps } from '../../Icon/IconTypes';
export const ${pathComponentName} = ({ stroke }: PathProps) => (
<>
<title>${untitledUIIconName}</title>
${Object.values(paths)
.map((path) => `<path
className="dry-icon-${untitledUIIconName}"
stroke={stroke ? \`var(--\${stroke})\` : '#000'}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="${path.d}"
${!path.customProps
? ''
: Object.values(path.customProps).map((attrValuePair) => `${attrValuePair}`)}
/>`)
.join('')}
</>
);
`;
const testTemplate = (pathComponentName, untitledUIIconName, paths) => `import Icon from '../../Icon/Icon';
import { ${pathComponentName} } from './${pathComponentName}';
import { IconSizes } from '../../Icon/IconTypes';
import { render, screen } from '@testing-library/react';
describe('${pathComponentName} icon path', () => {
it('renders with a dry-prepended className', () => {
// ARRANGE
const { container } = render(
<Icon size={IconSizes.SMALL} Path={${pathComponentName}} />
);
// ASSERT
/** Some icons consist of multiple paths; those that have multiple are grouped with a <g> element */
const pathOrGroup = container.firstElementChild?.children[1];
expect(
Array.from(pathOrGroup?.classList ?? []).includes('dry-icon-${untitledUIIconName}')
).toBeTruthy();
});
it('renders the correct defaults', () => {
// ARRANGE
const { container } = render(<Icon Path={${pathComponentName}} />);
/** Some icons consist of multiple paths; those that have multiple are grouped with a <g> element */
const pathOrGroup = container.firstElementChild?.children[1];
expect(pathOrGroup).toHaveAttribute('stroke', '#000');
expect(pathOrGroup).toHaveAttribute('stroke-width', '2');
expect(pathOrGroup).toHaveAttribute('stroke-linecap', 'round');
expect(pathOrGroup).toHaveAttribute('stroke-linejoin', 'round');
});
it('renders the correct accessibility title', () => {
// ARRANGE
render(<Icon Path={${pathComponentName}} />);
// ASSERT
expect(screen.getByTitle('${untitledUIIconName}')).toBeInTheDocument();
});
it('renders with the consumer-specified stroke color', () => {
// ARRANGE
const color = 'primary';
render(
<Icon
Path={${pathComponentName}}
PathProps={{
stroke: 'primary',
}}
/>
);
// ASSERT
/** Some icons consist of multiple paths; those that have multiple are grouped with a <g> element */
const pathOrGroup =
screen.getByTitle('${untitledUIIconName}').parentElement?.children[1];
expect(pathOrGroup).toHaveAttribute('stroke', \`var(--\${color})\`);
});
it('renders the correct path(s)', () => {
// ARRANGE
const { container } = render(<Icon Path={${pathComponentName}} />);
// ASSERT
${generatePathAssertions(paths)}
});
});
`;
/**
* Generate dynamic test assertions based on:
* 1) the number of paths the icon consists of
* 2) the d attribute value for each path
* 3) the number of custom props for each path
*/
const generatePathAssertions = (paths) => {
const generateCustomPropsAssertions = (pathIndex, customProps) => Object.values(customProps).reduce((acc, curr) => {
const [attr, value] = curr.split('=');
/** Transform the camelCase React attribute name into the hyphenated DOM-supported attr name */
const hyphenatedAttr = attr.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`);
return `${acc}\nexpect(path${pathIndex}).toHaveAttribute('${hyphenatedAttr}', '${value.replace(/"/g, '')}');`;
}, '');
/** The container's firstElementChild's first child (index 0) will be the <title> element; the paths will start at index 1 */
return Object.values(paths).reduce((acc, curr, index) => {
const pathIndex = index + 1;
return `${acc}
const path${index} = container.firstElementChild?.children[${pathIndex}];
expect(path${index}).toHaveAttribute('d', '${curr.d}');
${generateCustomPropsAssertions(index, curr.customProps)}
`;
}, '');
};
/** Generate a map of untitled-ui-icon-name: Component for IconSearch */
const regenPathsIndexFile = () => __awaiter(void 0, void 0, void 0, function* () {
const files = yield fs.readdir('./src/components/Icons/paths');
yield fs.writeFile('./src/components/Icons/paths/index.ts',
/** Write imports */
`import type { ComponentType } from 'react';\n\n
${files
.map((fileName) => fileName === 'index.ts'
? '' // skip the index file
: `import { ${fileName} } from '../paths/${fileName}';`)
.join('\n')}\n
type AllIcons = {
[untitledUIIconName: string]: ComponentType;
};
export const ALL_ICONS: AllIcons = {
${files
.map((fileName) => fileName === 'index.ts'
? '' // skip the index file
: `'${fileName
.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`)
.replace(/([0-9][0-9])/g, (g) => `-${g}`)
.replace(/^-/, '') // remove leading hyphen prepended by the regex above, while preserving hyphens in the middle of the name
}': ${fileName},`)
.join('\n')}
}
export {
${files.map((fileName) => fileName === 'index.ts'
? '' // skip the index file
: fileName)}\n
}
`);
});
const collectIconInfoFromUser = () => __awaiter(void 0, void 0, void 0, function* () {
process.stdout.write('This utility will walk you through creating a new icon.\n');
process.stdout.write('Before we proceed, paste the icon SVG from Figma into SVGR. SVGR is sometimes able to collapse multiple paths into one. This link provides the correct defaults for the SVGR playground: https://react-svgr.com/playground/?dimensions=false&typescript=true\n');
const { untitledUIIconName, componentName, numberOfPaths } = yield inquirer_1.default.prompt([
{
type: 'input',
name: 'untitledUIIconName',
message: 'Enter the hyphenated Untitled UI icon name:',
},
{
type: 'input',
name: 'componentName',
message: 'Enter the PascalCase component name (if default is correct, press ENTER):',
default: (answers) => {
const { untitledUIIconName } = answers;
const componentName = untitledUIIconName
.split('-')
.map((word) => word[0].toUpperCase() + word.slice(1))
.join('');
return componentName;
},
},
{
min: 1,
default: 1,
type: 'number',
name: 'numberOfPaths',
message: 'Enter the number of paths the icon consists of:',
},
]);
const pathDataAttributes = {};
for (let pathIndex = 0; pathIndex < numberOfPaths; pathIndex++) {
const paths = yield inquirer_1.default.prompt({
type: 'input',
validate: (value) => {
if (value.length)
return true;
return 'Please enter a d attribute value for the icon path.';
},
name: `${pathIndex}`,
message: numberOfPaths === 1
? `Enter the d attribute value for the icon's <path> element:`
: `Enter the d attribute value for <path> ${pathIndex} of the icon:`,
});
pathDataAttributes[pathIndex] = {
d: paths[`${pathIndex}`],
customProps: {},
};
const { numberOfCustomProps } = yield inquirer_1.default.prompt([
{
default: 0,
type: 'number',
name: 'numberOfCustomProps',
message: '(Rare) Are there any custom props for this icon path other than stroke, strokeLinecap, strokeLinejoin, strokeWidth, or d? If so, how many? (if none, press ENTER):',
},
]);
for (let customPropIndex = 0; customPropIndex < numberOfCustomProps; customPropIndex++) {
const customProps = yield inquirer_1.default.prompt({
type: 'input',
name: `${customPropIndex}`,
message: `Enter the custom prop attribute / value pair for the icon's <path> element in HTML format (i.e. clipRule="evenodd"):`,
});
pathDataAttributes[pathIndex].customProps[customPropIndex] =
customProps[`${customPropIndex}`];
}
}
return {
untitledUIIconName,
componentName,
pathDataAttributes,
};
});
const writeIconPathDirectory = (_a) => __awaiter(void 0, [_a], void 0, function* ({ untitledUIIconName, componentName, pathDataAttributes, }) {
yield fs.mkdir(`./src/components/Icons/paths/${componentName}`, {
recursive: true,
});
yield fs.writeFile(`./src/components/Icons/paths/${componentName}/index.ts`, indexTemplate(componentName));
yield fs.writeFile(`./src/components/Icons/paths/${componentName}/${componentName}.tsx`, pathTemplate(componentName, untitledUIIconName, pathDataAttributes));
yield fs.writeFile(`./src/components/Icons/paths/${componentName}/${componentName}.test.tsx`, testTemplate(componentName, untitledUIIconName, pathDataAttributes));
yield regenPathsIndexFile();
/** Format generated files */
yield (0, format_1.default)('src/components/Icons/paths/**/');
(0, child_process_1.exec)('rome format ./src/components/Icons/paths/index.ts --write');
});
function generateIcon() {
return __awaiter(this, void 0, void 0, function* () {
const { untitledUIIconName, componentName, pathDataAttributes } = yield collectIconInfoFromUser();
yield writeIconPathDirectory({
untitledUIIconName,
componentName,
pathDataAttributes,
});
process.stdout.write(`Icon ${componentName} created successfully!\n`);
process.exit(0);
});
}
exports.generateIcon = generateIcon;