From 9459f36b33075b3bef712dce2e4ca3833529a97d Mon Sep 17 00:00:00 2001 From: kreuzerk Date: Mon, 15 Jun 2020 22:59:46 +0200 Subject: [PATCH] feat(converting): add new convertion option to convert svg-icons to objects --- README.md | 48 ++++++--- src/bin/svg-to-ts.ts | 24 ++++- src/lib/converters/object.converter.ts | 20 ++++ src/lib/converters/single-file.converter.ts | 2 +- src/lib/options/args-collector.ts | 59 ++++++++++- src/lib/options/config-collector.ts | 110 ++++++++++++-------- src/lib/options/convertion-options.ts | 33 ++++-- src/lib/options/default-options.ts | 1 + 8 files changed, 227 insertions(+), 70 deletions(-) create mode 100644 src/lib/converters/object.converter.ts diff --git a/README.md b/README.md index a732143..849aa49 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,8 @@ are made with Angular, however `svg-to-ts` can also be used with other framework - `svg-to-ts` optimizes your SVG icons under the hood - `svg-to-ts` automatically generates types and interfaces for your icons to improve typesafety - `svg-to-ts` was developed based on the experiences of providin an icon library for a large enterprise. -- highly configurable - supports multiple use cases. +- offers three different convertion modes ('object', 'single-file' and 'multiple-files') +- each method is highly configurable to supports multiple use cases. # How to use svg-to-ts @@ -78,6 +79,7 @@ Once you run `svg-to-ts` those configurations will be picked up. "generate-icons": "svg-to-ts" }, "svg-to-ts": { + "convertionType": "single-file", "srcFiles": ["./projects/dinosaur-icons/icons/**/*.svg"], "outputDirectory": "./projects/dinosaur-icons/icons", "interfaceName": "DinosaurIcon", @@ -107,6 +109,7 @@ Once you run `svg-to-ts` those configurations will be picked up. ```json { "svg-to-ts": { + "convertionType": "single-file", "srcFiles": ["./projects/dinosaur-icons/icons/**/*.svg"], "outputDirectory": "./projects/dinosaur-icons/icons", "interfaceName": "DinosaurIcon", @@ -139,19 +142,39 @@ If you decide to configure `svg-to-ts` by using a `.rc` file, it still makes sen } ``` -## Use-cases +## ConvertionTypes + +svg-to-ts offers three different kinds of convertion types; Converting your icons to a single object, +converting your icons to constants or converting your icons to single files. Each approach is designed +to solve a specific kind of problem. You can switch between approaches by passing `convertionType` property (`object`, `single-file` or `multiple-files`). + +### 1. Converting to a single object (`convertionType==='object'`) + +In this scenario the SVG icons are converted to a single object. It's an approach that is suitable if your icon registry +accepts an object with the filename as key and the svg data as key. + +Available options: + +| --version | type | default | output the version number | +| --------------- | ----------------------- | ---------------------------------------- | ---------------------------------------------------------------------------- | +| fileName | stirng | my-icons | file name of the generated file | +| delimiter | CAMEL, KEBAP, SNAKE | SNAKE | delimiter which is used to generate the types and name properties | +| svgoConfig | string or config object | check help command - to large to display | a path to your svgoConfiguration JSON file or an inline configuration object | +| srcFiles | string | "/\*.svg" | input files matching the given filename pattern | +| outputDirectory | string | "./dist" | name of the output directory | As mentioned above, `svg-to-ts` supports different use-cases. You can either generate you library to a single TypeScript file with multiple constants, to single TypeScript file per Icon or to allready precompiled JavaScript files. -### Use Case 1 - Treeshakable and typesafe with one file (simpler use cases) +### 2. Multiple constants - Treeshakable and typesafe with one file (`convertionType==='single-file'`) + +This approach converts your svg icons into multiple constants in the same file so that they can be used +in combination with an icon registry. It furthermore also generates all necssary types. **We wrote a step to step guide that explains this approach further and helps you create an icon library with this approach.** +[Find out more in this blogpost](https://medium.com/angular-in-depth/how-to-create-an-icon-library-in-angular-4f8863d95a) ![Output scenario one](https://raw.githubusercontent.com/kreuzerk/svg-to-ts/master/assets/example-src1.png) Only the icons included in the consuming SPA also end up in the final bundle of the SPA. -**We wrote a step to step guide that explains this approach further and helps you create an icon library with this approach.** -[Find out more in this blogpost](https://medium.com/angular-in-depth/how-to-create-an-icon-library-in-angular-4f8863d95a) - Available configurations: | --version | type | default | output the version number | @@ -166,7 +189,6 @@ Available configurations: | svgoConfig | string or config object | check help command - to large to display | a path to your svgoConfiguration JSON file or an inline configuration object | | srcFiles | string | "/\*.svg" | input files matching the given filename pattern | | outputDirectory | string | "./dist" | name of the output directory | -| outputDirectory | string | "./dist" | name of the output directory | #### Example usage @@ -183,7 +205,12 @@ and we end up with the following file in our `dist` folder. ![output](https://raw.githubusercontent.com/kreuzerk/svg-to-ts/master/assets/output.png) -### Use Case 2 - Fully tree shakable and optimized for lazy loading (more sophisticated) +### 3. Fully tree shakable and optimized for lazy loading (`convertionType==='multiple-files'`) + +This is the most sophisticated approach and also the approach that doesn't only support tree shaking but also +supports code splitting which is especially usefull in scenarios where you are using lazy loading. + +[Here's a step by step guide on how to create an icon library that is optimized for tree shaking](https://medium.com/angular-in-depth/how-to-create-a-fully-tree-shakable-icon-library-in-angular-c5488cf9cd76) ![fully tree shakable](https://raw.githubusercontent.com/kreuzerk/svg-to-ts/master/assets/fully-treeshakable.png) Often, having the SVGs in a single file is enough. However if you are in a more complex environment with bigger business @@ -196,8 +223,6 @@ gets reduced, but also, where they end up. Means, an icon that is only used in a end up there. ![Output scenario two](https://raw.githubusercontent.com/kreuzerk/svg-to-ts/master/assets/generated-files-src2.png) -[Here's a step by step guide on how to create an icon library that is optimized for tree shaking](https://medium.com/angular-in-depth/how-to-create-a-fully-tree-shakable-icon-library-in-angular-c5488cf9cd76) - Available configurations: | --version | type | default | output the version number | @@ -212,7 +237,6 @@ Available configurations: | srcFiles | string | "/\*.svg" | input files matching the given filename pattern | | svgoConfig | string or config object | check help command - to large to display | a path to your svgoConfiguration JSON file or an inline configuration object | | outputDirectory | string | "./dist" | name of the output directory | -| outputDirectory | string | "./dist" | name of the output directory | | optimizeForLazyLoading | boolean | false | when set to true, multiple files will be generated | | additionalModelOutputPath | string | null | if a path is specified we will generate an additional file containing interface and type to this path - can be useful to improve type safety | | iconsFolderName | string | "build" | name of the folder we will build the TypeScript files to | @@ -222,7 +246,7 @@ Available configurations: ## Which approach should I use -This depends on your use case. If you have a simple application, it's probably enought to go with the single file and the constants. +This depends on your use case. If you have a simple application, it's probably enought to go with the single file or even a object. If you build a framework that is used by multiple teams, then you should probably go with the fully tree shakable scenario (generating multiple files). ## Is it possilbe to create a standalone library? diff --git a/src/bin/svg-to-ts.ts b/src/bin/svg-to-ts.ts index 5bd156f..1545c1b 100755 --- a/src/bin/svg-to-ts.ts +++ b/src/bin/svg-to-ts.ts @@ -1,18 +1,34 @@ #!/usr/bin/env node import { convertToSingleFile } from '../lib/converters/single-file.converter'; import { convertToMultipleFiles } from '../lib/converters/multiple-files.converter'; -import { getOptions, MultiFileConvertionOptions, SingleFileConvertionOptions } from '../lib/options/convertion-options'; -import { printLogo } from '../lib/helpers/log-helper'; +import { + ConvertionType, + getOptions, + MultiFileConvertionOptions, + ObjectConvertionOptions, + SingleFileConvertionOptions +} from '../lib/options/convertion-options'; +import { info, printLogo } from '../lib/helpers/log-helper'; import { setupCommander } from '../lib/options/args-collector'; +import { convertToSingleObject } from '../lib/converters/object.converter'; (async () => { setupCommander(); printLogo(); const convertionOptions = await getOptions(); - if (convertionOptions.optimizeForLazyLoading) { + if (convertionOptions.convertionType === ConvertionType.MULTIPLE_FILES) { + info('We are using the convertiontype "multiple-files"'); await convertToMultipleFiles(convertionOptions as MultiFileConvertionOptions); - } else { + } + + if (convertionOptions.convertionType === ConvertionType.SINGLE_FILE) { + info('We are using the convertiontype "single-file"'); await convertToSingleFile(convertionOptions as SingleFileConvertionOptions); } + + if (convertionOptions.convertionType === ConvertionType.OBJECT) { + info('We are using the convertiontype "single-object"'); + await convertToSingleObject(convertionOptions as ObjectConvertionOptions); + } })(); diff --git a/src/lib/converters/object.converter.ts b/src/lib/converters/object.converter.ts new file mode 100644 index 0000000..4dcebca --- /dev/null +++ b/src/lib/converters/object.converter.ts @@ -0,0 +1,20 @@ +import { ObjectConvertionOptions } from '../options/convertion-options'; +import { writeFile } from '../helpers/file-helpers'; +import { error, success, underlineSuccess } from '../helpers/log-helper'; + +import { filesProcessor, SvgDefinition } from './shared.converter'; + +export const convertToSingleObject = async (convertionOptions: ObjectConvertionOptions): Promise => { + const { outputDirectory, fileName } = convertionOptions; + try { + const svgObject = {}; + const svgDefinitions = await filesProcessor(convertionOptions); + svgDefinitions.forEach( + (svgDefinition: SvgDefinition) => (svgObject[svgDefinition.filenameWithoutEnding] = svgDefinition.data) + ); + await writeFile(outputDirectory, fileName, `export const icons = ${JSON.stringify(svgObject)}`); + success(`Icons file successfully generated under ${underlineSuccess(outputDirectory)}`); + } catch (exception) { + error(`Something went wrong: ${exception}`); + } +}; diff --git a/src/lib/converters/single-file.converter.ts b/src/lib/converters/single-file.converter.ts index 8b39af6..d6d6985 100644 --- a/src/lib/converters/single-file.converter.ts +++ b/src/lib/converters/single-file.converter.ts @@ -21,7 +21,7 @@ const getSvgConstants = svgDefinitions => { }; export const convertToSingleFile = async (convertionOptions: SingleFileConvertionOptions): Promise => { - const { typeName, interfaceName, outputDirectory, fileName } = convertionOptions; + const { outputDirectory, fileName } = convertionOptions; try { const svgDefinitions = await filesProcessor(convertionOptions); if (svgDefinitions.length) { diff --git a/src/lib/options/args-collector.ts b/src/lib/options/args-collector.ts index 300a950..6a0d302 100644 --- a/src/lib/options/args-collector.ts +++ b/src/lib/options/args-collector.ts @@ -1,15 +1,25 @@ import commander from 'commander'; -import { MultiFileConvertionOptions, SingleFileConvertionOptions } from './convertion-options'; -import { DEFAULT_OPTIONS } from './default-options'; + import * as packgeJSON from '../../../package.json'; import { Delimiter } from '../generators/code-snippet-generators'; import { getSvgoConfig } from '../helpers/svg-optimization'; +import { + MultiFileConvertionOptions, + SingleFileConvertionOptions, + ObjectConvertionOptions, + ConvertionType +} from './convertion-options'; +import { DEFAULT_OPTIONS } from './default-options'; +import { error } from '../helpers/log-helper'; + export const setupCommander = () => { const collect = (value, previous) => previous.concat([value]); commander .version(packgeJSON.version) - .option('-t --typeName ', 'name of the generated enumeration type', DEFAULT_OPTIONS.typeName) + .option('-t --convertionType ', 'convetion type (object, single-file, multiple-files)') + .option('--objectName ', 'name of the exported object', DEFAULT_OPTIONS.objectName) + .option('--typeName ', 'name of the generated enumeration type', DEFAULT_OPTIONS.typeName) .option('--generateType ', 'prevent generating enumeration type', DEFAULT_OPTIONS.generateType) .option('--generateTypeObject ', 'generate type object', DEFAULT_OPTIONS.generateTypeObject) .option('-f --fileName ', 'name of the generated file', DEFAULT_OPTIONS.fileName) @@ -69,8 +79,18 @@ const toBoolean = (str: string, defaultValue: boolean): boolean => { return result; }; -export const collectArgumentOptions = async (): Promise => { +export const collectArgumentOptions = async (): Promise< + SingleFileConvertionOptions | MultiFileConvertionOptions | ObjectConvertionOptions +> => { + if (!commander.convertionType) { + error(`A convertionType is required, please specify one by passing it via --convertionType. + Valid convertiontypes are (object, single-file or multiple-files)`); + process.exit(); + } + let { + convertionType, + objectName, delimiter, fileName, interfaceName, @@ -104,9 +124,38 @@ export const collectArgumentOptions = async (): Promise => { const explorerSync = cosmiconfigSync(packgeJSON.name); const cosmiConfigResult = explorerSync.search(); @@ -17,38 +23,14 @@ export const collectConfigurationOptions = async (): Promise< }; const mergeWithDefaults = async ( - options: MultiFileConvertionOptions | SingleFileConvertionOptions -): Promise => { + options +): Promise => { const configOptions = { ...options }; - if (!configOptions.typeName) { - configOptions.typeName = DEFAULT_OPTIONS.typeName; - info(`No typeName provided, "${DEFAULT_OPTIONS.typeName}" will be used`); - } - - if (configOptions.generateType === null) { - configOptions.generateType = DEFAULT_OPTIONS.generateType; - info(`No generateType provided, "${DEFAULT_OPTIONS.generateType}" will be used`); - } - - if (configOptions.generateTypeObject === null) { - configOptions.generateTypeObject = DEFAULT_OPTIONS.generateTypeObject; - info(`No generateTypeObject provided, "${DEFAULT_OPTIONS.generateTypeObject}" will be used`); - } - - if (!configOptions.interfaceName) { - configOptions.interfaceName = DEFAULT_OPTIONS.interfaceName; - info(`No interfaceName provided, "${DEFAULT_OPTIONS.interfaceName}" will be used`); - } - - if (typeof configOptions.prefix !== 'string') { - configOptions.prefix = DEFAULT_OPTIONS.prefix; - info(`No prefix provided, "${DEFAULT_OPTIONS.prefix}" will be used`); - } - - if (!configOptions.delimiter) { - configOptions.delimiter = DEFAULT_OPTIONS.delimiter; - info(`No delimiter provided, "${DEFAULT_OPTIONS.delimiter}" will be used`); + if (!options.convertionType) { + error(`A convertionType is required, please specify one by passing it via --convertionType. + Valid convertiontypes are (object, single-file or multiple-files)`); + process.exit(); } if (!configOptions.outputDirectory) { @@ -68,7 +50,58 @@ const mergeWithDefaults = async ( configOptions.svgoConfig = await getSvgoConfig(configOptions.svgoConfig); } - if (configOptions.optimizeForLazyLoading) { + if (!configOptions.delimiter) { + configOptions.delimiter = DEFAULT_OPTIONS.delimiter; + info(`No delimiter provided, "${DEFAULT_OPTIONS.delimiter}" will be used`); + } + + if (options.convertionType === ConvertionType.OBJECT) { + info(`Convertiontype "Object" will be used`); + + if (!(configOptions as ObjectConvertionOptions).objectName) { + configOptions.outputDirectory = DEFAULT_OPTIONS.objectName; + info(`No outputDirectory provided, "${DEFAULT_OPTIONS.objectName}" will be used`); + } + } + + if (options.convertionType === ConvertionType.SINGLE_FILE || options.convertionType === ConvertionType.OBJECT) { + if (!(configOptions as SingleFileConvertionOptions).fileName) { + (configOptions as SingleFileConvertionOptions).fileName = DEFAULT_OPTIONS.modelFileName; + info(`No fileName provided, "${DEFAULT_OPTIONS.modelFileName}" will be used`); + } + } + + if ( + options.convertionType === ConvertionType.SINGLE_FILE || + options.convertionType === ConvertionType.MULTIPLE_FILES + ) { + if (!configOptions.typeName) { + configOptions.typeName = DEFAULT_OPTIONS.typeName; + info(`No typeName provided, "${DEFAULT_OPTIONS.typeName}" will be used`); + } + + if (configOptions.generateType === null) { + configOptions.generateType = DEFAULT_OPTIONS.generateType; + info(`No generateType provided, "${DEFAULT_OPTIONS.generateType}" will be used`); + } + + if (configOptions.generateTypeObject === null) { + configOptions.generateTypeObject = DEFAULT_OPTIONS.generateTypeObject; + info(`No generateTypeObject provided, "${DEFAULT_OPTIONS.generateTypeObject}" will be used`); + } + + if (!configOptions.interfaceName) { + configOptions.interfaceName = DEFAULT_OPTIONS.interfaceName; + info(`No interfaceName provided, "${DEFAULT_OPTIONS.interfaceName}" will be used`); + } + + if (typeof configOptions.prefix !== 'string') { + configOptions.prefix = DEFAULT_OPTIONS.prefix; + info(`No prefix provided, "${DEFAULT_OPTIONS.prefix}" will be used`); + } + } + + if (configOptions.convertionType === ConvertionType.MULTIPLE_FILES) { if (!(configOptions as MultiFileConvertionOptions).modelFileName) { (configOptions as MultiFileConvertionOptions).modelFileName = DEFAULT_OPTIONS.modelFileName; info(`No modelFileName provided, "${DEFAULT_OPTIONS.modelFileName}" will be used`); @@ -84,11 +117,6 @@ const mergeWithDefaults = async ( info(`No preCompileSources flag provided, "${DEFAULT_OPTIONS.compileSources}" will be used`); } return configOptions as MultiFileConvertionOptions; - } else { - if (!(configOptions as SingleFileConvertionOptions).fileName) { - (configOptions as SingleFileConvertionOptions).fileName = DEFAULT_OPTIONS.modelFileName; - info(`No fileName provided, "${DEFAULT_OPTIONS.modelFileName}" will be used`); - } - return configOptions as SingleFileConvertionOptions; } + return configOptions as SingleFileConvertionOptions; }; diff --git a/src/lib/options/convertion-options.ts b/src/lib/options/convertion-options.ts index 2eec7e8..abd3ade 100644 --- a/src/lib/options/convertion-options.ts +++ b/src/lib/options/convertion-options.ts @@ -3,31 +3,50 @@ import { collectConfigurationOptions } from './config-collector'; import { collectArgumentOptions } from './args-collector'; import { info } from '../helpers/log-helper'; -export interface ConvertionOptions { +export enum ConvertionType { + OBJECT = 'object', + SINGLE_FILE = 'single-file', + MULTIPLE_FILES = 'multiple-files' +} + +export interface CommonConvertionOptions { + srcFiles: string[]; + outputDirectory: string; + svgoConfig: any; delimiter: Delimiter; +} + +export interface CommonFileConvertionOptions extends CommonConvertionOptions { typeName: string; generateType: boolean; generateTypeObject: boolean; prefix: string; interfaceName: string; - srcFiles: string[]; - svgoConfig: any; - outputDirectory: string; optimizeForLazyLoading: string; } -export interface SingleFileConvertionOptions extends ConvertionOptions { +export interface ObjectConvertionOptions extends CommonConvertionOptions { + convertionType: ConvertionType.OBJECT; + fileName: string; + objectName: string; +} + +export interface SingleFileConvertionOptions extends CommonFileConvertionOptions { + convertionType: ConvertionType.SINGLE_FILE; fileName: string; } -export interface MultiFileConvertionOptions extends ConvertionOptions { +export interface MultiFileConvertionOptions extends CommonFileConvertionOptions { + convertionType: ConvertionType.MULTIPLE_FILES; modelFileName: string; additionalModelOutputPath: string | null; iconsFolderName: string; compileSources: boolean; } -export const getOptions = async (): Promise => { +export const getOptions = async (): Promise< + MultiFileConvertionOptions | SingleFileConvertionOptions | ObjectConvertionOptions +> => { const configOptions = await collectConfigurationOptions(); if (configOptions) { diff --git a/src/lib/options/default-options.ts b/src/lib/options/default-options.ts index c3c9212..7581529 100644 --- a/src/lib/options/default-options.ts +++ b/src/lib/options/default-options.ts @@ -4,6 +4,7 @@ export const DEFAULT_OPTIONS = { fileName: 'my-icons', delimiter: Delimiter.SNAKE, interfaceName: 'MyIcon', + objectName: 'icons', outputDirectory: './dist', prefix: 'myIcon', srcFiles: ['*.svg'],