utils/node_modules/@rushstack/node-core-library/lib/PackageName.js
2024-02-07 01:33:07 -05:00

213 lines
8.2 KiB
JavaScript

"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
Object.defineProperty(exports, "__esModule", { value: true });
exports.PackageName = exports.PackageNameParser = void 0;
/**
* A configurable parser for validating and manipulating NPM package names such as `my-package` or `@scope/my-package`.
*
* @remarks
* If you do not need to customize the parser configuration, it is recommended to use {@link PackageName}
* which exposes these operations as a simple static class.
*
* @public
*/
class PackageNameParser {
constructor(options = {}) {
this._options = Object.assign({}, options);
}
/**
* This attempts to parse a package name that may include a scope component.
* The packageName must not be an empty string.
* @remarks
* This function will not throw an exception.
*
* @returns an {@link IParsedPackageNameOrError} structure whose `error` property will be
* nonempty if the string could not be parsed.
*/
tryParse(packageName) {
const result = {
scope: '',
unscopedName: '',
error: ''
};
let input = packageName;
if (input === null || input === undefined) {
result.error = 'The package name must not be null or undefined';
return result;
}
// Rule from npmjs.com:
// "The name must be less than or equal to 214 characters. This includes the scope for scoped packages."
if (packageName.length > 214) {
// Don't attempt to parse a ridiculously long input
result.error = 'The package name cannot be longer than 214 characters';
return result;
}
if (input[0] === '@') {
const indexOfScopeSlash = input.indexOf('/');
if (indexOfScopeSlash <= 0) {
result.scope = input;
result.error = `Error parsing "${packageName}": The scope must be followed by a slash`;
return result;
}
// Extract the scope substring
result.scope = input.substr(0, indexOfScopeSlash);
input = input.substr(indexOfScopeSlash + 1);
}
result.unscopedName = input;
if (result.scope === '@') {
result.error = `Error parsing "${packageName}": The scope name cannot be empty`;
return result;
}
if (result.unscopedName === '') {
result.error = 'The package name must not be empty';
return result;
}
// Rule from npmjs.com:
// "The name can't start with a dot or an underscore."
if (result.unscopedName[0] === '.' || result.unscopedName[0] === '_') {
result.error = `The package name "${packageName}" starts with an invalid character`;
return result;
}
// Convert "@scope/unscoped-name" --> "scopeunscoped-name"
const nameWithoutScopeSymbols = (result.scope ? result.scope.slice(1, -1) : '') + result.unscopedName;
if (!this._options.allowUpperCase) {
// "New packages must not have uppercase letters in the name."
// This can't be enforced because "old" packages are still actively maintained.
// Example: https://www.npmjs.com/package/Base64
// However it's pretty reasonable to require the scope to be lower case
if (result.scope !== result.scope.toLowerCase()) {
result.error = `The package scope "${result.scope}" must not contain upper case characters`;
return result;
}
}
// "The name ends up being part of a URL, an argument on the command line, and a folder name.
// Therefore, the name can't contain any non-URL-safe characters"
const match = nameWithoutScopeSymbols.match(PackageNameParser._invalidNameCharactersRegExp);
if (match) {
result.error = `The package name "${packageName}" contains an invalid character: "${match[0]}"`;
return result;
}
return result;
}
/**
* Same as {@link PackageName.tryParse}, except this throws an exception if the input
* cannot be parsed.
* @remarks
* The packageName must not be an empty string.
*/
parse(packageName) {
const result = this.tryParse(packageName);
if (result.error) {
throw new Error(result.error);
}
return result;
}
/**
* {@inheritDoc IParsedPackageName.scope}
*/
getScope(packageName) {
return this.parse(packageName).scope;
}
/**
* {@inheritDoc IParsedPackageName.unscopedName}
*/
getUnscopedName(packageName) {
return this.parse(packageName).unscopedName;
}
/**
* Returns true if the specified package name is valid, or false otherwise.
* @remarks
* This function will not throw an exception.
*/
isValidName(packageName) {
const result = this.tryParse(packageName);
return !result.error;
}
/**
* Throws an exception if the specified name is not a valid package name.
* The packageName must not be an empty string.
*/
validate(packageName) {
this.parse(packageName);
}
/**
* Combines an optional package scope with an unscoped root name.
* @param scope - Must be either an empty string, or a scope name such as "\@example"
* @param unscopedName - Must be a nonempty package name that does not contain a scope
* @returns A full package name such as "\@example/some-library".
*/
combineParts(scope, unscopedName) {
if (scope !== '') {
if (scope[0] !== '@') {
throw new Error('The scope must start with an "@" character');
}
}
if (scope.indexOf('/') >= 0) {
throw new Error('The scope must not contain a "/" character');
}
if (unscopedName[0] === '@') {
throw new Error('The unscopedName cannot start with an "@" character');
}
if (unscopedName.indexOf('/') >= 0) {
throw new Error('The unscopedName must not contain a "/" character');
}
let result;
if (scope === '') {
result = unscopedName;
}
else {
result = scope + '/' + unscopedName;
}
// Make sure the result is a valid package name
this.validate(result);
return result;
}
}
// encodeURIComponent() escapes all characters except: A-Z a-z 0-9 - _ . ! ~ * ' ( )
// However, these are disallowed because they are shell characters: ! ~ * ' ( )
PackageNameParser._invalidNameCharactersRegExp = /[^A-Za-z0-9\-_\.]/;
exports.PackageNameParser = PackageNameParser;
/**
* Provides basic operations for validating and manipulating NPM package names such as `my-package`
* or `@scope/my-package`.
*
* @remarks
* This is the default implementation of {@link PackageNameParser}, exposed as a convenient static class.
* If you need to configure the parsing rules, use `PackageNameParser` instead.
*
* @public
*/
class PackageName {
/** {@inheritDoc PackageNameParser.tryParse} */
static tryParse(packageName) {
return PackageName._parser.tryParse(packageName);
}
/** {@inheritDoc PackageNameParser.parse} */
static parse(packageName) {
return this._parser.parse(packageName);
}
/** {@inheritDoc PackageNameParser.getScope} */
static getScope(packageName) {
return this._parser.getScope(packageName);
}
/** {@inheritDoc PackageNameParser.getUnscopedName} */
static getUnscopedName(packageName) {
return this._parser.getUnscopedName(packageName);
}
/** {@inheritDoc PackageNameParser.isValidName} */
static isValidName(packageName) {
return this._parser.isValidName(packageName);
}
/** {@inheritDoc PackageNameParser.validate} */
static validate(packageName) {
return this._parser.validate(packageName);
}
/** {@inheritDoc PackageNameParser.combineParts} */
static combineParts(scope, unscopedName) {
return this._parser.combineParts(scope, unscopedName);
}
}
PackageName._parser = new PackageNameParser();
exports.PackageName = PackageName;
//# sourceMappingURL=PackageName.js.map