From 55810ea512f102896fdbb44b765857b5568bf7cd Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Thu, 8 Aug 2024 13:29:02 +0200 Subject: [PATCH] The one with multiple configs and linters --- README.md | 32 +++ package-lock.json | 63 +++++- package.json | 26 ++- src/index.ts | 198 +++++++++++++++--- test-apps/remix-vite-cjs/icons/de.svg | 1 + .../remix-vite-cjs/public/icons/sprite.svg | 6 + .../remix-vite-cjs/public/icons/types.ts | 7 + test-apps/remix-vite/app/icons/sprite.svg | 30 ++- test-apps/remix-vite/app/icons/types.ts | 10 +- test-apps/remix-vite/biome.json | 55 +++++ test-apps/remix-vite/icons/test copy.svg | 3 + test-apps/remix-vite/public/icons/sprite.svg | 20 ++ test-apps/remix-vite/public/icons/types.ts | 5 + test-apps/remix-vite/vite.config.ts | 21 +- 14 files changed, 401 insertions(+), 76 deletions(-) create mode 100644 test-apps/remix-vite-cjs/icons/de.svg create mode 100644 test-apps/remix-vite-cjs/public/icons/sprite.svg create mode 100644 test-apps/remix-vite-cjs/public/icons/types.ts create mode 100644 test-apps/remix-vite/biome.json create mode 100644 test-apps/remix-vite/icons/test copy.svg create mode 100644 test-apps/remix-vite/public/icons/sprite.svg create mode 100644 test-apps/remix-vite/public/icons/types.ts diff --git a/README.md b/README.md index 43570da..4cdc372 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,10 @@ export default { fileName: "icon.svg", // The cwd, defaults to process.cwd() cwd: process.cwd(), + // What formatter to use to format the generated files, prettier or biome, defaults to no formatter + formatter: "biome", + // The path to the formatter config file, defaults to no path + pathToFormatterConfig: "./biome.json", // Callback function that is called when the script is generating the icon name // This is useful if you want to modify the icon name before it is written to the file iconNameTransformer: (iconName) => iconName @@ -47,6 +51,34 @@ export default { }; ``` +You can also pass an array of configs to the plugin to generate multiple spritesheets and types files at the same time (and watch those folders for changes). +```javascript +// vite.config.js +import { iconsSpritesheet } from 'vite-plugin-icons-spritesheet'; + +export default { + plugins: [ + iconsSpritesheet([ + { + withTypes: true, + inputDir: "icons/subset1", + outputDir: "public/icons1", + typesOutputFile: "app/icons1.ts", + fileName: "icon1.svg", + }, + { + withTypes: true, + inputDir: "icons/subset2", + outputDir: "public/icons2", + typesOutputFile: "app/icons2.ts", + fileName: "icon2.svg", + }, + ]), + ], +}; +``` + + Example component file: ```jsx diff --git a/package-lock.json b/package-lock.json index 81db8dc..75da368 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,24 @@ { "name": "vite-plugin-icons-spritesheet", - "version": "1.3.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vite-plugin-icons-spritesheet", - "version": "1.3.0", - "license": "ISC", + "version": "2.0.0", + "license": "MIT", "workspaces": [ ".", "test-apps/*" ], "dependencies": { + "@biomejs/js-api": "^0.6.2", + "@biomejs/wasm-nodejs": "^1.8.3", "chalk": "^4.1.2", "glob": "^10.3.12", - "node-html-parser": "^6.1.13" + "node-html-parser": "^6.1.13", + "prettier": "^3.3.3" }, "devDependencies": { "@types/node": "^20.12.7", @@ -692,6 +695,32 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@biomejs/js-api": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@biomejs/js-api/-/js-api-0.6.2.tgz", + "integrity": "sha512-4IhoDIL/O6ifUT2i/lDqXBGTuQpwurQ2i81440L5N4B90bYo9boHdtcK4ZjliZmN31ST0TDh6s9aiZEAGi+0og==", + "peerDependencies": { + "@biomejs/wasm-bundler": "^1.8.2", + "@biomejs/wasm-nodejs": "^1.8.2", + "@biomejs/wasm-web": "^1.8.2" + }, + "peerDependenciesMeta": { + "@biomejs/wasm-bundler": { + "optional": true + }, + "@biomejs/wasm-nodejs": { + "optional": true + }, + "@biomejs/wasm-web": { + "optional": true + } + } + }, + "node_modules/@biomejs/wasm-nodejs": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/wasm-nodejs/-/wasm-nodejs-1.8.3.tgz", + "integrity": "sha512-4E7qOBjbQDHKG2MwEXM6DzacQqibGhPQSCSgzziP7enpmJYsZbvW1mYUTlOHio1/OgadyixQpZ/gZjj2IEeISg==" + }, "node_modules/@emotion/hash": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", @@ -1993,6 +2022,21 @@ "node": ">=0.10" } }, + "node_modules/@remix-run/dev/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/@remix-run/express": { "version": "2.9.1", "resolved": "https://registry.npmjs.org/@remix-run/express/-/express-2.9.1.tgz", @@ -9388,15 +9432,14 @@ } }, "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true, + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" diff --git a/package.json b/package.json index 1d8caa6..58441c0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vite-plugin-icons-spritesheet", - "version": "2.0.0", - "description": "Vite plugin that generates a spritesheet out of your icons.", + "version": "2.1.0", + "description": "Vite plugin that generates a spritesheet and types out of your icons folder.", "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.mts", @@ -10,7 +10,12 @@ "spritesheet", "vite", "plugin", - "generator" + "generator", + "react", + "angular", + "vue", + "nextjs", + "remix" ], "exports": { ".": { @@ -63,9 +68,12 @@ ], "homepage": "https://github.com/forge42dev/vite-plugin-icons-spritesheet#readme", "dependencies": { + "@biomejs/js-api": "^0.6.2", + "@biomejs/wasm-nodejs": "^1.8.3", "chalk": "^4.1.2", "glob": "^10.3.12", - "node-html-parser": "^6.1.13" + "node-html-parser": "^6.1.13", + "prettier": "^3.3.3" }, "peerDependencies": { "vite": ">=5.2.0" @@ -77,12 +85,12 @@ "@vitest/coverage-v8": "^1.5.2", "eslint": "8.56", "eslint-plugin-unused-imports": "^3.1.0", + "happy-dom": "^14.7.1", + "husky": "^9.0.11", "npm-run-all": "^4.1.5", "tsup": "^8.0.2", "typescript": "^5.4.5", - "happy-dom": "^14.7.1", - "husky": "^9.0.11", - "vitest": "^1.5.2", - "vite": "5.2.11" + "vite": "5.2.11", + "vitest": "^1.5.2" } -} +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 04d26c0..ade8a28 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,14 +7,60 @@ import chalk from "chalk"; import type { Plugin } from "vite"; import { normalizePath } from "vite"; import { mkdir } from "node:fs/promises"; +import { Biome, Distribution } from "@biomejs/js-api"; +import * as prettier from "prettier"; + +type Formatter = "biome" | "prettier"; interface PluginProps { + /** + * Should the plugin generate TypeScript types for the icon names + * @default false + */ withTypes?: boolean; + /** + * The path to the icon directory + * @example "./icons" + */ inputDir: string; + /** + * Output path for the generated + * @example "./public/icons" + */ outputDir: string; + /** + * Output path for the generated type file + * @example "./app/types.ts" + */ typesOutputFile?: string; + /** + * The name of the generated spritesheet + * @default sprite.svg + * @example "icon.svg" + */ fileName?: string; + /** + * What formatter to use to format the generated files. Can be "biome" or "prettier" + * @default no formatter + * @example "biome" + */ + formatter?: Formatter; + /** + * The path to the formatter config file + * @default no path + * @example "./biome.json" + */ + pathToFormatterConfig?: string; + /** + * The cwd, defaults to process.cwd() + * @default process.cwd() + */ cwd?: string; + /** + * Callback function that is called when the script is generating the icon name + * This is useful if you want to modify the icon name before it is written to the file + * @example (iconName) => iconName.replace("potato", "mash-em,boil-em,put-em-in-a-stew") + */ iconNameTransformer?: (fileName: string) => string; } @@ -24,6 +70,8 @@ const generateIcons = async ({ outputDir, typesOutputFile = `${outputDir}/types.ts`, cwd, + formatter, + pathToFormatterConfig, fileName = "sprite.svg", iconNameTransformer, }: PluginProps) => { @@ -46,6 +94,8 @@ const generateIcons = async ({ outputPath: path.join(outputDir, fileName), outputDirRelative, iconNameTransformer, + formatter, + pathToFormatterConfig, }); if (withTypes) { @@ -57,6 +107,8 @@ const generateIcons = async ({ await generateTypes({ names: files.map((file: string) => transformIconName(file, iconNameTransformer ?? fileNameToCamelCase)), outputPath: path.join(typesOutputDir, typesFile), + formatter, + pathToFormatterConfig, }); } }; @@ -80,12 +132,26 @@ async function generateSvgSprite({ outputPath, outputDirRelative, iconNameTransformer, + formatter, + pathToFormatterConfig, }: { files: string[]; inputDir: string; outputPath: string; outputDirRelative?: string; iconNameTransformer?: (fileName: string) => string; + /** + * What formatter to use to format the generated files. Can be "biome" or "prettier" + * @default no formatter + * @example "biome" + */ + formatter?: Formatter; + /** + * The path to the formatter config file + * @default no path + * @example "./biome.json" + */ + pathToFormatterConfig?: string; }) { // Each SVG becomes a symbol and we wrap them all in a single SVG const symbols = await Promise.all( @@ -117,11 +183,67 @@ async function generateSvgSprite({ "", "", ].join("\n"); + const formattedOutput = await lintFileContent(output, formatter, pathToFormatterConfig, "svg"); + + return writeIfChanged( + outputPath, + formattedOutput, + `🖼️ Generated SVG spritesheet in ${chalk.green(outputDirRelative)}` + ); +} + +async function tryReadFile(path: string): Promise { + return fs.readFile(path, "utf8").catch(() => undefined); +} + +function tryParseJson(json: string | undefined): Record | undefined { + if (!json) { + return undefined; + } + try { + const data = JSON.parse(json); + return data; + } catch (e) { + return undefined; + } +} + +async function lintFileContent( + fileContent: string, + formatter: Formatter | undefined, + pathToFormatterConfig: string | undefined, + typeOfFile: "ts" | "svg" +) { + if (!formatter) { + return fileContent; + } + const formatterConfig = pathToFormatterConfig ? await tryReadFile(pathToFormatterConfig) : undefined; + const formatterConfigJson = tryParseJson(formatterConfig); + // TODO biome formatter for svg (atm it doesn't work) + if (formatter === "biome" && typeOfFile === "ts") { + const biome = await Biome.create({ + distribution: Distribution.NODE, + }); + if (formatterConfigJson) { + biome.applyConfiguration(formatterConfigJson); + } + return biome.formatContent(fileContent, { + filePath: "temp.ts", + }).content; + } - return writeIfChanged(outputPath, output, `🖼️ Generated SVG spritesheet in ${chalk.green(outputDirRelative)}`); + return prettier.format(fileContent, { + parser: typeOfFile === "ts" ? "typescript" : "html", + ...formatterConfigJson, + }); } -async function generateTypes({ names, outputPath }: { names: string[]; outputPath: string }) { +async function generateTypes({ + names, + outputPath, + formatter, + pathToFormatterConfig, +}: { names: string[]; outputPath: string } & Pick) { const output = [ "// This file is generated by icon spritesheet generator", "", @@ -133,10 +255,11 @@ async function generateTypes({ names, outputPath }: { names: string[]; outputPat "export type IconName = typeof iconNames[number]", "", ].join("\n"); + const formattedOutput = await lintFileContent(output, formatter, pathToFormatterConfig, "ts"); const file = await writeIfChanged( outputPath, - output, + formattedOutput, `${chalk.blueBright("TS")} Generated icon types in ${chalk.green(outputPath)}` ); return file; @@ -161,41 +284,50 @@ async function writeIfChanged(filepath: string, newContent: string, message: str } // biome-ignore lint/suspicious/noExplicitAny: -export const iconsSpritesheet: (args: PluginProps) => any = ({ - withTypes, - inputDir, - outputDir, - typesOutputFile, - fileName, - cwd, - iconNameTransformer, -}) => { - const iconGenerator = async () => - generateIcons({ +export const iconsSpritesheet: (args: PluginProps | PluginProps[]) => any = (maybeConfigs) => { + const configs = Array.isArray(maybeConfigs) ? maybeConfigs : [maybeConfigs]; + + return configs.map((config, i) => { + const { withTypes, inputDir, outputDir, typesOutputFile, fileName, + cwd, iconNameTransformer, - }); - return { - name: "icon-spritesheet-generator", - async buildStart() { - await iconGenerator(); - }, - async watchChange(file, type) { - const inputPath = normalizePath(path.join(cwd ?? process.cwd(), inputDir)); - if (file.includes(inputPath) && file.endsWith(".svg") && ["create", "delete"].includes(type.event)) { + formatter, + pathToFormatterConfig, + } = config; + const iconGenerator = async () => + generateIcons({ + withTypes, + inputDir, + outputDir, + typesOutputFile, + fileName, + iconNameTransformer, + formatter, + pathToFormatterConfig, + }); + return { + name: `icon-spritesheet-generator${i > 0 ? i.toString() : ""}`, + async buildStart() { await iconGenerator(); - } - }, - async handleHotUpdate({ file }) { - const inputPath = normalizePath(path.join(cwd ?? process.cwd(), inputDir)); - if (file.includes(inputPath) && file.endsWith(".svg")) { - await iconGenerator(); - } - }, - // biome-ignore lint/suspicious/noExplicitAny: - } satisfies Plugin; + }, + async watchChange(file, type) { + const inputPath = normalizePath(path.join(cwd ?? process.cwd(), inputDir)); + if (file.includes(inputPath) && file.endsWith(".svg") && ["create", "delete"].includes(type.event)) { + await iconGenerator(); + } + }, + async handleHotUpdate({ file }) { + const inputPath = normalizePath(path.join(cwd ?? process.cwd(), inputDir)); + if (file.includes(inputPath) && file.endsWith(".svg")) { + await iconGenerator(); + } + }, + // biome-ignore lint/suspicious/noExplicitAny: + } satisfies Plugin; + }); }; diff --git a/test-apps/remix-vite-cjs/icons/de.svg b/test-apps/remix-vite-cjs/icons/de.svg new file mode 100644 index 0000000..841eb20 --- /dev/null +++ b/test-apps/remix-vite-cjs/icons/de.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test-apps/remix-vite-cjs/public/icons/sprite.svg b/test-apps/remix-vite-cjs/public/icons/sprite.svg new file mode 100644 index 0000000..834f919 --- /dev/null +++ b/test-apps/remix-vite-cjs/public/icons/sprite.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/test-apps/remix-vite-cjs/public/icons/types.ts b/test-apps/remix-vite-cjs/public/icons/types.ts new file mode 100644 index 0000000..6a9afe6 --- /dev/null +++ b/test-apps/remix-vite-cjs/public/icons/types.ts @@ -0,0 +1,7 @@ +// This file is generated by icon spritesheet generator + +export const iconNames = [ + "De", +] as const + +export type IconName = typeof iconNames[number] diff --git a/test-apps/remix-vite/app/icons/sprite.svg b/test-apps/remix-vite/app/icons/sprite.svg index 0f8e9ae..adc64f7 100644 --- a/test-apps/remix-vite/app/icons/sprite.svg +++ b/test-apps/remix-vite/app/icons/sprite.svg @@ -1,12 +1,20 @@ - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + diff --git a/test-apps/remix-vite/app/icons/types.ts b/test-apps/remix-vite/app/icons/types.ts index 7143627..861f709 100644 --- a/test-apps/remix-vite/app/icons/types.ts +++ b/test-apps/remix-vite/app/icons/types.ts @@ -1,11 +1,5 @@ // This file is generated by icon spritesheet generator -export const iconNames = [ - "Test", - "De", - "C", - "B", - "A", -] as const +export const iconNames = ["Test", "Test copy", "De", "C", "B", "A"] as const; -export type IconName = typeof iconNames[number] +export type IconName = (typeof iconNames)[number]; diff --git a/test-apps/remix-vite/biome.json b/test-apps/remix-vite/biome.json new file mode 100644 index 0000000..1d0b8f5 --- /dev/null +++ b/test-apps/remix-vite/biome.json @@ -0,0 +1,55 @@ +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "defaultBranch": "main", + "useIgnoreFile": true + }, + "formatter": { + "enabled": true, + "formatWithErrors": false, + "indentStyle": "tab", + "lineEnding": "lf", + "lineWidth": 120 + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "recommended": true + }, + "style": { + "recommended": true + }, + "complexity": { + "recommended": true + }, + "security": { + "recommended": true + }, + "performance": { + "recommended": true + }, + "correctness": { + "recommended": true + }, + "a11y": { + "recommended": true + }, + "nursery": { + "recommended": true + } + } + }, + "javascript": { + "formatter": { + "semicolons": "asNeeded", + "trailingCommas": "es5" + } + } +} \ No newline at end of file diff --git a/test-apps/remix-vite/icons/test copy.svg b/test-apps/remix-vite/icons/test copy.svg new file mode 100644 index 0000000..7bbc380 --- /dev/null +++ b/test-apps/remix-vite/icons/test copy.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/test-apps/remix-vite/public/icons/sprite.svg b/test-apps/remix-vite/public/icons/sprite.svg new file mode 100644 index 0000000..adc64f7 --- /dev/null +++ b/test-apps/remix-vite/public/icons/sprite.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + diff --git a/test-apps/remix-vite/public/icons/types.ts b/test-apps/remix-vite/public/icons/types.ts new file mode 100644 index 0000000..861f709 --- /dev/null +++ b/test-apps/remix-vite/public/icons/types.ts @@ -0,0 +1,5 @@ +// This file is generated by icon spritesheet generator + +export const iconNames = ["Test", "Test copy", "De", "C", "B", "A"] as const; + +export type IconName = (typeof iconNames)[number]; diff --git a/test-apps/remix-vite/vite.config.ts b/test-apps/remix-vite/vite.config.ts index 49133a0..e810a01 100644 --- a/test-apps/remix-vite/vite.config.ts +++ b/test-apps/remix-vite/vite.config.ts @@ -9,11 +9,22 @@ export default defineConfig({ plugins: [ remix(), tsconfigPaths(), - iconsSpritesheet({ - withTypes: true, - inputDir: "icons", - outputDir: "./app/icons", - }), + iconsSpritesheet([ + { + withTypes: true, + inputDir: "icons", + outputDir: "./app/icons", + formatter: "prettier", + // pathToFormatterConfig: "./biome.json", + }, + { + withTypes: true, + inputDir: "icons", + outputDir: "./public/icons", + formatter: "biome", + // pathToFormatterConfig: "./biome.json", + } + ]), ], build: { assetsInlineLimit: 0,