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. *