diff --git a/.changeset/safe-tags-add.md b/.changeset/safe-tags-add.md new file mode 100644 index 000000000000..3606829fc799 --- /dev/null +++ b/.changeset/safe-tags-add.md @@ -0,0 +1,7 @@ +--- +'@astrojs/internal-helpers': patch +'astro': patch +'create-astro': patch +--- + +Allows Astro CLI commands to install tagged package versions diff --git a/packages/internal-helpers/src/cli.ts b/packages/internal-helpers/src/cli.ts index 09889d96e168..2337596c687d 100644 --- a/packages/internal-helpers/src/cli.ts +++ b/packages/internal-helpers/src/cli.ts @@ -27,19 +27,28 @@ export function validatePackageName(packageName: string): boolean { } /** - * Validates a package name and throws an error if invalid. + * Validates a package name with an optional tag or version and throws an error if invalid. * - * @param packageName - The package name to validate - * @throws {Error} If the package name is invalid + * @param packageName - The package name and optional tag or version to validate + * @throws {Error} If the package name, tag, or version is invalid * * @example * ```ts * assertValidPackageName('react'); // OK + * assertValidPackageName('react@latest'); // OK * assertValidPackageName('react; whoami'); // throws Error * ``` */ export function assertValidPackageName(packageName: string): asserts packageName is string { - if (!validatePackageName(packageName)) { + const tagSeparator = packageName.lastIndexOf('@'); + const hasTag = tagSeparator > 0; + const untaggedPackageName = hasTag ? packageName.slice(0, tagSeparator) : packageName; + const tag = hasTag ? packageName.slice(tagSeparator + 1) : undefined; + + if ( + !validatePackageName(untaggedPackageName) || + (tag !== undefined && !validatePackageName(tag)) + ) { throw new Error( `Invalid package name "${packageName}". Package names must follow npm naming rules: ` + `lowercase letters, numbers, hyphens, underscores, and dots. ` + diff --git a/packages/internal-helpers/test/cli.test.ts b/packages/internal-helpers/test/cli.test.ts new file mode 100644 index 000000000000..21e5060729f1 --- /dev/null +++ b/packages/internal-helpers/test/cli.test.ts @@ -0,0 +1,19 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { assertValidPackageName } from '../src/cli.ts'; + +describe('assertValidPackageName', () => { + it('accepts package names with safe tags and versions', () => { + assert.doesNotThrow(() => assertValidPackageName('react@latest')); + assert.doesNotThrow(() => assertValidPackageName('@astrojs/react@latest')); + assert.doesNotThrow(() => assertValidPackageName('@astrojs/react@5.0.0-beta.1')); + }); + + it('rejects unsafe or malformed package specifiers', () => { + assert.throws(() => assertValidPackageName('react;whoami@latest')); + assert.throws(() => assertValidPackageName('react@latest;whoami')); + assert.throws(() => assertValidPackageName('react@$(whoami)')); + assert.throws(() => assertValidPackageName('react@latest@next')); + assert.throws(() => assertValidPackageName('@astrojs/react@')); + }); +});