Skip to content

Commit 38265d4

Browse files
naokihabafengmk2
andauthored
fix: add troubleshooting tips for npm template not found error (#1055)
### Linked issue resolves #828 ### Description The behavior when specifying an invalid template name (e.g., vite-plus-app) with the vp create command has been improved. With this update, the CLI now checks the npm registry before executing the package manager. If the template is not found (HTTP 404 error), it will display an error message and terminate the process immediately. --------- Co-authored-by: MK (fengmk2) <fengmk2@gmail.com>
1 parent 611729c commit 38265d4

3 files changed

Lines changed: 89 additions & 0 deletions

File tree

packages/cli/src/create/templates/remote.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as prompts from '@voidzero-dev/vite-plus-prompts';
22
import colors from 'picocolors';
33

44
import type { WorkspaceInfo } from '../../types/index.js';
5+
import { checkNpmPackageExists } from '../../utils/package.js';
56
import {
67
type ExecutionResult,
78
formatDlxCommand,
@@ -40,6 +41,18 @@ export async function executeRemoteTemplate(
4041
} else {
4142
// TODO: prompt for project name if not provided for degit
4243
// Template not found - use package manager runner (npx/pnpm dlx/etc.)
44+
if (!isGitHubTemplate) {
45+
// templateInfo.command is the npm package name (e.g. "create-vite", "@tanstack/create-start")
46+
const packageExists = await checkNpmPackageExists(templateInfo.command);
47+
if (!packageExists) {
48+
if (!silent) {
49+
prompts.log.error(
50+
`Template "${templateInfo.command}" not found on npm. Run ${yellow('vp create --list')} to see available templates.`,
51+
);
52+
}
53+
return { exitCode: 1 };
54+
}
55+
}
4356
result = await runRemoteTemplateCommand(
4457
workspaceInfo,
4558
workspaceInfo.rootDir,
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { afterEach, describe, expect, it, vi } from 'vitest';
2+
3+
import { checkNpmPackageExists } from '../package.js';
4+
5+
describe('checkNpmPackageExists', () => {
6+
afterEach(() => {
7+
vi.restoreAllMocks();
8+
});
9+
10+
it('returns true when package exists (200)', async () => {
11+
vi.spyOn(globalThis, 'fetch').mockResolvedValue({ status: 200, ok: true } as Response);
12+
expect(await checkNpmPackageExists('create-vite')).toBe(true);
13+
});
14+
15+
it('returns false when package does not exist (404)', async () => {
16+
vi.spyOn(globalThis, 'fetch').mockResolvedValue({ status: 404, ok: false } as Response);
17+
expect(await checkNpmPackageExists('create-vite-plus-app')).toBe(false);
18+
});
19+
20+
it('returns true on network error', async () => {
21+
vi.spyOn(globalThis, 'fetch').mockRejectedValue(new TypeError('fetch failed'));
22+
expect(await checkNpmPackageExists('create-vite')).toBe(true);
23+
});
24+
25+
it('strips version from unscoped package name', async () => {
26+
const mockFetch = vi
27+
.spyOn(globalThis, 'fetch')
28+
.mockResolvedValue({ status: 200, ok: true } as Response);
29+
await checkNpmPackageExists('create-vite@latest');
30+
expect(mockFetch).toHaveBeenCalledWith(
31+
'https://registry.npmjs.org/create-vite',
32+
expect.objectContaining({ method: 'HEAD' }),
33+
);
34+
});
35+
36+
it('strips version from scoped package name', async () => {
37+
const mockFetch = vi
38+
.spyOn(globalThis, 'fetch')
39+
.mockResolvedValue({ status: 200, ok: true } as Response);
40+
await checkNpmPackageExists('@tanstack/create-start@latest');
41+
expect(mockFetch).toHaveBeenCalledWith(
42+
'https://registry.npmjs.org/@tanstack/create-start',
43+
expect.objectContaining({ method: 'HEAD' }),
44+
);
45+
});
46+
47+
it('does not strip scope from scoped package without version', async () => {
48+
const mockFetch = vi
49+
.spyOn(globalThis, 'fetch')
50+
.mockResolvedValue({ status: 200, ok: true } as Response);
51+
await checkNpmPackageExists('@tanstack/create-start');
52+
expect(mockFetch).toHaveBeenCalledWith(
53+
'https://registry.npmjs.org/@tanstack/create-start',
54+
expect.objectContaining({ method: 'HEAD' }),
55+
);
56+
});
57+
});

packages/cli/src/utils/package.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,22 @@ export function hasVitePlusDependency(
6363
) {
6464
return Boolean(pkg?.dependencies?.[VITE_PLUS_NAME] || pkg?.devDependencies?.[VITE_PLUS_NAME]);
6565
}
66+
67+
/**
68+
* Check if an npm package exists in the public registry.
69+
* Returns true if the package exists or if the check could not be performed (network error, timeout).
70+
* Returns false only if the registry definitively responds with 404.
71+
*/
72+
export async function checkNpmPackageExists(packageName: string): Promise<boolean> {
73+
const atIndex = packageName.indexOf('@', 2);
74+
const name = atIndex === -1 ? packageName : packageName.slice(0, atIndex);
75+
try {
76+
const response = await fetch(`https://registry.npmjs.org/${name}`, {
77+
method: 'HEAD',
78+
signal: AbortSignal.timeout(3000),
79+
});
80+
return response.status !== 404;
81+
} catch {
82+
return true; // Network error or timeout - let the package manager handle it
83+
}
84+
}

0 commit comments

Comments
 (0)