Skip to content

Commit 12bf2bb

Browse files
committed
fix(migration): gate staged migration on hook preflight, normalize hooks dir, surface staged errors
- Extract preflightGitHooksSetup() from setupGitHooks() to run deterministic checks before migration rewrites lint-staged config. When hooks setup would fail (other hook tools, subdirectory project, unsupported lint-staged config), migration now preserves lint-staged scripts and config in package.json instead of rewriting them. - Fix hookScript() path segment counting: filter out '.' segments so paths like "./.config/husky" produce the same depth as ".config/husky". - Surface real resolveViteConfig errors in `vp staged` instead of masking them as "No staged config found".
1 parent 15a3882 commit 12bf2bb

13 files changed

Lines changed: 190 additions & 80 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
> printf 'export default {\n staged: {\n "*.ts": "vp check --fix",\n },\n // syntax error: missing closing brace\n' > vite.config.ts
2+
[1]> vp staged # should show actual config error, not 'No staged config found'
3+
failed to load config from <cwd>/vite.config.ts
4+
Failed to load vite.config: Build failed with 1 error:
5+
6+
[PARSE_ERROR] Error: Unexpected token
7+
╭─[ vite.config.ts:5:42 ]
8+
9+
5 │ // syntax error: missing closing brace
10+
│ │
11+
│ ╰─
12+
───╯
13+
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"commands": [
3+
{
4+
"command": "printf 'export default {\\n staged: {\\n \"*.ts\": \"vp check --fix\",\\n },\\n // syntax error: missing closing brace\\n' > vite.config.ts",
5+
"ignoreOutput": true
6+
},
7+
"vp staged # should show actual config error, not 'No staged config found'"
8+
]
9+
}

packages/cli/snap-tests-global/migration-lint-staged-ts-config/snap.txt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@ pnpm@latest installing...
99

1010
pnpm@<semver> installed
1111

12-
✘ lint-staged.config.ts — please migrate to "staged" in vite.config.ts manually
13-
14-
Only "staged" in vite.config.ts is supported. See https://viteplus.dev/migration/#lint-staged
15-
1612
⚠ Unsupported lint-staged config format — skipping git hooks setup. Please configure git hooks manually.
1713

1814
Wrote agent instructions to AGENTS.md

packages/cli/snap-tests-global/migration-lintstagedrc-not-support/snap.txt

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,6 @@ pnpm@latest installing...
99

1010
pnpm@<semver> installed
1111

12-
✘ .lintstagedrc is not JSON format — please migrate to "staged" in vite.config.ts manually
13-
14-
✘ .lintstagedrc.yaml — please migrate to "staged" in vite.config.ts manually
15-
16-
✘ lint-staged.config.mjs — please migrate to "staged" in vite.config.ts manually
17-
18-
Only "staged" in vite.config.ts is supported. See https://viteplus.dev/migration/#lint-staged
19-
2012
⚠ Unsupported lint-staged config format — skipping git hooks setup. Please configure git hooks manually.
2113

2214
Wrote agent instructions to AGENTS.md
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "migration-other-hook-tool-with-lint-staged",
3+
"scripts": {
4+
"check-staged": "lint-staged"
5+
},
6+
"devDependencies": {
7+
"lint-staged": "^16.2.6",
8+
"simple-git-hooks": "^2.11.1",
9+
"vite": "^7.0.0"
10+
},
11+
"lint-staged": {
12+
"*.ts": "eslint --fix"
13+
}
14+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
> vp migrate --no-interactive # hooks should be skipped due to simple-git-hooks
2+
VITE+ - The Unified Toolchain for the Web
3+
4+
5+
Using default package manager: pnpm
6+
7+
pnpm@latest installing...
8+
9+
pnpm@<semver> installed
10+
11+
⚠ Detected simple-git-hooks — skipping git hooks setup. Please configure git hooks manually.
12+
13+
Wrote agent instructions to AGENTS.md
14+
✔ Migration completed!
15+
16+
17+
> cat package.json # lint-staged config and scripts should be preserved
18+
{
19+
"name": "migration-other-hook-tool-with-lint-staged",
20+
"scripts": {
21+
"check-staged": "lint-staged"
22+
},
23+
"devDependencies": {
24+
"lint-staged": "^16.2.6",
25+
"simple-git-hooks": "^2.11.1",
26+
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
27+
"vite-plus": "latest"
28+
},
29+
"lint-staged": {
30+
"*.ts": "eslint --fix"
31+
},
32+
"pnpm": {
33+
"overrides": {
34+
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
35+
"vitest": "npm:@voidzero-dev/vite-plus-test@latest"
36+
}
37+
},
38+
"packageManager": "pnpm@<semver>"
39+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"commands": [
3+
"vp migrate --no-interactive # hooks should be skipped due to simple-git-hooks",
4+
"cat package.json # lint-staged config and scripts should be preserved"
5+
]
6+
}

packages/cli/snap-tests-global/migration-subpath/snap.txt

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@ pnpm@latest installing...
99

1010
pnpm@<semver> installed
1111

12-
✔ Created vite.config.ts in foo/vite.config.ts
13-
14-
✔ Merged staged config into foo/vite.config.ts
15-
1612
⚠ Subdirectory project detected — skipping git hooks setup. Configure hooks at the repository root.
1713

1814
Wrote agent instructions to AGENTS.md
@@ -22,6 +18,15 @@ Wrote agent instructions to AGENTS.md
2218
> cat foo/package.json # check package.json
2319
{
2420
"name": "migration-subpath",
21+
"lint-staged": {
22+
"*.@(js|ts|tsx|yml|yaml|md|json|html|toml)": [
23+
"oxfmt --staged",
24+
"eslint --fix"
25+
],
26+
"*.@(js|ts|tsx)": [
27+
"oxlint --fix"
28+
]
29+
},
2530
"pnpm": {
2631
"overrides": {
2732
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
@@ -34,21 +39,8 @@ Wrote agent instructions to AGENTS.md
3439
"packageManager": "pnpm@<semver>"
3540
}
3641

37-
> cat foo/vite.config.ts # check vite.config.ts
38-
import { defineConfig } from 'vite-plus';
39-
40-
export default defineConfig({
41-
staged: {
42-
"*.@(js|ts|tsx|yml|yaml|md|json|html|toml)": [
43-
"vp fmt --staged",
44-
"eslint --fix"
45-
],
46-
"*.@(js|ts|tsx)": [
47-
"vp lint --fix"
48-
]
49-
},
50-
51-
});
42+
[1]> cat foo/vite.config.ts # check vite.config.ts
43+
cat: foo/vite.config.ts: No such file or directory
5244

5345
> git config --local core.hooksPath || echo 'core.hooksPath is not set' # should NOT be set
5446
core.hooksPath is not set
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { hookScript } from '../hooks.js';
4+
5+
function countDirnameCalls(script: string): number {
6+
// Count nested dirname calls in the `d=...` line
7+
const match = script.match(/^d=(.+)$/m);
8+
if (!match) {
9+
return 0;
10+
}
11+
return (match[1].match(/dirname/g) ?? []).length;
12+
}
13+
14+
describe('hookScript', () => {
15+
it('should compute correct depth for simple dir', () => {
16+
// ".vite-hooks" → 1 segment → depth 3
17+
const script = hookScript('.vite-hooks');
18+
expect(countDirnameCalls(script)).toBe(3);
19+
});
20+
21+
it('should compute correct depth for nested dir', () => {
22+
// ".config/husky" → 2 segments → depth 4
23+
const script = hookScript('.config/husky');
24+
expect(countDirnameCalls(script)).toBe(4);
25+
});
26+
27+
it('should handle ./ prefix correctly (bug case)', () => {
28+
// "./.config/husky" should produce same depth as ".config/husky"
29+
// Before fix: filter(Boolean) kept "." → 3 segments → depth 5 (wrong)
30+
// After fix: filter out "." → 2 segments → depth 4 (correct)
31+
const withDot = hookScript('./.config/husky');
32+
const withoutDot = hookScript('.config/husky');
33+
expect(countDirnameCalls(withDot)).toBe(countDirnameCalls(withoutDot));
34+
expect(countDirnameCalls(withDot)).toBe(4);
35+
});
36+
37+
it('should handle ./ prefix for simple dir', () => {
38+
// "./custom-hooks" should produce same depth as "custom-hooks"
39+
const withDot = hookScript('./custom-hooks');
40+
const withoutDot = hookScript('custom-hooks');
41+
expect(countDirnameCalls(withDot)).toBe(countDirnameCalls(withoutDot));
42+
expect(countDirnameCalls(withDot)).toBe(3);
43+
});
44+
});

packages/cli/src/config/hooks.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ function nestedDirname(depth: number): string {
3030

3131
// The shell script that dispatches to user-defined hooks in <dir>/
3232
// `depth` = number of path segments in `dir` + 2 (for `_` subdir + hook filename)
33-
function hookScript(dir: string): string {
33+
export function hookScript(dir: string): string {
3434
// Count segments: ".vite-hooks" → 1, ".config/husky" → 2
35-
const segments = dir.split('/').filter(Boolean).length;
35+
// Filter out empty strings and '.' to handle paths like "./.config/husky"
36+
const segments = dir.split('/').filter((s) => s !== '' && s !== '.').length;
3637
const depth = segments + 2; // +2 for _ subdir and hook filename
3738
const rootExpr = nestedDirname(depth);
3839
return `#!/usr/bin/env sh

0 commit comments

Comments
 (0)