From 9932a48c256f5ec0fc01512a1b4a0820646df9fd Mon Sep 17 00:00:00 2001 From: semimikoh Date: Fri, 11 Sep 2026 11:10:53 +0900 Subject: [PATCH] fix(create): remove nested monorepo lint config The standalone library template enables lint type checking, but nested lint configuration is not supported in monorepos. Remove the generated library's lint block during monorepo scaffolding while preserving it for standalone libraries. Apply the cleanup when adding libraries to existing monorepos as well. Use an AST-based config-key transform restricted to direct config objects, and add CLI snapshot coverage for both creation paths. --- .../create_missing_typecheck/snapshots.toml | 1 + .../snapshots/create_missing_typecheck.md | 20 +++++ .../snapshots.toml | 8 ++ ...eate_monorepo_library_omits_nested_lint.md | 25 ++++++ crates/vp_migration/src/lib.rs | 4 +- crates/vp_migration/src/vite_config.rs | 80 +++++++++++++++++++ packages/cli/binding/index.cjs | 1 + packages/cli/binding/index.d.cts | 6 ++ packages/cli/binding/src/migration.rs | 17 ++++ .../cli/src/create/__tests__/monorepo.spec.ts | 57 +++++++++++++ packages/cli/src/create/templates/builtin.ts | 4 + packages/cli/src/create/templates/monorepo.ts | 21 +++++ 12 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_next_command_monorepo_library/snapshots/create_monorepo_library_omits_nested_lint.md diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_missing_typecheck/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_missing_typecheck/snapshots.toml index a5103679fa..e0c5fe64e0 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_missing_typecheck/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_missing_typecheck/snapshots.toml @@ -8,4 +8,5 @@ steps = [ { argv = ["vp", "create", "vite:monorepo", "--no-interactive"], comment = "create monorepo", snapshot = false, continue-on-failure = true }, { argv = ["vpt", "print-file", "vite-plus-monorepo/vite.config.ts"], comment = "check monorepo root vite.config.ts has typeAware and typeCheck", continue-on-failure = true }, { argv = ["vpt", "stat-file", "vite-plus-monorepo/apps/website/vite.config.ts", "--assert-not", "file"], comment = "sub-app should NOT have typeAware/typeCheck", continue-on-failure = true }, + { argv = ["vpt", "print-file", "vite-plus-monorepo/packages/utils/vite.config.ts"], comment = "sub-library should NOT have nested lint config", continue-on-failure = true }, ] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_missing_typecheck/snapshots/create_missing_typecheck.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_missing_typecheck/snapshots/create_missing_typecheck.md index abecad003e..ce07913fb6 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_missing_typecheck/snapshots/create_missing_typecheck.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_missing_typecheck/snapshots/create_missing_typecheck.md @@ -60,3 +60,23 @@ sub-app should NOT have typeAware/typeCheck ``` vite-plus-monorepo/apps/website/vite.config.ts: missing ``` + +## `vpt print-file vite-plus-monorepo/packages/utils/vite.config.ts` + +sub-library should NOT have nested lint config + +``` +import { defineConfig } from "vite-plus"; + +export default defineConfig({ + pack: { + deps: { resolveDepSubpath: true }, + dts: { + generator: "tsgo", + }, + exports: true, + }, + + fmt: {}, +}); +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_next_command_monorepo_library/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_next_command_monorepo_library/snapshots.toml index 7dec68a174..b7973869bc 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_next_command_monorepo_library/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_next_command_monorepo_library/snapshots.toml @@ -4,3 +4,11 @@ vp = "global" steps = [ { argv = ["vp", "create", "vite:library", "--no-interactive"], comment = "monorepo: next command should suggest vp run", timeout = 120000, continue-on-failure = true }, ] + +[[case]] +name = "create_monorepo_library_omits_nested_lint" +vp = "global" +steps = [ + { argv = ["vp", "create", "vite:library", "--no-interactive"], comment = "create a library in an existing monorepo", timeout = 120000, snapshot = false, continue-on-failure = true }, + { argv = ["vpt", "print-file", "packages/vite-plus-library/vite.config.ts"], comment = "nested library config should omit lint", continue-on-failure = true }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_next_command_monorepo_library/snapshots/create_monorepo_library_omits_nested_lint.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_next_command_monorepo_library/snapshots/create_monorepo_library_omits_nested_lint.md new file mode 100644 index 0000000000..1ef28cd61c --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_next_command_monorepo_library/snapshots/create_monorepo_library_omits_nested_lint.md @@ -0,0 +1,25 @@ +# create_monorepo_library_omits_nested_lint + +## `vp create vite:library --no-interactive` + +create a library in an existing monorepo + + +## `vpt print-file packages/vite-plus-library/vite.config.ts` + +nested library config should omit lint + +``` +import { defineConfig } from "vite-plus"; + +export default defineConfig({ + pack: { + dts: { + tsgo: true, + }, + exports: true, + }, + + fmt: {}, +}); +``` diff --git a/crates/vp_migration/src/lib.rs b/crates/vp_migration/src/lib.rs index 427db5ea50..f77a04b9d5 100644 --- a/crates/vp_migration/src/lib.rs +++ b/crates/vp_migration/src/lib.rs @@ -23,6 +23,6 @@ pub use import_rewriter::{ }; pub use package::{rewrite_eslint, rewrite_prettier, rewrite_scripts}; pub use vite_config::{ - MergeResult, has_config_key, merge_json_config, merge_tsdown_config, upsert_json_config, - wrap_lazy_plugins, + MergeResult, has_config_key, merge_json_config, merge_tsdown_config, remove_config_key, + upsert_json_config, wrap_lazy_plugins, }; diff --git a/crates/vp_migration/src/vite_config.rs b/crates/vp_migration/src/vite_config.rs index 55adc106af..0658f1c9e5 100644 --- a/crates/vp_migration/src/vite_config.rs +++ b/crates/vp_migration/src/vite_config.rs @@ -324,6 +324,54 @@ pub fn has_config_key(vite_config_content: &str, config_key: &str) -> Result Result { + let uses_function_callback = check_function_callback(vite_config_content)?; + let grep = SupportLang::TypeScript.ast_grep(vite_config_content); + let root = grep.root(); + let mut edits = Vec::new(); + + for node in root.dfs() { + let matches_key = match node.kind().as_ref() { + "pair" => node.field("key").is_some_and(|key| pair_key_matches(&key, config_key)), + "shorthand_property_identifier" => node.text() == config_key, + _ => continue, + }; + if !matches_key { + continue; + } + let Some(parent_object) = node.parent() else { continue }; + if parent_object.kind() != "object" || !is_direct_recognized_config_object(&parent_object) { + continue; + } + + let range = node.range(); + edits.push((range.start, range.end)); + if let Some(next) = node.next_all().find(|sibling| sibling.kind() != "comment") + && next.kind() == "," + { + let comma = next.range(); + edits.push((comma.start, comma.end)); + } + } + + edits.sort_by_key(|(start, _)| std::cmp::Reverse(*start)); + let updated = !edits.is_empty(); + let mut content = vite_config_content.to_owned(); + for (start, end) in edits { + content.replace_range(start..end, ""); + } + + Ok(MergeResult { content, updated, uses_function_callback }) +} + /// Wrap safe inline Vite plugin arrays with `lazyPlugins(() => [...])`. /// /// This transform is intentionally conservative: it only touches direct @@ -1069,6 +1117,38 @@ export default defineConfig({ assert!(!has_config_key(cfg, "staged").unwrap()); } + // ── remove_config_key ───────────────────────────────────────────────── + + #[test] + fn test_remove_config_key_from_define_config() { + let cfg = r#"export default defineConfig({ + pack: { exports: true }, + lint: { options: { typeAware: true, typeCheck: true } }, + fmt: {}, +}); +"#; + let result = remove_config_key(cfg, "lint").unwrap(); + + assert!(result.updated); + assert!(!result.content.contains("lint:")); + assert!(result.content.contains("pack: { exports: true }")); + assert!(result.content.contains("fmt: {}")); + } + + #[test] + fn test_remove_config_key_ignores_nested_and_unrecognized_objects() { + for cfg in [ + "export default defineConfig({ plugin: { lint: {} } });", + "export default defineConfig(() => ({ plugin: { config() { return { lint: {} } } } }));", + "export default defineConfig(() => config);", + "module.exports = { lint: {} };", + ] { + let result = remove_config_key(cfg, "lint").unwrap(); + assert!(!result.updated); + assert_eq!(result.content, cfg); + } + } + #[test] fn test_has_config_key_quoted_key() { let cfg = r#"import { defineConfig } from 'vite-plus'; diff --git a/packages/cli/binding/index.cjs b/packages/cli/binding/index.cjs index 67109dc1e8..bcdb05f3bd 100644 --- a/packages/cli/binding/index.cjs +++ b/packages/cli/binding/index.cjs @@ -974,6 +974,7 @@ module.exports.parseCreateArgs = nativeBinding.parseCreateArgs; module.exports.parseHooksArgs = nativeBinding.parseHooksArgs; module.exports.parseMigrateArgs = nativeBinding.parseMigrateArgs; module.exports.parseStagedArgs = nativeBinding.parseStagedArgs; +module.exports.removeConfigKey = nativeBinding.removeConfigKey; module.exports.rewriteEslint = nativeBinding.rewriteEslint; module.exports.rewriteImportsInDirectory = nativeBinding.rewriteImportsInDirectory; module.exports.rewritePrettier = nativeBinding.rewritePrettier; diff --git a/packages/cli/binding/index.d.cts b/packages/cli/binding/index.d.cts index 7e637156df..63c2bd2915 100644 --- a/packages/cli/binding/index.d.cts +++ b/packages/cli/binding/index.d.cts @@ -3754,6 +3754,12 @@ export interface PathAccess { readDir: boolean; } +/** Remove a top-level key from a recognized Vite config object. */ +export declare function removeConfigKey( + viteConfigPath: string, + configKey: string, +): MergeJsonConfigResult; + /** * Rewrite ESLint scripts: rename `eslint` → `vp lint` and strip ESLint-only flags. * diff --git a/packages/cli/binding/src/migration.rs b/packages/cli/binding/src/migration.rs index 39501fc176..6c1be38630 100644 --- a/packages/cli/binding/src/migration.rs +++ b/packages/cli/binding/src/migration.rs @@ -183,6 +183,23 @@ pub fn has_config_key(vite_config_path: String, config_key: String) -> Result Result { + let content = std::fs::read_to_string(&vite_config_path).map_err(anyhow::Error::from)?; + let result = + vp_migration::remove_config_key(&content, &config_key).map_err(anyhow::Error::from)?; + + Ok(MergeJsonConfigResult { + content: result.content, + updated: result.updated, + uses_function_callback: result.uses_function_callback, + }) +} + /// Error from batch import rewriting #[napi(object)] pub struct BatchRewriteError { diff --git a/packages/cli/src/create/__tests__/monorepo.spec.ts b/packages/cli/src/create/__tests__/monorepo.spec.ts index ecc8093792..adff06b93a 100644 --- a/packages/cli/src/create/__tests__/monorepo.spec.ts +++ b/packages/cli/src/create/__tests__/monorepo.spec.ts @@ -8,6 +8,7 @@ import { PackageManager } from '../../types/index.js'; import { alignMonorepoTypeScriptVersion, dropAliasedRuntimeDevDeps, + removeNestedLibraryLintConfig, } from '../templates/monorepo.js'; function writePackageJson(directory: string, devDependencies: Record): void { @@ -113,6 +114,62 @@ describe('alignMonorepoTypeScriptVersion', () => { }); }); +describe('removeNestedLibraryLintConfig', () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-monorepo-lint-config-')); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('removes root-only lint options from the nested library config', () => { + const configPath = path.join(tmpDir, 'vite.config.ts'); + fs.writeFileSync( + configPath, + `import { defineConfig } from 'vite-plus'; + +export default defineConfig({ + pack: { exports: true }, + lint: { + options: { + typeAware: true, + typeCheck: true, + }, + }, + fmt: {}, +}); +`, + ); + + removeNestedLibraryLintConfig(tmpDir); + + const content = fs.readFileSync(configPath, 'utf8'); + expect(content).not.toContain('lint:'); + expect(content).toContain('pack: { exports: true }'); + expect(content).toContain('fmt: {}'); + }); + + it('removes the complete nested lint config', () => { + const configPath = path.join(tmpDir, 'vite.config.ts'); + fs.writeFileSync( + configPath, + `import { defineConfig } from 'vite-plus'; + +export default defineConfig({ + lint: { rules: { 'no-console': 'error' } }, +}); +`, + ); + + removeNestedLibraryLintConfig(tmpDir); + + expect(fs.readFileSync(configPath, 'utf8')).not.toContain('lint:'); + }); +}); + describe('dropAliasedRuntimeDevDeps', () => { let tmpDir: string; diff --git a/packages/cli/src/create/templates/builtin.ts b/packages/cli/src/create/templates/builtin.ts index 3766f65bc3..29b6b329d3 100644 --- a/packages/cli/src/create/templates/builtin.ts +++ b/packages/cli/src/create/templates/builtin.ts @@ -9,6 +9,7 @@ import type { ExecutionWithProjectDir } from '../command.ts'; import { discoverTemplate } from '../discovery.ts'; import { setPackageName } from '../utils.ts'; import { executeGeneratorScaffold } from './generator.ts'; +import { removeNestedLibraryLintConfig } from './monorepo.ts'; import { runRemoteTemplateCommand } from './remote.ts'; import { BuiltinTemplate, type BuiltinTemplateInfo, LibraryTemplateRepo } from './types.ts'; @@ -49,6 +50,9 @@ export async function executeBuiltinTemplate( } const fullPath = path.join(workspaceInfo.rootDir, templateInfo.targetDir); setPackageName(fullPath, templateInfo.packageName); + if (workspaceInfo.isMonorepo) { + removeNestedLibraryLintConfig(fullPath); + } return { ...result, projectDir: templateInfo.targetDir }; } diff --git a/packages/cli/src/create/templates/monorepo.ts b/packages/cli/src/create/templates/monorepo.ts index 159a3086b9..ffcdf56d0d 100644 --- a/packages/cli/src/create/templates/monorepo.ts +++ b/packages/cli/src/create/templates/monorepo.ts @@ -4,6 +4,7 @@ import path from 'node:path'; import * as prompts from '@voidzero-dev/vite-plus-prompts'; +import { removeConfigKey } from '../../../binding/index.js'; import { rewriteMonorepoProject } from '../../migration/migrator.ts'; import { PackageManager, type WorkspaceInfo } from '../../types/index.ts'; import { editJsonFile } from '../../utils/json.ts'; @@ -152,6 +153,7 @@ export async function executeMonorepoTemplate( : 'utils'; const libraryProjectPath = path.join(fullPath, libraryDir); setPackageName(libraryProjectPath, libraryPackageName); + removeNestedLibraryLintConfig(libraryProjectPath); // Perform auto-migration on the created library rewriteMonorepoProject( libraryProjectPath, @@ -165,6 +167,25 @@ export async function executeMonorepoTemplate( return { exitCode: 0, projectDir: templateInfo.targetDir }; } +/** + * Remove the root-only lint options shipped by the standalone library template. + * + * The same remote template is also used by `vite:library`, where this config is + * valid. A library created as a workspace member, however, gets its lint config + * from the monorepo root, so retaining it here creates an invalid nested config. + */ +export function removeNestedLibraryLintConfig(projectPath: string): void { + const configPath = path.join(projectPath, 'vite.config.ts'); + if (!fs.existsSync(configPath)) { + return; + } + + const result = removeConfigKey(configPath, 'lint'); + if (result.updated) { + fs.writeFileSync(configPath, result.content); + } +} + /** * Keep every scaffolded workspace member on the same TypeScript version. *