Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5d3469a
feat(report): add frameworkShimAdded field to MigrationReport
naokihaba Apr 17, 2026
23fb9be
feat(framework): add framework detection and shim functionality for V…
naokihaba Apr 17, 2026
7b8f9ae
feat(migration): add framework shim confirmation and handling for Typ…
naokihaba Apr 17, 2026
95c9bf1
feat(framework): add framework detection and shim functionality for V…
naokihaba Apr 17, 2026
9870638
feat(migration): add initial configuration files for Vue framework shim
naokihaba Apr 17, 2026
a071bb8
docs(migrator): update comments regarding Svelte file handling and mo…
naokihaba Apr 17, 2026
0e816f8
chore: add .gitkeep file to migration-framework-shim-vue directory
naokihaba Apr 17, 2026
9ebcfd5
feat(migration): update snap.txt to reflect TypeScript shim addition …
naokihaba Apr 17, 2026
497308d
feat(migration): add initial Astro framework shim configuration and r…
naokihaba Apr 17, 2026
9bb73f6
feat(framework): add framework detection and shim installation during…
naokihaba Apr 17, 2026
760d754
feat(test): add integration tests for framework shim creation flow
naokihaba Apr 17, 2026
278796b
feat(framework): add initial Vue framework shim configuration and rel…
naokihaba Apr 17, 2026
38d5f0b
style(tests): format code for better readability in migrator.spec.ts
naokihaba Apr 17, 2026
82718b3
feat(astro): add initial steps and snapshot for Astro framework shim …
naokihaba Apr 17, 2026
4208936
refactor(framework): change detectFramework to return an array of fra…
naokihaba Apr 17, 2026
03a42b2
test(framework): update detectFramework tests to return arrays of fra…
naokihaba Apr 17, 2026
f8fb0f3
refactor(framework): update framework shim handling to support multip…
naokihaba Apr 17, 2026
7dafa42
feat(migration): add initial migration framework shim for Astro and Vue
naokihaba Apr 17, 2026
7a0e29b
style: reorder dependencies in package.json and format code for bette…
naokihaba Apr 17, 2026
7112e7e
feat(migration): enhance framework shim detection and addition for wo…
naokihaba Apr 17, 2026
d2624ad
fix(migration): ensure framework shim is added only if detected in th…
naokihaba Apr 17, 2026
64cfdaf
refactor(migration): improve framework shim detection logic for bette…
naokihaba Apr 17, 2026
75b1d19
style(migration): format code for better readability in collectMigrat…
naokihaba Apr 17, 2026
aa3b53b
Merge branch 'main' into main
naokihaba Apr 19, 2026
5997791
feat(snap-tests): add installation and formatting checks for Astro an…
naokihaba Apr 20, 2026
95619fa
Merge branch 'main' into main
naokihaba Apr 20, 2026
2eeefbc
Merge branch 'main' into main
fengmk2 Apr 21, 2026
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,7 @@
{
"name": "migration-framework-shim-vue",
"devDependencies": {
"vite": "^7.0.0",
"vue": "^3.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
> vp migrate --no-interactive --no-hooks # migration should add Vue shim when vue dependency is detected
VITE+ - The Unified Toolchain for the Web

◇ Migrated . to Vite+<repeat>
• Node <semver> pnpm <semver>
✓ Dependencies installed in <variable>ms
• 1 config update applied

[1]> cat src/env.d.ts # check Vue shim was written
cat: src/env.d.ts: No such file or directory
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"ignoredPlatforms": ["win32"],
"env": {
"VITE_DISABLE_AUTO_INSTALL": "1",
"CI": "",
"VP_SKIP_INSTALL": ""
},
"commands": [
"vp migrate --no-interactive --no-hooks # migration should add Vue shim when vue dependency is detected",
"cat src/env.d.ts # check Vue shim was written"
]
}
115 changes: 115 additions & 0 deletions packages/cli/src/migration/__tests__/migrator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { PackageManager } from '../../types/index.js';
import { createMigrationReport } from '../report.js';

// Mock VITE_PLUS_VERSION to a stable value for snapshot tests.
// When tests run via `vp test`, the env var is injected with the actual version,
Expand All @@ -21,6 +22,9 @@ const {
parseNvmrcVersion,
detectNodeVersionManagerFile,
migrateNodeVersionManagerFile,
detectFramework,
hasFrameworkShim,
addFrameworkShim,
} = await import('../migrator.js');

describe('rewritePackageJson', () => {
Expand Down Expand Up @@ -304,6 +308,7 @@ describe('migrateNodeVersionManagerFile', () => {
prettierMigrated: false,
nodeVersionFileMigrated: false,
gitHooksConfigured: false,
frameworkShimAdded: false,
warnings: [],
manualSteps: [],
};
Expand Down Expand Up @@ -334,6 +339,7 @@ describe('migrateNodeVersionManagerFile', () => {
prettierMigrated: false,
nodeVersionFileMigrated: false,
gitHooksConfigured: false,
frameworkShimAdded: false,
warnings: [],
manualSteps: [],
};
Expand Down Expand Up @@ -366,6 +372,7 @@ describe('migrateNodeVersionManagerFile', () => {
prettierMigrated: false,
nodeVersionFileMigrated: false,
gitHooksConfigured: false,
frameworkShimAdded: false,
warnings: [],
manualSteps: [],
};
Expand Down Expand Up @@ -401,6 +408,7 @@ describe('migrateNodeVersionManagerFile', () => {
prettierMigrated: false,
nodeVersionFileMigrated: false,
gitHooksConfigured: false,
frameworkShimAdded: false,
warnings: [],
manualSteps: [],
};
Expand Down Expand Up @@ -638,3 +646,110 @@ describe('rewriteMonorepo bun catalog', () => {
expect(workspaces.packages).toEqual(['packages/*']);
});
});

describe('framework shim', () => {
let tmpDir: string;

beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-test-'));
});

afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});

describe('detectFramework', () => {
it('returns vue when vue is in devDependencies', () => {
fs.writeFileSync(
path.join(tmpDir, 'package.json'),
JSON.stringify({ devDependencies: { vue: '^3.0.0' } }),
);
expect(detectFramework(tmpDir)).toBe('vue');
});

it('returns astro when astro is in devDependencies', () => {
fs.writeFileSync(
path.join(tmpDir, 'package.json'),
JSON.stringify({ devDependencies: { astro: '^4.0.0' } }),
);
expect(detectFramework(tmpDir)).toBe('astro');
});

it('returns null when no framework dependency is present', () => {
fs.writeFileSync(
path.join(tmpDir, 'package.json'),
JSON.stringify({ devDependencies: { vite: '^7.0.0' } }),
);
expect(detectFramework(tmpDir)).toBeNull();
});

it('returns null when package.json does not exist', () => {
expect(detectFramework(tmpDir)).toBeNull();
});
});

describe('hasFrameworkShim', () => {
it('returns true when src/env.d.ts contains vue shim', () => {
const srcDir = path.join(tmpDir, 'src');
fs.mkdirSync(srcDir);
fs.writeFileSync(
path.join(srcDir, 'env.d.ts'),
"declare module '*.vue' { export default {} }\n",
);
expect(hasFrameworkShim(tmpDir, 'vue')).toBe(true);
});

it('returns false when src/env.d.ts does not contain vue shim', () => {
const srcDir = path.join(tmpDir, 'src');
fs.mkdirSync(srcDir);
fs.writeFileSync(path.join(srcDir, 'env.d.ts'), '/// <reference types="vite-plus/client" />\n');
expect(hasFrameworkShim(tmpDir, 'vue')).toBe(false);
});

it('returns false when env.d.ts does not exist', () => {
expect(hasFrameworkShim(tmpDir, 'vue')).toBe(false);
});

it('returns true when root env.d.ts contains astro/client reference', () => {
fs.writeFileSync(
path.join(tmpDir, 'env.d.ts'),
'/// <reference types="astro/client" />\n',
);
expect(hasFrameworkShim(tmpDir, 'astro')).toBe(true);
});
});

describe('addFrameworkShim', () => {
it('creates src/env.d.ts with vue shim when src/ exists and no env.d.ts', () => {
fs.mkdirSync(path.join(tmpDir, 'src'));
addFrameworkShim(tmpDir, 'vue');
const content = fs.readFileSync(path.join(tmpDir, 'src', 'env.d.ts'), 'utf-8');
expect(content).toContain("declare module '*.vue'");
expect(content).toContain('DefineComponent');
});

it('creates root env.d.ts with vue shim when no src/ dir', () => {
addFrameworkShim(tmpDir, 'vue');
const content = fs.readFileSync(path.join(tmpDir, 'env.d.ts'), 'utf-8');
expect(content).toContain("declare module '*.vue'");
});

it('appends vue shim to existing src/env.d.ts', () => {
const srcDir = path.join(tmpDir, 'src');
fs.mkdirSync(srcDir);
const existing = '/// <reference types="vite-plus/client" />\n';
fs.writeFileSync(path.join(srcDir, 'env.d.ts'), existing);
addFrameworkShim(tmpDir, 'vue');
const content = fs.readFileSync(path.join(srcDir, 'env.d.ts'), 'utf-8');
expect(content).toContain('/// <reference types="vite-plus/client" />');
expect(content).toContain("declare module '*.vue'");
});

it('sets frameworkShimAdded on report', () => {
fs.mkdirSync(path.join(tmpDir, 'src'));
const report = createMigrationReport();
addFrameworkShim(tmpDir, 'vue', report);
expect(report.frameworkShimAdded).toBe(true);
});
});
});
51 changes: 50 additions & 1 deletion packages/cli/src/migration/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,14 @@ import { accent, log, muted } from '../utils/terminal.ts';
import type { PackageDependencies } from '../utils/types.ts';
import { detectWorkspace } from '../utils/workspace.ts';
import {
addFrameworkShim,
checkVitestVersion,
checkViteVersion,
detectEslintProject,
detectFramework,
detectNodeVersionManagerFile,
detectPrettierProject,
hasFrameworkShim,
installGitHooks,
mergeViteConfigFiles,
migrateEslintToOxlint,
Expand All @@ -54,6 +57,7 @@ import {
preflightGitHooksSetup,
rewriteMonorepo,
rewriteStandaloneProject,
type Framework,
type NodeVersionManagerDetection,
} from './migrator.ts';
import { createMigrationReport, type MigrationReport } from './report.ts';
Expand Down Expand Up @@ -204,6 +208,30 @@ async function confirmNodeVersionFileMigration(
return true;
}

async function confirmFrameworkShim(
framework: Framework,
interactive: boolean,
): Promise<boolean> {
const frameworkNames: Record<Framework, string> = { vue: 'Vue', astro: 'Astro' };
const name = frameworkNames[framework];
if (interactive) {
const confirmed = await prompts.confirm({
message:
`Add TypeScript shim for ${name} component files (*.${framework})?\n ` +
styleText(
'gray',
`Lets TypeScript recognize .${framework} files until vp check fully supports them.`,
),
initialValue: true,
});
if (prompts.isCancel(confirmed)) {
cancelAndExit();
}
return confirmed;
}
return true;
}

const helpMessage = renderCliDoc({
usage: 'vp migrate [PATH] [OPTIONS]',
summary:
Expand Down Expand Up @@ -356,6 +384,7 @@ interface MigrationPlan {
prettierConfigFile?: string;
migrateNodeVersionFile: boolean;
nodeVersionDetection?: NodeVersionManagerDetection;
frameworkShimFramework?: Framework;
}

async function collectMigrationPlan(
Expand Down Expand Up @@ -493,6 +522,16 @@ async function collectMigrationPlan(
);
}

// 11. Framework shim detection + prompt
const detectedFramework = detectFramework(rootDir);
let frameworkShimFramework: Framework | undefined;
if (detectedFramework && !hasFrameworkShim(rootDir, detectedFramework)) {
const addShim = await confirmFrameworkShim(detectedFramework, options.interactive);
if (addShim) {
frameworkShimFramework = detectedFramework;
}
}

const plan: MigrationPlan = {
packageManager,
shouldSetupHooks,
Expand All @@ -506,6 +545,7 @@ async function collectMigrationPlan(
prettierConfigFile: prettierProject.configFile,
migrateNodeVersionFile,
nodeVersionDetection,
frameworkShimFramework,
};

return plan;
Expand Down Expand Up @@ -588,6 +628,9 @@ function showMigrationSummary(options: {
if (report.gitHooksConfigured) {
log(`${styleText('gray', '•')} Git hooks configured`);
}
if (report.frameworkShimAdded) {
log(`${styleText('gray', '•')} TypeScript shim added for framework component files`);
}
if (report.warnings.length > 0) {
log(`${styleText('yellow', '!')} Warnings:`);
for (const warning of report.warnings) {
Expand Down Expand Up @@ -807,7 +850,13 @@ async function executeMigrationPlan(
silent: true,
});

// 11. Reinstall after migration
// 11. Add framework shim if requested
if (plan.frameworkShimFramework) {
updateMigrationProgress('Adding TypeScript shim');
addFrameworkShim(workspaceInfo.rootDir, plan.frameworkShimFramework, report);
}

// 12. Reinstall after migration
// npm needs --force to re-resolve packages with newly added overrides,
// otherwise the stale lockfile prevents override resolution.
const installArgs =
Expand Down
82 changes: 82 additions & 0 deletions packages/cli/src/migration/migrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,88 @@ function cleanupDeprecatedTsconfigOptions(
}
}

// Svelte is intentionally excluded: @sveltejs/vite-plugin-svelte and svelte-check handle
// .svelte file types automatically. No env.d.ts shim is needed or documented officially.
// https://svelte.dev/docs/svelte/typescript
export type Framework = 'vue' | 'astro';
Comment thread
fengmk2 marked this conversation as resolved.

const FRAMEWORK_SHIMS: Record<Framework, string> = {
// https://vuejs.org/guide/typescript/overview#volar-takeover-mode
vue: [
"declare module '*.vue' {",
" import type { DefineComponent } from 'vue';",
' const component: DefineComponent<{}, {}, unknown>;',
' export default component;',
'}',
Comment thread
fengmk2 marked this conversation as resolved.
].join('\n'),
// astro/client is the pre-v4.14 form; v4.14+ prefers `/// <reference path="../.astro/types.d.ts" />`
// but .astro/types.d.ts is generated at build time and may not exist yet after migration.
// astro/client remains valid and is still used in official Astro integrations.
// https://docs.astro.build/en/guides/typescript/#extending-global-types
astro: '/// <reference types="astro/client" />',
};

export function detectFramework(projectPath: string): Framework | null {
const packageJsonPath = path.join(projectPath, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
return null;
}
const pkg = readJsonFile(packageJsonPath) as {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
};
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
for (const framework of ['vue', 'astro'] as const) {
if (allDeps[framework]) {
return framework;
Comment thread
naokihaba marked this conversation as resolved.
Outdated
}
}
return null;
}

function getEnvDtsPath(projectPath: string): string {
const srcEnvDts = path.join(projectPath, 'src', 'env.d.ts');
const rootEnvDts = path.join(projectPath, 'env.d.ts');
for (const candidate of [srcEnvDts, rootEnvDts]) {
Comment thread
naokihaba marked this conversation as resolved.
if (fs.existsSync(candidate)) {
return candidate;
}
}
return fs.existsSync(path.join(projectPath, 'src')) ? srcEnvDts : rootEnvDts;
}

export function hasFrameworkShim(projectPath: string, framework: Framework): boolean {
const envDtsPath = getEnvDtsPath(projectPath);
if (!fs.existsSync(envDtsPath)) {
return false;
}

const content = fs.readFileSync(envDtsPath, 'utf-8');
if (framework === 'astro') {
return content.includes('astro/client');
}
return content.includes(`'*.${framework}'`) || content.includes(`"*.${framework}"`);
}

export function addFrameworkShim(
projectPath: string,
framework: Framework,
report?: MigrationReport,
): void {
const envDtsPath = getEnvDtsPath(projectPath);
const shim = FRAMEWORK_SHIMS[framework];
if (fs.existsSync(envDtsPath)) {
const existing = fs.readFileSync(envDtsPath, 'utf-8');
fs.writeFileSync(envDtsPath, `${existing.trimEnd()}\n\n${shim}\n`, 'utf-8');
} else {
fs.mkdirSync(path.dirname(envDtsPath), { recursive: true });
fs.writeFileSync(envDtsPath, `${shim}\n`, 'utf-8');
}
if (report) {
report.frameworkShimAdded = true;
}
}

/**
* Rewrite standalone project to add vite-plus dependencies
* @param projectPath - The path to the project
Expand Down
Loading
Loading