diff --git a/packages/shadcn/package.json b/packages/shadcn/package.json index 5f122fc784c..13c0fb04008 100644 --- a/packages/shadcn/package.json +++ b/packages/shadcn/package.json @@ -82,6 +82,7 @@ "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.17.2", + "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", @@ -105,6 +106,7 @@ "stringify-object": "^5.0.0", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", + "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, diff --git a/packages/shadcn/src/commands/create.ts b/packages/shadcn/src/commands/create.ts index f47265fb8bb..647097918be 100644 --- a/packages/shadcn/src/commands/create.ts +++ b/packages/shadcn/src/commands/create.ts @@ -1,3 +1,4 @@ +import { basename, resolve } from "node:path" import path from "path" import { getPreset, getPresets, getRegistryItems } from "@/src/registry/api" import { configWithDefaults } from "@/src/registry/config" @@ -15,6 +16,7 @@ import dedent from "dedent" import open from "open" import prompts from "prompts" +import { validateNpmName } from "../utils/validate-pkg" import { initOptionsSchema, runInit } from "./init" const SHADCN_URL = "https://ui.shadcn.com" @@ -88,10 +90,13 @@ export const create = new Command() message: "What is your project named?", initial: opts.template ? `${opts.template}-app` : "my-app", format: (value: string) => value.trim(), - validate: (value: string) => - value.length > 128 - ? `Name should be less than 128 characters.` - : true, + validate: (name) => { + const validation = validateNpmName(basename(resolve(name))) + if (validation.valid) { + return true + } + return "Invalid project name: " + validation.problems[0] + }, }) if (!enteredName) { diff --git a/packages/shadcn/src/utils/validate-pkg.ts b/packages/shadcn/src/utils/validate-pkg.ts new file mode 100644 index 00000000000..1f278f981f0 --- /dev/null +++ b/packages/shadcn/src/utils/validate-pkg.ts @@ -0,0 +1,25 @@ +import validateProjectName from "validate-npm-package-name" + +type ValidateNpmNameResult = + | { + valid: true + } + | { + valid: false + problems: string[] + } + +export function validateNpmName(name: string): ValidateNpmNameResult { + const nameValidation = validateProjectName(name) + if (nameValidation.validForNewPackages) { + return { valid: true } + } + + return { + valid: false, + problems: [ + ...(nameValidation.errors || []), + ...(nameValidation.warnings || []), + ], + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 983f8cf7163..fb7e4229294 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -458,6 +458,9 @@ importers: '@modelcontextprotocol/sdk': specifier: ^1.17.2 version: 1.17.2 + '@types/validate-npm-package-name': + specifier: ^4.0.2 + version: 4.0.2 browserslist: specifier: ^4.26.2 version: 4.26.2 @@ -527,6 +530,9 @@ importers: tsconfig-paths: specifier: ^4.2.0 version: 4.2.0 + validate-npm-package-name: + specifier: ^7.0.1 + version: 7.0.1 zod: specifier: ^3.24.1 version: 3.25.76 @@ -3400,6 +3406,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/validate-npm-package-name@4.0.2': + resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} + '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -8061,6 +8070,10 @@ packages: validate-npm-package-name@3.0.0: resolution: {integrity: sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw==} + validate-npm-package-name@7.0.1: + resolution: {integrity: sha512-BM0Upcemlce8/9+HE+/VpWqn3u3mYh6Om/FEC8yPMnEHwf710fW5Q6fhjT1SQyRlZD1G9CJbgfH+rWgAcIvjlQ==} + engines: {node: ^20.17.0 || >=22.9.0} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -11287,6 +11300,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/validate-npm-package-name@4.0.2': {} + '@types/yauzl@2.10.3': dependencies: '@types/node': 20.19.10 @@ -12789,7 +12804,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-module-utils@2.12.1(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: @@ -12800,7 +12815,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.33.0(jiti@2.6.1)))(eslint@9.33.0(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: @@ -12822,7 +12837,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -12851,7 +12866,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.33.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.33.0(jiti@2.6.1)))(eslint@9.33.0(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -16878,6 +16893,8 @@ snapshots: dependencies: builtins: 1.0.3 + validate-npm-package-name@7.0.1: {} + vary@1.1.2: {} vaul@1.1.2(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):