Skip to content
Closed
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
@@ -0,0 +1,15 @@
{
"name": "config-entry",
"private": true,
"type": "module",
"devDependencies": {
"@types/node": "24.10.3",
"typescript": "7.0.2",
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
"vite-plus": "latest"
},
"overrides": {
"vite": "npm:@voidzero-dev/vite-plus-core@latest"
},
"packageManager": "npm@11.11.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[[case]]
name = "config_entry_without_pack"
vp = "global"
local-registry = true
comment = "The published app config entry keeps native task loading and strict TypeScript 7 configuration without optional pack integrations."
steps = [
{ argv = ["vp", "install", "--ignore-scripts"], timeout = 120000, snapshot = false },
["vp", "run", "verify"],
]

[[case]]
name = "config_entry_with_pack"
vp = "global"
local-registry = true
comment = "Both the original entry and explicit pack type activation preserve real injected module types and references."
steps = [
{ argv = ["vpt", "json-edit", "package.json", "devDependencies.typescript", "6.0.3"], snapshot = false },
{ argv = ["vpt", "json-edit", "package.json", "devDependencies.@arethetypeswrong/core", "0.18.5"], snapshot = false },
{ argv = ["vpt", "json-edit", "package.json", "devDependencies.@volar/typescript", "2.4.28"], snapshot = false },
{ argv = ["vpt", "json-edit", "package.json", "devDependencies.@volar/language-service", "2.4.28"], snapshot = false },
{ argv = ["vp", "install", "--ignore-scripts"], timeout = 120000, snapshot = false },
["node", "verify.mjs", "pack"],
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# config_entry_with_pack

Both the original entry and explicit pack type activation preserve real injected module types and references.

## `vpt json-edit package.json devDependencies.typescript 6.0.3`


## `vpt json-edit package.json devDependencies.@arethetypeswrong/core 0.18.5`


## `vpt json-edit package.json devDependencies.@volar/typescript 2.4.28`


## `vpt json-edit package.json devDependencies.@volar/language-service 2.4.28`


## `vp install --ignore-scripts`


## `node verify.mjs pack`

```
Original and explicit pack configuration preserve injected module types and identity
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# config_entry_without_pack

The published app config entry keeps native task loading and strict TypeScript 7 configuration without optional pack integrations.

## `vp install --ignore-scripts`


## `vp run verify`

```
VITE+ - The Unified Toolchain for the Web

$ node verify.mjs
App configuration is strict without pack; ESM, CommonJS, and native plugins retain identity
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { readFileSync, writeFileSync } from 'node:fs';
import { createRequire } from 'node:module';

const require = createRequire(import.meta.url);

function typecheck(source) {
writeFileSync('fixture.ts', source);
const result = spawnSync(
process.execPath,
[
'node_modules/typescript/bin/tsc',
'--noEmit',
'--strict',
'--pretty',
'false',
'--skipLibCheck',
'false',
'--moduleResolution',
'bundler',
'--module',
'esnext',
'--target',
'es2022',
'--types',
'node',
'fixture.ts',
],
{ encoding: 'utf8' },
);
assert.equal(result.error, undefined);
assert.equal(result.stderr, '');
return result;
}

if (process.argv[2] === 'pack') {
const integration = `import * as attw from '@arethetypeswrong/core';
import * as volarTypeScript from '@volar/typescript';
const pack = {
entry: 'src/index.ts',
attw: { module: attw },
dts: { customLanguages: [{
extensionPatterns: [/\\.vue$/], volarTypeScript, createVolarPlugins: () => [],
}] },
};
export default defineConfig({ pack });`;
for (const imports of [
"import { defineConfig } from 'vite-plus';",
"import { defineConfig } from 'vite-plus/config'; import type {} from 'vite-plus/pack';",
]) {
const result = typecheck(`${imports}\n${integration}`);
assert.equal(result.status, 0, result.stdout);
}
const invalid = typecheck(`import { defineConfig } from 'vite-plus/config';
import type { PackUserConfig } from 'vite-plus/pack';
const pack: PackUserConfig = { entry: 42 };
export default defineConfig({ pack });`);
assert.notEqual(invalid.status, 0);
assert.match(invalid.stdout, /Type 'number' is not assignable/);
assert.doesNotMatch(invalid.stdout, /node_modules|Cannot find module/);

const { defineConfig } = await import('vite-plus');
const attw = await import('@arethetypeswrong/core');
const volarTypeScript = await import('@volar/typescript');
const pack = { attw: { module: attw }, dts: { customLanguages: [{ volarTypeScript }] } };
const result = defineConfig({ pack });
assert.equal(result.pack, pack);
assert.equal(result.pack.attw.module, attw);
assert.equal(result.pack.dts.customLanguages[0].volarTypeScript, volarTypeScript);
console.log(
'Original and explicit pack configuration preserve injected module types and identity',
);
} else {
for (const name of [
'@arethetypeswrong/core',
'@vitejs/devtools/cli-commands',
'publint',
'unplugin-unused',
'@volar/typescript',
]) {
assert.throws(() => require.resolve(name), { code: 'MODULE_NOT_FOUND' });
}
const result = typecheck(readFileSync('vite.config.ts', 'utf8'));
assert.equal(result.status, 0, result.stdout);

for (const [config, message] of [
["pack: { entry: 'src/index.ts' }", /'pack' does not exist/],
["fmt: { semi: 'yes' }", /not assignable to type 'boolean/],
]) {
const invalid = typecheck(`import { defineConfig } from 'vite-plus/config';
export default defineConfig({ ${config} });`);
assert.notEqual(invalid.status, 0);
assert.match(invalid.stdout, message);
assert.doesNotMatch(invalid.stdout, /node_modules|Cannot find module/);
}

const root = await import('vite-plus');
const config = await import('vite-plus/config');
assert.deepEqual(Object.keys(config), Object.keys(root));
for (const name of Object.keys(root)) {
assert.equal(config[name], root[name], name);
}
assert.equal(require('vite-plus/config'), require('vite-plus'));
const plugin = { name: 'consumer' };
const configured = config.defineConfig({ plugins: [plugin] });
assert.deepEqual(
configured.plugins.map(({ name }) => name),
[
'vite-plus:vitest-resolver',
'vite-plus:auto-inline-matcher-deps',
'vite-plus:coverage-version-guard',
'consumer',
],
);
assert.equal(configured.plugins.at(-1), plugin);
console.log(
'App configuration is strict without pack; ESM, CommonJS, and native plugins retain identity',
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineConfig } from 'vite-plus/config';

export default defineConfig({
build: { sourcemap: true },
fmt: { semi: true },
lint: { rules: { 'no-debugger': 'error' } },
test: { include: ['tests/**/*.test.ts'] },
run: { tasks: { verify: { command: 'node verify.mjs' } } },
});

throw new Error('Static tasks must not evaluate the Vite config.');
86 changes: 80 additions & 6 deletions crates/vp_migration/src/vite_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ fn merge_json_config_content(

// Apply the transformation
let (content, updated) = ast_grep::apply_rules(vite_config_content, &rule_yaml)?;
let content =
if updated && config_key == "pack" { enable_pack_config_types(content) } else { content };

Ok(MergeResult { content, updated, uses_function_callback })
}
Expand Down Expand Up @@ -555,7 +557,7 @@ fn has_conflicting_lazy_plugins_binding(content: &str) -> bool {
continue;
}
let text = node.text();
if imports_from_vite_plus(&text) {
if vite_plus_import_source(&node).is_some() {
continue;
}
if import_binds_lazy_plugins(&text) {
Expand Down Expand Up @@ -679,6 +681,7 @@ fn ensure_lazy_plugins_import(content: &str) -> String {
let root = grep.root();
let mut import_insert_at = None;
let mut value_import_replacement = None;
let mut import_source = "vite-plus";

for node in root.dfs() {
if node.kind() != "import_statement" {
Expand All @@ -688,7 +691,11 @@ fn ensure_lazy_plugins_import(content: &str) -> String {
Some(import_insert_at.map_or(node.range().end, |end: usize| end.max(node.range().end)));

let text = node.text();
if !imports_from_vite_plus(&text) || text.trim_start().starts_with("import type") {
let Some(source) = vite_plus_import_source(&node) else { continue };
if source == "vite-plus/config" {
import_source = "vite-plus/config";
}
if text.trim_start().starts_with("import type") {
continue;
}
let Some(open_brace) = text.find('{') else { continue };
Expand Down Expand Up @@ -725,7 +732,7 @@ fn ensure_lazy_plugins_import(content: &str) -> String {
return updated;
}

let import_stmt = "import { lazyPlugins } from 'vite-plus';";
let import_stmt = format!("import {{ lazyPlugins }} from '{import_source}';");
if let Some(insert_at) = import_insert_at {
let mut updated = content.to_owned();
updated.insert_str(insert_at, &format!("\n{import_stmt}"));
Expand All @@ -735,8 +742,34 @@ fn ensure_lazy_plugins_import(content: &str) -> String {
}
}

fn imports_from_vite_plus(import_statement: &str) -> bool {
import_statement.contains("from 'vite-plus'") || import_statement.contains("from \"vite-plus\"")
fn vite_plus_import_source<D: Doc>(node: &Node<'_, D>) -> Option<&'static str> {
match node.field("source")?.text().as_ref() {
"'vite-plus'" | "\"vite-plus\"" => Some("vite-plus"),
"'vite-plus/config'" | "\"vite-plus/config\"" => Some("vite-plus/config"),
_ => None,
}
}

fn enable_pack_config_types(mut content: String) -> String {
let grep = SupportLang::TypeScript.ast_grep(&content);
let mut edits = grep
.root()
.dfs()
.filter_map(|node| {
if node.kind() != "import_statement"
|| vite_plus_import_source(&node) != Some("vite-plus/config")
{
return None;
}
let source = node.field("source")?;
Some((source.range(), source.text().replace("vite-plus/config", "vite-plus")))
})
.collect::<Vec<_>>();
edits.sort_by_key(|(range, _)| std::cmp::Reverse(range.start));
for (range, replacement) in edits {
content.replace_range(range, &replacement);
}
content
}

fn has_lazy_plugins_specifier(specifiers: &str) -> bool {
Expand Down Expand Up @@ -988,7 +1021,9 @@ fn merge_tsdown_config_content(

// Step 2: Add pack: tsdownConfig to defineConfig
let pack_rule = generate_merge_rule("tsdownConfig", "pack");
let (final_content, _) = ast_grep::apply_rules(&content_with_import, &pack_rule)?;
let (final_content, pack_added) = ast_grep::apply_rules(&content_with_import, &pack_rule)?;
let final_content =
if pack_added { enable_pack_config_types(final_content) } else { final_content };

Ok(MergeResult { content: final_content, updated: true, uses_function_callback })
}
Expand Down Expand Up @@ -2212,6 +2247,26 @@ export default {
);
}

#[test]
fn test_wrap_lazy_plugins_preserves_config_entry() {
for import in [
"import { defineConfig } from 'vite-plus/config';",
"import { defineConfig } from\n'vite-plus/config';",
"import { defineConfig, lazyPlugins } from 'vite-plus/config';",
"import type { UserConfig } from 'vite-plus/config';",
"import { defineConfig /* keep */ } from 'vite-plus/config';",
] {
let content =
format!("{import}\nexport default defineConfig({{ plugins: [react()] }});");
let result = wrap_lazy_plugins_content(&content, None).unwrap();
assert!(result.updated);
assert!(result.content.contains("plugins: lazyPlugins(() => [react()])"));
assert!(!result.content.contains("from 'vite-plus'"));
let second = wrap_lazy_plugins_content(&result.content, None).unwrap();
assert!(!second.updated);
}
}

#[test]
fn test_wrap_lazy_plugins_handles_multiline_imports() {
let vite_config = r#"import {
Expand Down Expand Up @@ -2375,6 +2430,25 @@ export default defineConfig({
assert_eq!(second.content, first.content);
}

#[test]
fn test_merging_pack_enables_pack_configuration_types() {
let config = r#"import { defineConfig } from 'vite-plus/config';
import type { UserConfig } from 'vite-plus/config';
export type Config = UserConfig;
const label = 'vite-plus/config';
export default defineConfig({});"#;
for result in [
merge_json_config_content(config, "{ entry: 'src/index.ts' }", "pack").unwrap(),
merge_tsdown_config_content(config, "./tsdown.config.ts").unwrap(),
] {
assert!(result.updated);
assert!(result.content.contains("pack:"));
assert!(result.content.contains("import { defineConfig } from 'vite-plus';"));
assert!(result.content.contains("import type { UserConfig } from 'vite-plus';"));
assert!(result.content.contains("const label = 'vite-plus/config';"));
}
}

#[test]
fn test_merge_tsdown_config_content_simple() {
let vite_config = r#"import { defineConfig } from 'vite-plus';
Expand Down
14 changes: 13 additions & 1 deletion crates/vp_static_config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use rustc_hash::FxHashMap;
use vt_path::AbsolutePath;

/// Packages whose `defineConfig` helpers preserve top-level config fields.
const TRUSTED_DEFINE_CONFIG_PACKAGES: &[&str] = &["vite-plus", "vite"];
const TRUSTED_DEFINE_CONFIG_PACKAGES: &[&str] = &["vite-plus", "vite-plus/config", "vite"];
/// The name of the config helper static extraction trusts.
const DEFINE_CONFIG: &str = "defineConfig";

Expand Down Expand Up @@ -746,6 +746,18 @@ mod tests {
assert!(result.get("run").is_none());
}

#[test]
fn define_config_from_config_entry_is_static() {
for source in [
"import { defineConfig } from 'vite-plus/config'; export default defineConfig({ run: { cacheScripts: true } });",
"const { defineConfig } = require('vite-plus/config'); module.exports = defineConfig({ run: { cacheScripts: true } });",
"const defineConfig = require('vite-plus/config').defineConfig; module.exports = defineConfig({ run: { cacheScripts: true } });",
] {
let result = parse_js_ts_config(source, "js");
assert_json(&result, "run", serde_json::json!({ "cacheScripts": true }));
}
}

#[test]
fn define_config_import_from_other_package_is_non_static() {
let result = parse(
Expand Down
Loading
Loading