搭建node脚手架(二) 依赖冲突处理,生成项目配置文件

依赖冲突处理(conflict-resolve.ts)

操作步骤详解

  1. 依赖清理规则设定
// 精确匹配待删除依赖项
const packageNamesToRemove = [ ... ];
// 前缀匹配待删除依赖项  
const packagePrefixesToRemove = [ ... ];

// 示例说明:
// - 精确匹配项:eslint、prettier、husky 等
// - 前缀匹配项:eslint- 插件、@typescript-eslint/ 等

⚠️ 注意:这些依赖项若存在,可能与 PKG_NAME 内置工具产生冲突,需要移除。

配置文件检查

const checkUselessConfig = (cwd: string): string[] => {
  return [
    ...glob.sync('.eslintrc?...', { cwd }),
    ...glob.sync('.stylelintrc?...', { cwd }), 
    ...glob.sync('.markdownlint...', { cwd }),
    ...glob.sync('.prettierrc?...', { cwd }),
    ...glob.sync('tslint.@(yaml|yml|json)', { cwd }),
    ...glob.sync('.kylerc?...', { cwd })
  ];
};

🔍 功能说明:扫描项目目录,查找以下旧配置文件:

  • 各类 lint 配置(eslint/stylelint/prettier)
  • tslint 相关配置
  • 其他工具配置文件

需重写配置检查

const checkReWriteConfig = (cwd: string) => {
  return glob
    .sync('**/*.ejs', { cwd: path.resolve(__dirname, '../config') })
    .map(name => name.replace(/^_/, '.').replace(/\.ejs$/, ''))
    .filter(filename => fs.existsSync(path.resolve(cwd, filename)));
};

🔄 处理流程:

  1. 从模板目录查找 .ejs 配置文件
  2. 转换为实际配置文件名(如 _eslintrc.ejs → .eslintrc)
  3. 筛选出项目中已存在的配置文件

结果汇总

const willRemovePackage = dependencies.filter(...); // 待移除依赖
const uselessConfig = checkUselessConfig(cwd);     // 待删除配置 
const reWriteConfig = checkReWriteConfig(cwd);     // 待覆盖配置
const willChangeCount = willRemovePackage.length + uselessConfig.length + reWriteConfig.length;

用户确认环节

if (willChangeCount > 0) {
  log.warn(`检测到 ${PKG_NAME} 可能存在配置冲突...`);
  
  const { isOverWrite } = await inquirer.prompt({
    type: 'confirm',
    name: 'isOverWrite',
    message: '是否继续执行?'
  });
  
  if (!isOverWrite) process.exit(0);
}

📌 交互逻辑:

  • 展示待修改内容清单
  • 获取用户确认
  • 拒绝则终止流程

执行清理操作

// 删除旧配置文件
uselessConfig.forEach(name => {
  fs.removeSync(path.resolve(cwd, name));
});

// 清理 package.json
delete pkg.eslintConfig;
delete pkg.eslintIgnore; 
delete pkg.stylelint;

// 移除冲突依赖
willRemovePackage.forEach(name => {
  delete (pkg.dependencies || {})[name];
  delete (pkg.devDependencies || {})[name];
});

// 保存修改
fs.writeFileSync(path.resolve(cwd, 'package.json'), JSON.stringify(pkg, null, 2));

本流程用于确保环境清洁:

  1. 检测冲突配置和依赖
  2. 获取用户授权
  3. 执行清理操作
  4. 避免与新工具产生冲突

完整代码:

import path from 'path';
import fs from 'fs-extra';
import glob from 'glob';
import inquirer from 'inquirer';
import log from './log';
import { PKG_NAME } from './constants';
import type { PKG } from '../types';

// 精确移除依赖
const packageNamesToRemove = [
  '@babel/eslint-parser',
  '@commitlint/cli',
  '@iceworks/spec',
  'babel-eslint',
  'eslint',
  'husky',
  'markdownlint',
  'prettier',
  'stylelint',
  'tslint',
];

// 按前缀移除依赖
const packagePrefixesToRemove = [
  '@commitlint/',
  '@typescript-eslint/',
  'eslint-',
  'stylelint-',
  'markdownlint-',
  'commitlint-',
];

/**
 * 待删除的无用配置
 * @param cwd
 */
const checkUselessConfig = (cwd: string): string[] => {
  return []
    .concat(glob.sync('.eslintrc?(.@(yaml|yml|json))', { cwd }))
    .concat(glob.sync('.stylelintrc?(.@(yaml|yml|json))', { cwd }))
    .concat(glob.sync('.markdownlint@(rc|.@(yaml|yml|jsonc))', { cwd }))
    .concat(
      glob.sync('.prettierrc?(.@(cjs|config.js|config.cjs|yaml|yml|json|json5|toml))', { cwd }),
    )
    .concat(glob.sync('tslint.@(yaml|yml|json)', { cwd }))
    .concat(glob.sync('.kylerc?(.@(yaml|yml|json))', { cwd }));
};

/**
 * 待重写的配置
 * @param cwd
 */
const checkReWriteConfig = (cwd: string) => {
  return glob
    .sync('**/*.ejs', { cwd: path.resolve(__dirname, '../config') })
    .map((name) => name.replace(/^_/, '.').replace(/\.ejs$/, ''))
    .filter((filename) => fs.existsSync(path.resolve(cwd, filename)));
};

export default async (cwd: string, rewriteConfig?: boolean) => {
  const pkgPath = path.resolve(cwd, 'package.json');
  const pkg: PKG = fs.readJSONSync(pkgPath);
  const dependencies = [].concat(
    Object.keys(pkg.dependencies || {}),
    Object.keys(pkg.devDependencies || []),
  );
  const willRemovePackage = dependencies.filter(
    (name) =>
      packageNamesToRemove.includes(name) ||
      packagePrefixesToRemove.some((prefix) => name.startsWith(prefix)),
  );
  const uselessConfig = checkUselessConfig(cwd);
  const reWriteConfig = checkReWriteConfig(cwd);
  const willChangeCount = willRemovePackage.length + uselessConfig.length + reWriteConfig.length;

  // 提示是否移除原配置
  if (willChangeCount > 0) {
    log.warn(`检测到项目中存在可能与 ${PKG_NAME} 冲突的依赖和配置,为保证正常运行将`);

    if (willRemovePackage.length > 0) {
      log.warn('删除以下依赖:');
      log.warn(JSON.stringify(willRemovePackage, null, 2));
    }

    if (uselessConfig.length > 0) {
      log.warn('删除以下配置文件:');
      log.warn(JSON.stringify(uselessConfig, null, 2));
    }

    if (reWriteConfig.length > 0) {
      log.warn('覆盖以下配置文件:');
      log.warn(JSON.stringify(reWriteConfig, null, 2));
    }

    if (typeof rewriteConfig === 'undefined') {
      const { isOverWrite } = await inquirer.prompt({
        type: 'confirm',
        name: 'isOverWrite',
        message: '请确认是否继续:',
      });

      if (!isOverWrite) process.exit(0);
    } else if (!reWriteConfig) {
      process.exit(0);
    }
  }

  // 删除配置文件
  for (const name of uselessConfig) {
    fs.removeSync(path.resolve(cwd, name));
  }

  // 修正 package.json
  delete pkg.eslintConfig;
  delete pkg.eslintIgnore;
  delete pkg.stylelint;
  for (const name of willRemovePackage) {
    delete (pkg.dependencies || {})[name];
    delete (pkg.devDependencies || {})[name];
  }
  fs.writeFileSync(path.resolve(cwd, 'package.json'), JSON.stringify(pkg, null, 2), 'utf8');

  return pkg;
};

EJS 模板引擎

1️⃣ EJS 简介

EJS(Embedded JavaScript)是一种高效的模板引擎,它允许开发者在文本文件(如 HTML 或配置文件)中嵌入 JavaScript 代码,实现动态内容生成。典型应用场景包括:填充用户选项、循环生成列表、条件渲染等。

2️⃣ ejs.render 基础用法

import ejs from 'ejs';

const template = `
Hello <%= name %>!
<% if (age >= 18) { %>
You are an adult.
<% } else { %>
You are a minor.
<% } %>
`;

const data = { name: 'Alice', age: 20 };
const result = ejs.render(template, data);

console.log(result);

输出结果:

Hello Alice!
You are an adult.

语法说明:

  • <%= ... %>:输出表达式值(自动转义)
  • <% ... %>:执行 JavaScript 逻辑(不输出)
  • data:模板渲染时传入的数据对象

3️⃣ 实际应用示例

let content = ejs.render(
  fs.readFileSync(path.resolve(templatePath, name), 'utf8'),
  {
    eslintIgnores: ESLINT_IGNORE_PATTERN,
    stylelintExt: STYLELINT_FILE_EXT,
    stylelintIgnores: STYLELINT_IGNORE_PATTERN,
    markdownLintIgnores: MARKDOWN_LINT_IGNORE_PATTERN,
    ...data, // 合并用户配置选项
  }
);

实现功能:

  1. 读取指定路径的 EJS 模板文件
  2. 注入多类配置数据:
    • ESLint 忽略规则
    • Stylelint 相关配置(文件扩展名和忽略规则)
    • Markdownlint 忽略规则
    • 用户自定义选项(如是否启用 Prettier、Stylelint 等)
  3. 生成最终文件内容:
    • 替换模板中的变量占位符
    • 执行条件判断和循环逻辑

根据模板生成项目配置文件(generate-template.ts)

1️⃣ 核心功能

配置文件生成

  • 根据用户选择(ESLint/Stylelint/Prettier/Markdownlint等)生成对应配置文件
  • 支持生成格式:.eslintrc.stylelintrc.prettierrc.vscode/settings.json

智能配置合并

  • 当项目存在VSCode配置时自动进行深度合并
  • 采用lodash.mergeWith实现智能合并
  • 特殊处理数组类型配置(如files.exclude/eslint.validate),自动去重

模板化渲染

  • 基于EJS模板引擎动态生成文件内容
  • 自动注入用户选项和预设忽略模式
  • 支持文件名转换规则:_filename.filename(如_eslintrc.ejs.eslintrc

2️⃣ 实现逻辑

模板定位

const templatePath = path.resolve(__dirname, '../config');

模板匹配

  • 根据vscode标识动态选择模板目录
const templates = glob.sync(`${vscode ? '_vscode' : '**'}/*.ejs`, { cwd: templatePath });

模板渲染

ejs.render(fs.readFileSync(templatePath + name), {
  eslintIgnores,
  stylelintExt,
  stylelintIgnores,
  markdownLintIgnores,
  ...data
});

配置合并处理

if (/^_vscode/.test(name)) {
  content = mergeVSCodeConfig(filepath, content);
}

文件写入

fs.outputFileSync(filepath, content, 'utf8');

3️⃣ 核心优势

模板化架构:通过.ejs模板实现配置灵活扩展
安全合并机制:智能保留现有VSCode配置
自动命名转换_前缀文件自动转为.配置文件
智能规则注入:自动集成各lint工具的忽略规则

基于模板引擎的智能配置生成器,可安全合并现有VSCode配置,避免覆盖用户自定义设置。

完整代码:

import path from 'path'; 
import fs from 'fs-extra'; 
import _ from 'lodash';
import glob from 'glob'; 
import ejs from 'ejs'; /
import {
  ESLINT_IGNORE_PATTERN, // ESLint 忽略文件模式
  STYLELINT_FILE_EXT, // Stylelint 支持的文件扩展名
  STYLELINT_IGNORE_PATTERN, // Stylelint 忽略文件模式
  MARKDOWN_LINT_IGNORE_PATTERN, // Markdownlint 忽略文件模式
} from './constants';

/**
 * VSCode 配置合并
 * 将新生成的 VSCode 配置与现有配置进行智能合并
 * @param filepath 目标配置文件路径
 * @param content 新生成的配置内容
 * @returns 合并后的配置内容
 */
const mergeVSCodeConfig = (filepath: string, content: string) => {
  // 如果目标文件不存在,直接返回新内容
  if (!fs.existsSync(filepath)) return content;

  try {
    // 读取现有配置文件
    const targetData = fs.readJSONSync(filepath);
    // 解析新生成的配置内容
    const sourceData = JSON.parse(content);
    
    // 使用 lodash 的 mergeWith 进行深度合并
    return JSON.stringify(
      _.mergeWith(targetData, sourceData, (target, source) => {
        // 如果两个值都是数组,则合并并去重
        if (Array.isArray(target) && Array.isArray(source)) {
          return [...new Set(source.concat(target))];
        }
      }),
      null,
      2, // 缩进 2 个空格
    );
  } catch (e) {
    // 解析失败时返回空字符串
    return '';
  }
};

/**
 * 生成配置文件模板
 * 根据用户选择的配置选项生成相应的配置文件
 * @param cwd 当前工作目录
 * @param data 配置数据(用户选择的各种选项)
 * @param vscode 是否只生成 VSCode 相关配置,默认为 false
 */
export default (cwd: string, data: Record<string, any>, vscode?: boolean) => {
  // 模板文件所在目录
  const templatePath = path.resolve(__dirname, '../config');
  
  // 根据 vscode 参数决定匹配的模板文件
  // 如果 vscode 为 true,只匹配 _vscode 目录下的模板
  // 否则匹配所有目录下的 .ejs 模板文件
  const templates = glob.sync(`${vscode ? '_vscode' : '**'}/*.ejs`, { cwd: templatePath });
  
  // 遍历所有模板文件
  for (const name of templates) {
    // 生成目标文件路径
    // 移除 .ejs 扩展名,将 _ 开头的文件名改为 . 开头
    const filepath = path.resolve(cwd, name.replace(/\.ejs$/, '').replace(/^_/, '.'));
    
    // 使用 EJS 模板引擎渲染文件内容
    let content = ejs.render(fs.readFileSync(path.resolve(templatePath, name), 'utf8'), {
      eslintIgnores: ESLINT_IGNORE_PATTERN, // ESLint 忽略模式
      stylelintExt: STYLELINT_FILE_EXT, // Stylelint 文件扩展名
      stylelintIgnores: STYLELINT_IGNORE_PATTERN, // Stylelint 忽略模式
      markdownLintIgnores: MARKDOWN_LINT_IGNORE_PATTERN, // Markdownlint 忽略模式
      ...data, // 用户选择的配置选项
    });

    // 如果是 VSCode 配置文件,需要与现有配置合并
    if (/^_vscode/.test(name)) {
      content = mergeVSCodeConfig(filepath, content);
    }

    // 跳过空文件
    if (!content.trim()) continue;

    // 写入文件到目标路径
    fs.outputFileSync(filepath, content, 'utf8');
  }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值