Skip to content

Commit a3035cf

Browse files
authored
fix(create): remove nested monorepo lint config (#2667)
- remove the generated library lint block when scaffolding a Vite+ monorepo - preserve the lint configuration for standalone `vite:library` projects - add an AST-based top-level config key removal helper and regression coverage Fixes #2640
1 parent ce940e3 commit a3035cf

12 files changed

Lines changed: 241 additions & 2 deletions

File tree

‎crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_missing_typecheck/snapshots.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ steps = [
88
{ argv = ["vp", "create", "vite:monorepo", "--no-interactive"], comment = "create monorepo", snapshot = false, continue-on-failure = true },
99
{ 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 },
1010
{ 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 },
11+
{ argv = ["vpt", "print-file", "vite-plus-monorepo/packages/utils/vite.config.ts"], comment = "sub-library should NOT have nested lint or fmt config", continue-on-failure = true },
1112
]

‎crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_missing_typecheck/snapshots/create_missing_typecheck.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,21 @@ sub-app should NOT have typeAware/typeCheck
6060
```
6161
vite-plus-monorepo/apps/website/vite.config.ts: missing
6262
```
63+
64+
## `vpt print-file vite-plus-monorepo/packages/utils/vite.config.ts`
65+
66+
sub-library should NOT have nested lint or fmt config
67+
68+
```
69+
import { defineConfig } from "vite-plus";
70+
71+
export default defineConfig({
72+
pack: {
73+
deps: { resolveDepSubpath: true },
74+
dts: {
75+
generator: "tsgo",
76+
},
77+
exports: true,
78+
},
79+
});
80+
```

‎crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_next_command_monorepo_library/snapshots.toml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,11 @@ vp = "global"
44
steps = [
55
{ argv = ["vp", "create", "vite:library", "--no-interactive"], comment = "monorepo: next command should suggest vp run", timeout = 120000, continue-on-failure = true },
66
]
7+
8+
[[case]]
9+
name = "create_monorepo_library_omits_nested_lint"
10+
vp = "global"
11+
steps = [
12+
{ argv = ["vp", "create", "vite:library", "--no-interactive"], comment = "create a library in an existing monorepo", timeout = 120000, snapshot = false, continue-on-failure = true },
13+
{ argv = ["vpt", "print-file", "packages/vite-plus-library/vite.config.ts"], comment = "nested library config should omit lint and fmt", continue-on-failure = true },
14+
]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# create_monorepo_library_omits_nested_lint
2+
3+
## `vp create vite:library --no-interactive`
4+
5+
create a library in an existing monorepo
6+
7+
8+
## `vpt print-file packages/vite-plus-library/vite.config.ts`
9+
10+
nested library config should omit lint and fmt
11+
12+
```
13+
import { defineConfig } from "vite-plus";
14+
15+
export default defineConfig({
16+
pack: {
17+
dts: {
18+
tsgo: true,
19+
},
20+
exports: true,
21+
},
22+
});
23+
```

‎crates/vp_migration/src/lib.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@ pub use import_rewriter::{
2323
};
2424
pub use package::{rewrite_eslint, rewrite_prettier, rewrite_scripts};
2525
pub use vite_config::{
26-
MergeResult, has_config_key, merge_json_config, merge_tsdown_config, upsert_json_config,
27-
wrap_lazy_plugins,
26+
MergeResult, has_config_key, merge_json_config, merge_tsdown_config, remove_config_key,
27+
upsert_json_config, wrap_lazy_plugins,
2828
};

‎crates/vp_migration/src/vite_config.rs‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,54 @@ pub fn has_config_key(vite_config_content: &str, config_key: &str) -> Result<boo
324324
Ok(false)
325325
}
326326

327+
/// Remove a top-level key from each recognized Vite config object.
328+
///
329+
/// Unrecognized config shapes are left untouched. The returned content may
330+
/// retain whitespace where the property was removed; callers format generated
331+
/// configs after migration.
332+
pub fn remove_config_key(
333+
vite_config_content: &str,
334+
config_key: &str,
335+
) -> Result<MergeResult, Error> {
336+
let uses_function_callback = check_function_callback(vite_config_content)?;
337+
let grep = SupportLang::TypeScript.ast_grep(vite_config_content);
338+
let root = grep.root();
339+
let mut edits = Vec::new();
340+
341+
for node in root.dfs() {
342+
let matches_key = match node.kind().as_ref() {
343+
"pair" => node.field("key").is_some_and(|key| pair_key_matches(&key, config_key)),
344+
"shorthand_property_identifier" => node.text() == config_key,
345+
_ => continue,
346+
};
347+
if !matches_key {
348+
continue;
349+
}
350+
let Some(parent_object) = node.parent() else { continue };
351+
if parent_object.kind() != "object" || !is_direct_recognized_config_object(&parent_object) {
352+
continue;
353+
}
354+
355+
let range = node.range();
356+
edits.push((range.start, range.end));
357+
if let Some(next) = node.next_all().find(|sibling| sibling.kind() != "comment")
358+
&& next.kind() == ","
359+
{
360+
let comma = next.range();
361+
edits.push((comma.start, comma.end));
362+
}
363+
}
364+
365+
edits.sort_by_key(|(start, _)| std::cmp::Reverse(*start));
366+
let updated = !edits.is_empty();
367+
let mut content = vite_config_content.to_owned();
368+
for (start, end) in edits {
369+
content.replace_range(start..end, "");
370+
}
371+
372+
Ok(MergeResult { content, updated, uses_function_callback })
373+
}
374+
327375
/// Wrap safe inline Vite plugin arrays with `lazyPlugins(() => [...])`.
328376
///
329377
/// This transform is intentionally conservative: it only touches direct
@@ -1069,6 +1117,38 @@ export default defineConfig({
10691117
assert!(!has_config_key(cfg, "staged").unwrap());
10701118
}
10711119

1120+
// ── remove_config_key ─────────────────────────────────────────────────
1121+
1122+
#[test]
1123+
fn test_remove_config_key_from_define_config() {
1124+
let cfg = r#"export default defineConfig({
1125+
pack: { exports: true },
1126+
lint: { options: { typeAware: true, typeCheck: true } },
1127+
fmt: {},
1128+
});
1129+
"#;
1130+
let result = remove_config_key(cfg, "lint").unwrap();
1131+
1132+
assert!(result.updated);
1133+
assert!(!result.content.contains("lint:"));
1134+
assert!(result.content.contains("pack: { exports: true }"));
1135+
assert!(result.content.contains("fmt: {}"));
1136+
}
1137+
1138+
#[test]
1139+
fn test_remove_config_key_ignores_nested_and_unrecognized_objects() {
1140+
for cfg in [
1141+
"export default defineConfig({ plugin: { lint: {} } });",
1142+
"export default defineConfig(() => ({ plugin: { config() { return { lint: {} } } } }));",
1143+
"export default defineConfig(() => config);",
1144+
"module.exports = { lint: {} };",
1145+
] {
1146+
let result = remove_config_key(cfg, "lint").unwrap();
1147+
assert!(!result.updated);
1148+
assert_eq!(result.content, cfg);
1149+
}
1150+
}
1151+
10721152
#[test]
10731153
fn test_has_config_key_quoted_key() {
10741154
let cfg = r#"import { defineConfig } from 'vite-plus';

‎packages/cli/binding/index.cjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,6 +1054,7 @@ module.exports.parseCreateArgs = nativeBinding.parseCreateArgs;
10541054
module.exports.parseHooksArgs = nativeBinding.parseHooksArgs;
10551055
module.exports.parseMigrateArgs = nativeBinding.parseMigrateArgs;
10561056
module.exports.parseStagedArgs = nativeBinding.parseStagedArgs;
1057+
module.exports.removeConfigKey = nativeBinding.removeConfigKey;
10571058
module.exports.rewriteEslint = nativeBinding.rewriteEslint;
10581059
module.exports.rewriteImportsInDirectory = nativeBinding.rewriteImportsInDirectory;
10591060
module.exports.rewritePrettier = nativeBinding.rewritePrettier;

‎packages/cli/binding/index.d.cts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3799,6 +3799,12 @@ export interface PathAccess {
37993799
readDir: boolean;
38003800
}
38013801

3802+
/** Remove a top-level key from a recognized Vite config object. */
3803+
export declare function removeConfigKey(
3804+
viteConfigPath: string,
3805+
configKey: string,
3806+
): MergeJsonConfigResult;
3807+
38023808
/**
38033809
* Rewrite ESLint scripts: rename `eslint` → `vp lint` and strip ESLint-only flags.
38043810
*

‎packages/cli/binding/src/migration.rs‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,23 @@ pub fn has_config_key(vite_config_path: String, config_key: String) -> Result<bo
183183
Ok(vp_migration::has_config_key(&content, &config_key).map_err(anyhow::Error::from)?)
184184
}
185185

186+
/// Remove a top-level key from a recognized Vite config object.
187+
#[napi]
188+
pub fn remove_config_key(
189+
vite_config_path: String,
190+
config_key: String,
191+
) -> Result<MergeJsonConfigResult> {
192+
let content = std::fs::read_to_string(&vite_config_path).map_err(anyhow::Error::from)?;
193+
let result =
194+
vp_migration::remove_config_key(&content, &config_key).map_err(anyhow::Error::from)?;
195+
196+
Ok(MergeJsonConfigResult {
197+
content: result.content,
198+
updated: result.updated,
199+
uses_function_callback: result.uses_function_callback,
200+
})
201+
}
202+
186203
/// Error from batch import rewriting
187204
#[napi(object)]
188205
pub struct BatchRewriteError {

‎packages/cli/src/create/__tests__/monorepo.spec.ts‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { PackageManager } from '../../types/index.js';
88
import {
99
alignMonorepoTypeScriptVersion,
1010
dropAliasedRuntimeDevDeps,
11+
removeNestedLibraryToolConfig,
1112
} from '../templates/monorepo.js';
1213

1314
function writePackageJson(directory: string, devDependencies: Record<string, string>): void {
@@ -113,6 +114,62 @@ describe('alignMonorepoTypeScriptVersion', () => {
113114
});
114115
});
115116

117+
describe('removeNestedLibraryToolConfig', () => {
118+
let tmpDir: string;
119+
120+
beforeEach(() => {
121+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-monorepo-lint-config-'));
122+
});
123+
124+
afterEach(() => {
125+
fs.rmSync(tmpDir, { recursive: true, force: true });
126+
});
127+
128+
it('removes root-only lint and format options from the nested library config', () => {
129+
const configPath = path.join(tmpDir, 'vite.config.ts');
130+
fs.writeFileSync(
131+
configPath,
132+
`import { defineConfig } from 'vite-plus';
133+
134+
export default defineConfig({
135+
pack: { exports: true },
136+
lint: {
137+
options: {
138+
typeAware: true,
139+
typeCheck: true,
140+
},
141+
},
142+
fmt: {},
143+
});
144+
`,
145+
);
146+
147+
removeNestedLibraryToolConfig(tmpDir);
148+
149+
const content = fs.readFileSync(configPath, 'utf8');
150+
expect(content).not.toContain('lint:');
151+
expect(content).not.toContain('fmt:');
152+
expect(content).toContain('pack: { exports: true }');
153+
});
154+
155+
it('removes the complete nested lint config', () => {
156+
const configPath = path.join(tmpDir, 'vite.config.ts');
157+
fs.writeFileSync(
158+
configPath,
159+
`import { defineConfig } from 'vite-plus';
160+
161+
export default defineConfig({
162+
lint: { rules: { 'no-console': 'error' } },
163+
});
164+
`,
165+
);
166+
167+
removeNestedLibraryToolConfig(tmpDir);
168+
169+
expect(fs.readFileSync(configPath, 'utf8')).not.toContain('lint:');
170+
});
171+
});
172+
116173
describe('dropAliasedRuntimeDevDeps', () => {
117174
let tmpDir: string;
118175

0 commit comments

Comments
 (0)