diff --git a/package-lock.json b/package-lock.json index 49c3672..b8df25c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "@types/semver": "^7.5.2", "command-line-args": "5.2.1", "semver": "7.6.0", - "tar": "6.2.1" + "tar": "6.2.1", + "validate-npm-package-name": "^5.0.1" }, "bin": { "npm-publish": "bin/npm-publish.js" @@ -22,6 +23,7 @@ "@types/command-line-args": "^5.2.1", "@types/node": "^20.10.3", "@types/tar": "^6.1.6", + "@types/validate-npm-package-name": "^4.0.2", "@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/parser": "^7.2.0", "@vitest/coverage-istanbul": "^1.0.1", @@ -1535,6 +1537,12 @@ "integrity": "sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==", "dev": true }, + "node_modules/@types/validate-npm-package-name": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz", + "integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==", + "dev": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz", @@ -5743,6 +5751,14 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/vite": { "version": "5.2.7", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.7.tgz", @@ -7016,6 +7032,12 @@ "integrity": "sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==", "dev": true }, + "@types/validate-npm-package-name": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz", + "integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==", + "dev": true + }, "@typescript-eslint/eslint-plugin": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz", @@ -9929,6 +9951,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==" + }, "vite": { "version": "5.2.7", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.7.tgz", diff --git a/package.json b/package.json index c0a5f91..9b185b8 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@types/command-line-args": "^5.2.1", "@types/node": "^20.10.3", "@types/tar": "^6.1.6", + "@types/validate-npm-package-name": "^4.0.2", "@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/parser": "^7.2.0", "@vitest/coverage-istanbul": "^1.0.1", @@ -81,6 +82,7 @@ "@types/semver": "^7.5.2", "command-line-args": "5.2.1", "semver": "7.6.0", - "tar": "6.2.1" + "tar": "6.2.1", + "validate-npm-package-name": "^5.0.1" } } diff --git a/src/__tests__/normalize-options.test.ts b/src/__tests__/normalize-options.test.ts index 1c115dd..46220f3 100644 --- a/src/__tests__/normalize-options.test.ts +++ b/src/__tests__/normalize-options.test.ts @@ -134,7 +134,7 @@ describe("normalizeOptions", () => { }); }); - it("should validate tag value", () => { + it("should validate tag type", () => { expect(() => { subject.normalizeOptions(manifest, { token: "abc123", @@ -144,6 +144,16 @@ describe("normalizeOptions", () => { }).toThrow(errors.InvalidTagError); }); + it("should validate tag value", () => { + expect(() => { + subject.normalizeOptions(manifest, { + token: "abc123", + // tag must not require contain characters encoded by encodeUriComponent + tag: "fresh&clean", + }); + }).toThrow(errors.InvalidTagError); + }); + it("should validate access value", () => { expect(() => { subject.normalizeOptions(manifest, { diff --git a/src/errors.ts b/src/errors.ts index dd50ff3..7a70ac1 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -62,7 +62,7 @@ export class PackageJsonParseError extends SyntaxError { export class InvalidPackageNameError extends TypeError { public constructor(value: unknown) { - super(`Package name must be a string, got "${String(value)}"`); + super(`Package name is not valid, got "${String(value)}"`); this.name = "InvalidPackageNameError"; } } diff --git a/src/normalize-options.ts b/src/normalize-options.ts index a02d9a6..473830e 100644 --- a/src/normalize-options.ts +++ b/src/normalize-options.ts @@ -97,8 +97,13 @@ const validateRegistry = (value: unknown): URL => { }; const validateTag = (value: unknown): string => { - if (typeof value === "string" && value.length > 0) { - return value; + if (typeof value === "string") { + const trimmedValue = value.trim(); + const encodedValue = encodeURIComponent(trimmedValue); + + if (trimmedValue.length > 0 && trimmedValue === encodedValue) { + return value; + } } throw new errors.InvalidTagError(value); diff --git a/src/npm/call-npm-cli.ts b/src/npm/call-npm-cli.ts index e4aa567..63f3a07 100644 --- a/src/npm/call-npm-cli.ts +++ b/src/npm/call-npm-cli.ts @@ -38,7 +38,8 @@ export const PUBLISH = "publish"; export const E404 = "E404"; export const EPUBLISHCONFLICT = "EPUBLISHCONFLICT"; -const NPM = os.platform() === "win32" ? "npm.cmd" : "npm"; +const IS_WINDOWS = os.platform() === "win32"; +const NPM = IS_WINDOWS ? "npm.cmd" : "npm"; const JSON_MATCH_RE = /(\{[\s\S]*\})/mu; const baseArguments = (options: NpmCliOptions) => @@ -106,6 +107,7 @@ async function execNpm( const npm = childProcess.spawn(NPM, commandArguments, { env: { ...process.env, ...environment }, + shell: IS_WINDOWS, }); npm.stdout.on("data", (data: string) => (stdout += data)); diff --git a/src/read-manifest.ts b/src/read-manifest.ts index 1793681..e9bf2e5 100644 --- a/src/read-manifest.ts +++ b/src/read-manifest.ts @@ -1,5 +1,6 @@ import fs from "node:fs/promises"; import path from "node:path"; +import validatePackageName from "validate-npm-package-name"; import semverValid from "semver/functions/valid.js"; import tarList from "tar/lib/list.js"; import type { ReadEntry } from "tar"; @@ -39,10 +40,14 @@ const isTarball = (file: unknown): file is string => { return typeof file === "string" && path.extname(file) === TARBALL_EXTNAME; }; -const validateVersion = (version: unknown): string | undefined => { +const normalizeVersion = (version: unknown): string | undefined => { return semverValid(version as string) ?? undefined; }; +const validateName = (name: unknown): name is string => { + return validatePackageName(name as string).validForNewPackages; +}; + const readPackageJson = async (...pathSegments: string[]): Promise => { const file = path.resolve(...pathSegments); @@ -110,13 +115,13 @@ export async function readManifest( try { manifestJson = JSON.parse(manifestContents) as Record; name = manifestJson["name"]; - version = validateVersion(manifestJson["version"]); + version = normalizeVersion(manifestJson["version"]); publishConfig = manifestJson["publishConfig"] ?? {}; } catch (error) { throw new errors.PackageJsonParseError(packageSpec, error); } - if (typeof name !== "string" || name.length === 0) { + if (!validateName(name)) { throw new errors.InvalidPackageNameError(name); }