Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
]
Original file line number Diff line number Diff line change
Expand Up @@ -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: {},
});
```
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
]
Original file line number Diff line number Diff line change
@@ -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: {},
});
```
4 changes: 2 additions & 2 deletions crates/vp_migration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
80 changes: 80 additions & 0 deletions crates/vp_migration/src/vite_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,54 @@ pub fn has_config_key(vite_config_content: &str, config_key: &str) -> Result<boo
Ok(false)
}

/// Remove a top-level key from each recognized Vite config object.
///
/// Unrecognized config shapes are left untouched. The returned content may
/// retain whitespace where the property was removed; callers format generated
/// configs after migration.
pub fn remove_config_key(
vite_config_content: &str,
config_key: &str,
) -> Result<MergeResult, Error> {
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
Expand Down Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions packages/cli/binding/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/binding/index.d.cts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
17 changes: 17 additions & 0 deletions packages/cli/binding/src/migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,23 @@ pub fn has_config_key(vite_config_path: String, config_key: String) -> Result<bo
Ok(vp_migration::has_config_key(&content, &config_key).map_err(anyhow::Error::from)?)
}

/// Remove a top-level key from a recognized Vite config object.
#[napi]
pub fn remove_config_key(
vite_config_path: String,
config_key: String,
) -> Result<MergeJsonConfigResult> {
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 {
Expand Down
57 changes: 57 additions & 0 deletions packages/cli/src/create/__tests__/monorepo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { PackageManager } from '../../types/index.js';
import {
alignMonorepoTypeScriptVersion,
dropAliasedRuntimeDevDeps,
removeNestedLibraryLintConfig,
} from '../templates/monorepo.js';

function writePackageJson(directory: string, devDependencies: Record<string, string>): void {
Expand Down Expand Up @@ -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;

Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/create/templates/builtin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 };
}

Expand Down
21 changes: 21 additions & 0 deletions packages/cli/src/create/templates/monorepo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -152,6 +153,7 @@ export async function executeMonorepoTemplate(
: 'utils';
const libraryProjectPath = path.join(fullPath, libraryDir);
setPackageName(libraryProjectPath, libraryPackageName);
removeNestedLibraryLintConfig(libraryProjectPath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply lint removal to libraries added to existing monorepos

When vp create vite:library is run inside an existing monorepo, execution downloads the same LibraryTemplateRepo through executeBuiltinTemplate (packages/cli/src/create/templates/builtin.ts:33-52) and then goes directly through rewriteMonorepoProject (packages/cli/src/create/bin.ts:1229-1244); neither path calls this helper. That generated workspace member therefore retains the same unsupported nested lint block, so the regression remains for the normal add-a-library flow even though the initial vite:monorepo library is fixed. Invoke this cleanup from the shared library scaffold path when the destination is a monorepo member.

Useful? React with 👍 / 👎.

// Perform auto-migration on the created library
rewriteMonorepoProject(
libraryProjectPath,
Expand All @@ -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.
*
Expand Down
Loading