vectify 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,6 +20,7 @@ English | [简体中文](./README.zh-CN.md)
20
20
  - [Configuration](#configuration)
21
21
  - [Basic Options](#basic-options)
22
22
  - [Generation Options](#generation-options)
23
+ - [Auto Formatting](#auto-formatting)
23
24
  - [Watch Mode](#watch-mode)
24
25
  - [SVGO Configuration](#svgo-configuration)
25
26
  - [Lifecycle Hooks](#lifecycle-hooks)
@@ -185,6 +186,7 @@ All available options for `defineConfig()` are documented in the tables below.
185
186
  | `prefix` | `string` | `''` | ❌ | Prefix added to all component names. Useful for namespacing. | `prefix: 'Icon'` → `IconArrowRight` |
186
187
  | `suffix` | `string` | `''` | ❌ | Suffix added to all component names. | `suffix: 'Icon'` → `ArrowRightIcon` |
187
188
  | `transform` | `(name: string) => string` | - | ❌ | Custom function to transform SVG filename to component name. Overrides default PascalCase conversion and prefix/suffix. | `transform: (n) => 'X' + n` |
189
+ | `format` | `boolean` \| `'prettier'` \| `'eslint'` \| `'biome'` \| `FormatConfig` | `false` | ❌ | Auto-format generated files after generation. See [Auto Formatting](#auto-formatting) for details. | `format: true` |
188
190
 
189
191
  #### `generateOptions` Object
190
192
 
@@ -195,6 +197,71 @@ All available options for `defineConfig()` are documented in the tables below.
195
197
  | `preview` | `boolean` | `false` | Generate interactive `preview.html` for browsing all icons locally. Useful for design review. | `preview: true` |
196
198
  | `cleanOutput` | `boolean` | `false` | Remove orphaned component files that no longer have corresponding SVG files. Helps keep output directory clean. | `cleanOutput: true` |
197
199
 
200
+ #### Auto Formatting
201
+
202
+ Vectify can automatically format generated files using your project's formatter. This ensures generated code matches your project's code style.
203
+
204
+ **Quick Start:**
205
+
206
+ ```typescript
207
+ export default defineConfig({
208
+ framework: 'react',
209
+ input: './icons',
210
+ output: './src/icons',
211
+ format: true, // Auto-detect and use project formatter
212
+ })
213
+ ```
214
+
215
+ **Format Options:**
216
+
217
+ | Value | Description |
218
+ |-------|-------------|
219
+ | `false` | Disable formatting (default) |
220
+ | `true` | Auto-detect formatter (biome > prettier > eslint) |
221
+ | `'prettier'` | Use Prettier |
222
+ | `'eslint'` | Use ESLint --fix |
223
+ | `'biome'` | Use Biome |
224
+ | `{ tool, args }` | Full configuration object |
225
+
226
+ **Auto-Detection Priority:**
227
+
228
+ When `format: true`, Vectify looks for config files in this order:
229
+ 1. `biome.json` / `biome.jsonc` → Uses Biome
230
+ 2. `.prettierrc*` / `prettier.config.*` → Uses Prettier
231
+ 3. `eslint.config.*` / `.eslintrc*` → Uses ESLint
232
+
233
+ **Full Configuration:**
234
+
235
+ ```typescript
236
+ export default defineConfig({
237
+ format: {
238
+ tool: 'prettier', // 'auto' | 'prettier' | 'eslint' | 'biome'
239
+ args: '--single-quote', // Additional CLI arguments
240
+ },
241
+ })
242
+ ```
243
+
244
+ **Examples:**
245
+
246
+ ```typescript
247
+ // Auto-detect formatter
248
+ format: true
249
+
250
+ // Use specific formatter
251
+ format: 'prettier'
252
+ format: 'eslint'
253
+ format: 'biome'
254
+
255
+ // With custom arguments
256
+ format: {
257
+ tool: 'prettier',
258
+ args: '--tab-width 4',
259
+ }
260
+
261
+ // Disable formatting
262
+ format: false
263
+ ```
264
+
198
265
  #### `watch` Object
199
266
 
200
267
  | Parameter | Type | Default | Description | Example |
package/README.zh-CN.md CHANGED
@@ -20,6 +20,7 @@
20
20
  - [配置选项](#配置选项)
21
21
  - [基础配置](#基础配置)
22
22
  - [生成选项](#生成选项)
23
+ - [自动格式化](#自动格式化)
23
24
  - [监听模式](#监听模式)
24
25
  - [SVGO 配置](#svgo-配置)
25
26
  - [生命周期钩子](#生命周期钩子)
@@ -185,6 +186,7 @@ npx vectify watch [选项]
185
186
  | `prefix` | `string` | `''` | ❌ | 添加到所有组件名称前的前缀。用于命名空间 | `prefix: 'Icon'` → `IconArrowRight` |
186
187
  | `suffix` | `string` | `''` | ❌ | 添加到所有组件名称后的后缀 | `suffix: 'Icon'` → `ArrowRightIcon` |
187
188
  | `transform` | `(name: string) => string` | - | ❌ | 自定义函数,将 SVG 文件名转换为组件名。覆盖默认的 PascalCase 转换和 prefix/suffix | `transform: (n) => 'X' + n` |
189
+ | `format` | `boolean` \| `'prettier'` \| `'eslint'` \| `'biome'` \| `FormatConfig` | `false` | ❌ | 生成后自动格式化文件。详见 [自动格式化](#自动格式化) | `format: true` |
188
190
 
189
191
  #### `generateOptions` 对象
190
192
 
@@ -195,6 +197,71 @@ npx vectify watch [选项]
195
197
  | `preview` | `boolean` | `false` | 生成交互式 `preview.html` 用于本地浏览所有图标。适合设计审查 | `preview: true` |
196
198
  | `cleanOutput` | `boolean` | `false` | 移除不再有对应 SVG 文件的孤立组件。帮助保持输出目录整洁 | `cleanOutput: true` |
197
199
 
200
+ #### 自动格式化
201
+
202
+ Vectify 可以使用项目中的格式化工具自动格式化生成的文件。确保生成的代码符合项目的代码风格。
203
+
204
+ **快速开始:**
205
+
206
+ ```typescript
207
+ export default defineConfig({
208
+ framework: 'react',
209
+ input: './icons',
210
+ output: './src/icons',
211
+ format: true, // 自动检测并使用项目格式化工具
212
+ })
213
+ ```
214
+
215
+ **格式化选项:**
216
+
217
+ | 值 | 说明 |
218
+ |----|------|
219
+ | `false` | 禁用格式化(默认) |
220
+ | `true` | 自动检测格式化工具(biome > prettier > eslint) |
221
+ | `'prettier'` | 使用 Prettier |
222
+ | `'eslint'` | 使用 ESLint --fix |
223
+ | `'biome'` | 使用 Biome |
224
+ | `{ tool, args }` | 完整配置对象 |
225
+
226
+ **自动检测优先级:**
227
+
228
+ 当 `format: true` 时,Vectify 按以下顺序查找配置文件:
229
+ 1. `biome.json` / `biome.jsonc` → 使用 Biome
230
+ 2. `.prettierrc*` / `prettier.config.*` → 使用 Prettier
231
+ 3. `eslint.config.*` / `.eslintrc*` → 使用 ESLint
232
+
233
+ **完整配置:**
234
+
235
+ ```typescript
236
+ export default defineConfig({
237
+ format: {
238
+ tool: 'prettier', // 'auto' | 'prettier' | 'eslint' | 'biome'
239
+ args: '--single-quote', // 额外的 CLI 参数
240
+ },
241
+ })
242
+ ```
243
+
244
+ **示例:**
245
+
246
+ ```typescript
247
+ // 自动检测格式化工具
248
+ format: true
249
+
250
+ // 使用指定的格式化工具
251
+ format: 'prettier'
252
+ format: 'eslint'
253
+ format: 'biome'
254
+
255
+ // 带自定义参数
256
+ format: {
257
+ tool: 'prettier',
258
+ args: '--tab-width 4',
259
+ }
260
+
261
+ // 禁用格式化
262
+ format: false
263
+ ```
264
+
198
265
  #### `watch` 对象
199
266
 
200
267
  | 参数 | 类型 | 默认值 | 说明 | 示例 |
@@ -639,7 +639,7 @@ async function findConfig() {
639
639
  }
640
640
 
641
641
  // src/generators/index.ts
642
- import path3 from "path";
642
+ import path4 from "path";
643
643
 
644
644
  // src/parsers/optimizer.ts
645
645
  import { optimize } from "svgo";
@@ -673,6 +673,100 @@ async function optimizeSvg(svgContent, config) {
673
673
  }
674
674
  }
675
675
 
676
+ // src/utils/formatter.ts
677
+ import { exec } from "child_process";
678
+ import path3 from "path";
679
+ import process3 from "process";
680
+ import { promisify } from "util";
681
+ var execAsync = promisify(exec);
682
+ var FORMATTER_PATTERNS = {
683
+ biome: ["biome.json", "biome.jsonc"],
684
+ prettier: [
685
+ ".prettierrc",
686
+ ".prettierrc.json",
687
+ ".prettierrc.yml",
688
+ ".prettierrc.yaml",
689
+ ".prettierrc.js",
690
+ ".prettierrc.cjs",
691
+ ".prettierrc.mjs",
692
+ "prettier.config.js",
693
+ "prettier.config.cjs",
694
+ "prettier.config.mjs"
695
+ ],
696
+ eslint: [
697
+ "eslint.config.js",
698
+ "eslint.config.mjs",
699
+ "eslint.config.cjs",
700
+ "eslint.config.ts",
701
+ ".eslintrc",
702
+ ".eslintrc.js",
703
+ ".eslintrc.cjs",
704
+ ".eslintrc.json",
705
+ ".eslintrc.yml",
706
+ ".eslintrc.yaml"
707
+ ]
708
+ };
709
+ var FORMATTER_COMMANDS = {
710
+ biome: (outputDir, args) => `npx @biomejs/biome format --write ${args || ""} "${outputDir}"`.trim(),
711
+ prettier: (outputDir, args) => `npx prettier --write ${args || ""} "${outputDir}"`.trim(),
712
+ eslint: (outputDir, args) => `npx eslint --fix ${args || ""} "${outputDir}"`.trim()
713
+ };
714
+ function normalizeFormatOption(format) {
715
+ if (format === false) {
716
+ return null;
717
+ }
718
+ if (format === true) {
719
+ return { tool: "auto" };
720
+ }
721
+ if (typeof format === "string") {
722
+ return { tool: format };
723
+ }
724
+ return format;
725
+ }
726
+ async function detectFormatter() {
727
+ const cwd = process3.cwd();
728
+ const priority = ["biome", "prettier", "eslint"];
729
+ for (const tool of priority) {
730
+ const patterns = FORMATTER_PATTERNS[tool];
731
+ for (const pattern of patterns) {
732
+ const configPath = path3.join(cwd, pattern);
733
+ if (await fileExists(configPath)) {
734
+ return tool;
735
+ }
736
+ }
737
+ }
738
+ return null;
739
+ }
740
+ async function formatOutput(outputDir, format) {
741
+ const config = normalizeFormatOption(format);
742
+ if (!config) {
743
+ return { success: true };
744
+ }
745
+ let tool = null;
746
+ if (config.tool === "auto") {
747
+ tool = await detectFormatter();
748
+ if (!tool) {
749
+ return {
750
+ success: true,
751
+ error: "No formatter detected. Install prettier, eslint, or biome to enable auto-formatting."
752
+ };
753
+ }
754
+ } else {
755
+ tool = config.tool || "prettier";
756
+ }
757
+ const command = FORMATTER_COMMANDS[tool](outputDir, config.args);
758
+ try {
759
+ await execAsync(command, { cwd: process3.cwd() });
760
+ return { success: true, tool };
761
+ } catch (error) {
762
+ return {
763
+ success: false,
764
+ tool,
765
+ error: `Format failed with ${tool}: ${error.message}`
766
+ };
767
+ }
768
+ }
769
+
676
770
  // src/generators/index.ts
677
771
  async function generateIcons(config, dryRun = false) {
678
772
  const stats = {
@@ -714,6 +808,14 @@ async function generateIcons(config, dryRun = false) {
714
808
  if (config.generateOptions?.preview && !dryRun) {
715
809
  await generatePreviewHtml(svgFiles, config);
716
810
  }
811
+ if (config.format && !dryRun) {
812
+ const formatResult = await formatOutput(config.output, config.format);
813
+ if (formatResult.success && formatResult.tool) {
814
+ console.log(`Formatted with ${formatResult.tool}`);
815
+ } else if (formatResult.error) {
816
+ console.warn(formatResult.error);
817
+ }
818
+ }
717
819
  if (config.hooks?.onComplete) {
718
820
  await config.hooks.onComplete(stats);
719
821
  }
@@ -724,7 +826,7 @@ async function generateIcons(config, dryRun = false) {
724
826
  }
725
827
  async function generateIconComponent(svgFile, config, dryRun = false) {
726
828
  let svgContent = await readFile(svgFile);
727
- const fileName = path3.basename(svgFile);
829
+ const fileName = path4.basename(svgFile);
728
830
  if (config.hooks?.beforeParse) {
729
831
  svgContent = await config.hooks.beforeParse(svgContent, fileName);
730
832
  }
@@ -748,7 +850,7 @@ async function generateIconComponent(svgFile, config, dryRun = false) {
748
850
  code = await config.hooks.afterGenerate(code, componentName);
749
851
  }
750
852
  const fileExt = strategy.getComponentExtension(typescript);
751
- const outputPath = path3.join(config.output, `${componentName}.${fileExt}`);
853
+ const outputPath = path4.join(config.output, `${componentName}.${fileExt}`);
752
854
  if (dryRun) {
753
855
  console.log(` ${componentName}.${fileExt}`);
754
856
  } else {
@@ -759,7 +861,7 @@ async function generateBaseComponent(config, dryRun = false) {
759
861
  const typescript = config.typescript ?? true;
760
862
  const strategy = getFrameworkStrategy(config.framework);
761
863
  const { code, fileName } = strategy.generateBaseComponent(typescript);
762
- const outputPath = path3.join(config.output, fileName);
864
+ const outputPath = path4.join(config.output, fileName);
763
865
  if (dryRun) {
764
866
  console.log(` ${fileName}`);
765
867
  } else {
@@ -770,9 +872,9 @@ async function generateIndexFile(svgFiles, config, dryRun = false) {
770
872
  const typescript = config.typescript ?? true;
771
873
  const strategy = getFrameworkStrategy(config.framework);
772
874
  const ext = strategy.getIndexExtension(typescript);
773
- const usesDefaultExport = config.framework === "vue" || config.framework === "svelte" || config.framework === "preact";
875
+ const usesDefaultExport = ["vue", "svelte", "react", "preact"].includes(config.framework);
774
876
  const exports = svgFiles.map((svgFile) => {
775
- const fileName = path3.basename(svgFile);
877
+ const fileName = path4.basename(svgFile);
776
878
  const componentName = getComponentName(
777
879
  fileName,
778
880
  config.prefix,
@@ -785,7 +887,7 @@ async function generateIndexFile(svgFiles, config, dryRun = false) {
785
887
  return `export { ${componentName} } from './${componentName}'`;
786
888
  }
787
889
  }).join("\n");
788
- const indexPath = path3.join(config.output, `index.${ext}`);
890
+ const indexPath = path4.join(config.output, `index.${ext}`);
789
891
  if (dryRun) {
790
892
  console.log(` index.${ext}`);
791
893
  } else {
@@ -795,7 +897,7 @@ async function generateIndexFile(svgFiles, config, dryRun = false) {
795
897
  }
796
898
  async function generatePreviewHtml(svgFiles, config) {
797
899
  const componentNames = svgFiles.map((svgFile) => {
798
- const fileName = path3.basename(svgFile);
900
+ const fileName = path4.basename(svgFile);
799
901
  return getComponentName(
800
902
  fileName,
801
903
  config.prefix,
@@ -967,7 +1069,7 @@ async function generatePreviewHtml(svgFiles, config) {
967
1069
  </script>
968
1070
  </body>
969
1071
  </html>`;
970
- const previewPath = path3.join(config.output, "preview.html");
1072
+ const previewPath = path4.join(config.output, "preview.html");
971
1073
  await writeFile(previewPath, html);
972
1074
  }
973
1075
  async function cleanOutputDirectory(svgFiles, config) {
@@ -976,7 +1078,7 @@ async function cleanOutputDirectory(svgFiles, config) {
976
1078
  const fileExt = strategy.getComponentExtension(config.typescript ?? true);
977
1079
  const expectedComponents = new Set(
978
1080
  svgFiles.map((svgFile) => {
979
- const fileName = path3.basename(svgFile, ".svg");
1081
+ const fileName = path4.basename(svgFile, ".svg");
980
1082
  const componentName = getComponentName(
981
1083
  fileName,
982
1084
  config.prefix,
@@ -1003,7 +1105,7 @@ async function cleanOutputDirectory(svgFiles, config) {
1003
1105
  continue;
1004
1106
  }
1005
1107
  if (!expectedComponents.has(file)) {
1006
- const filePath = path3.join(config.output, file);
1108
+ const filePath = path4.join(config.output, file);
1007
1109
  await unlink(filePath);
1008
1110
  console.log(`Deleted orphaned component: ${file}`);
1009
1111
  }
@@ -1072,15 +1174,15 @@ ${chalk.bold("Output:")} ${chalk.cyan(config.output)}`);
1072
1174
  }
1073
1175
 
1074
1176
  // src/commands/init.ts
1075
- import path4 from "path";
1076
- import process3 from "process";
1177
+ import path5 from "path";
1178
+ import process4 from "process";
1077
1179
  import chalk2 from "chalk";
1078
1180
  import inquirer from "inquirer";
1079
1181
  import ora2 from "ora";
1080
1182
  async function init(options = {}) {
1081
1183
  try {
1082
1184
  const projectRoot = await findProjectRoot();
1083
- const currentDir = process3.cwd();
1185
+ const currentDir = process4.cwd();
1084
1186
  if (currentDir !== projectRoot) {
1085
1187
  console.log(chalk2.yellow(`
1086
1188
  Note: Project root detected at ${chalk2.cyan(projectRoot)}`));
@@ -1101,8 +1203,8 @@ Note: Project root detected at ${chalk2.cyan(projectRoot)}`));
1101
1203
  }
1102
1204
  }
1103
1205
  ]);
1104
- const configPath = path4.resolve(projectRoot, pathAnswers.configPath);
1105
- const configDir = path4.dirname(configPath);
1206
+ const configPath = path5.resolve(projectRoot, pathAnswers.configPath);
1207
+ const configDir = path5.dirname(configPath);
1106
1208
  if (!options.force && await fileExists(configPath)) {
1107
1209
  const { overwrite } = await inquirer.prompt([
1108
1210
  {
@@ -1166,14 +1268,14 @@ Note: Project root detected at ${chalk2.cyan(projectRoot)}`));
1166
1268
  default: ""
1167
1269
  }
1168
1270
  ]);
1169
- const inputPath = path4.resolve(projectRoot, answers.input);
1170
- const outputPath = path4.resolve(projectRoot, answers.output);
1271
+ const inputPath = path5.resolve(projectRoot, answers.input);
1272
+ const outputPath = path5.resolve(projectRoot, answers.output);
1171
1273
  const spinner = ora2("Setting up directories...").start();
1172
1274
  await ensureDir(inputPath);
1173
1275
  spinner.text = `Created input directory: ${chalk2.cyan(answers.input)}`;
1174
1276
  await ensureDir(outputPath);
1175
1277
  spinner.succeed(`Created output directory: ${chalk2.cyan(answers.output)}`);
1176
- const relativeConfigDir = path4.relative(configDir, projectRoot) || ".";
1278
+ const relativeConfigDir = path5.relative(configDir, projectRoot) || ".";
1177
1279
  const configContent = generateConfigContent(answers, relativeConfigDir);
1178
1280
  spinner.start("Creating config file...");
1179
1281
  await writeFile(configPath, configContent);
@@ -1216,7 +1318,7 @@ export default defineConfig({
1216
1318
  }
1217
1319
 
1218
1320
  // src/commands/watch.ts
1219
- import path5 from "path";
1321
+ import path6 from "path";
1220
1322
  import chalk3 from "chalk";
1221
1323
  import chokidar from "chokidar";
1222
1324
  import ora3 from "ora";
@@ -1238,7 +1340,7 @@ async function watch(options = {}) {
1238
1340
  spinner.start("Generating icon components...");
1239
1341
  const initialStats = await generateIcons(config);
1240
1342
  spinner.succeed(`Generated ${chalk3.green(initialStats.success)} icon components`);
1241
- const watchPath = path5.join(config.input, "**/*.svg");
1343
+ const watchPath = path6.join(config.input, "**/*.svg");
1242
1344
  const debounce = config.watch?.debounce ?? 300;
1243
1345
  const ignore = config.watch?.ignore ?? ["**/node_modules/**", "**/.git/**"];
1244
1346
  console.log(chalk3.bold("\nWatching for changes..."));
@@ -1256,7 +1358,7 @@ async function watch(options = {}) {
1256
1358
  }).on("change", (filePath) => {
1257
1359
  handleChange("changed", filePath, config, debounce, debounceTimer);
1258
1360
  }).on("unlink", (filePath) => {
1259
- console.log(chalk3.yellow(`SVG file removed: ${path5.basename(filePath)}`));
1361
+ console.log(chalk3.yellow(`SVG file removed: ${path6.basename(filePath)}`));
1260
1362
  handleChange("removed", filePath, config, debounce, debounceTimer);
1261
1363
  }).on("error", (error) => {
1262
1364
  console.error(chalk3.red(`Watcher error: ${error.message}`));
@@ -1275,7 +1377,7 @@ ${chalk3.yellow("Stopping watch mode...")}`);
1275
1377
  }
1276
1378
  }
1277
1379
  function handleChange(event, filePath, config, debounce, timer) {
1278
- const fileName = path5.basename(filePath);
1380
+ const fileName = path6.basename(filePath);
1279
1381
  if (timer) {
1280
1382
  clearTimeout(timer);
1281
1383
  }