Skip to content

♻ Refactor(index.js): add JSDocs, rebuild functions with test 100% passing #127

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
264 changes: 164 additions & 100 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,105 +1,169 @@
'use strict'
const { builtinModules: builtins } = require('module')

var scopedPackagePattern = new RegExp('^(?:@([^/]+?)[/])?([^/]+?)$')
var blacklist = [
'node_modules',
'favicon.ico',
]

function validate (name) {
var warnings = []
var errors = []

if (name === null) {
errors.push('name cannot be null')
return done(warnings, errors)
}

if (name === undefined) {
errors.push('name cannot be undefined')
return done(warnings, errors)
}

if (typeof name !== 'string') {
errors.push('name must be a string')
return done(warnings, errors)
}

if (!name.length) {
errors.push('name length must be greater than zero')
}

if (name.match(/^\./)) {
errors.push('name cannot start with a period')
}

if (name.match(/^_/)) {
errors.push('name cannot start with an underscore')
}

if (name.trim() !== name) {
errors.push('name cannot contain leading or trailing spaces')
}

// No funny business
blacklist.forEach(function (blacklistedName) {
if (name.toLowerCase() === blacklistedName) {
errors.push(blacklistedName + ' is a blacklisted name')
"use strict";
const { builtinModules: builtins } = require("module");

//--------------------------------
// STATIC GLOBALS
//--------------------------------

const SCOPED_PACKAGE_PATTERN = new RegExp("^(?:@([^/]+?)[/])?([^/]+?)$");
const BLACK_LIST = ["node_modules", "favicon.ico"];

//--------------------------------
// JSDOCS global types
//--------------------------------

/**
* @typedef {Object} ValidationReturn
* @property {boolean} validForNewPackages - Indicates if the package name is valid for new packages.
* @property {boolean} validForOldPackages - Indicates if the package name is valid for old packages.
* @property {string[]} [warnings] - An array of warnings related to the package name.
* @property {string[]} [errors] - An array of errors related to the package name.
*/

//--------------------------------
// Functions
//--------------------------------

/**
* Checks if the provided string is a valid npm package name.
*
* @example validate("some-package")
*
* @param {string} name - NPM package name to validate
* @returns {ValidationReturn}
* ```
* {
* validForNewPackages: boolean,
* validForOldPackages: boolean
* }
* ```
*
*/

function validate(name) {
const results = { warnings: [], errors: [] };

/**
* @typedef {Object} ValidationRule
* @property { !MyCallBack } condition - A function that returns true if the validation condition is met.
* @property { string } errors - The error message to return if the condition is met.
* @property { string } warnings - The warning message to return if the condition is met.
* @property { boolean } exit - Indicates whether to exit validation if this condition is met.
*/

/**
* Validation rules for npm package names.
* Each rule contains a condition to check, an error message if the condition fails,
* and an optional exit flag to indicate if validation should stop if the condition is met.
*
* @type {Object<string, ValidationRule>}
*/

const ValidationList = {
NULL_NAME: {
condition: () => name === null,
errors: "name cannot be null",
exit: true,
},
UNDEFINED_NAME: {
condition: () => name === undefined,
errors: "name cannot be undefined",
exit: true,
},
INVALID_TYPE: {
condition: () => typeof name !== "string",
errors: "name must be a string",
exit: true,
},
TOO_SHORT_LENGTH_NAME: {
condition: () => !name.length,
errors: "name length must be greater than zero",
},
TOO_LONG_LENGTH_NAME: {
condition: () => name.length > 214,
warnings: "name can no longer contain more than 214 characters",
},
CANNOT_START_WITH_PERIOD: {
condition: () => name.startsWith("."),
errors: "name cannot start with a period",
},
CANNOT_START_WITH_UNDERSCORE: {
condition: () => name.startsWith("_"),
errors: "name cannot start with an underscore",
},
CANNOT_HAVE_SPACES: {
condition: () => name !== name.trim(),
errors: "name cannot contain leading or trailing spaces",
},
CORE_MODULE_NAME: {
condition: () => builtins.includes(name.toLowerCase()),
warnings: `${name} is a core module name`,
},
NO_CAPITAL_LETTERS: {
condition: () => name !== name.toLowerCase(),
warnings: "name can no longer contain capital letters",
},
SPECIAL_CHARACTERS: {
condition: () => /[~'!()*]/.test(name),
warnings: 'name can no longer contain special characters ("~\'!()*")',
},
URL_FRIENDLY: {
condition: () =>
encodeURIComponent(name) !== name && !isScopedPackage(name),
errors: "name can only contain URL-friendly characters",
},
BLACK_LISTED: {
condition: () => BLACK_LIST.includes(name.toLowerCase()),
errors: `${name} is a blacklisted name`,
},
};

// Function to Validation List
for (const key in ValidationList) {
const currentValidation = ValidationList[key];
if (
Object.prototype.hasOwnProperty.call(ValidationList, key) &&
currentValidation.condition()
) {
if (currentValidation.errors)
results.errors.push(currentValidation.errors);
if (currentValidation.warnings)
results.warnings.push(currentValidation.warnings);
if (currentValidation.exit) break;
}
})

// Generate warnings for stuff that used to be allowed

// core module names like http, events, util, etc
if (builtins.includes(name.toLowerCase())) {
warnings.push(name + ' is a core module name')
}

if (name.length > 214) {
warnings.push('name can no longer contain more than 214 characters')
}

// mIxeD CaSe nAMEs
if (name.toLowerCase() !== name) {
warnings.push('name can no longer contain capital letters')
}

if (/[~'!()*]/.test(name.split('/').slice(-1)[0])) {
warnings.push('name can no longer contain special characters ("~\'!()*")')
}

if (encodeURIComponent(name) !== name) {
// Maybe it's a scoped package name, like @user/package
var nameMatch = name.match(scopedPackagePattern)
if (nameMatch) {
var user = nameMatch[1]
var pkg = nameMatch[2]
if (encodeURIComponent(user) === user && encodeURIComponent(pkg) === pkg) {
return done(warnings, errors)
}
}

errors.push('name can only contain URL-friendly characters')
}

return done(warnings, errors)
}

var done = function (warnings, errors) {
var result = {
validForNewPackages: errors.length === 0 && warnings.length === 0,
validForOldPackages: errors.length === 0,
warnings: warnings,
errors: errors,
}
if (!result.warnings.length) {
delete result.warnings
}
if (!result.errors.length) {
delete result.errors
}
return result
return done(results.warnings, results.errors);
}

module.exports = validate
/**
*
* Checks if the provided string is a scope package.
*
* @param {string} name
* @returns
*/
const isScopedPackage = (name) => {
const nameMatch = name.match(SCOPED_PACKAGE_PATTERN);
return (
nameMatch &&
encodeURIComponent(nameMatch[1]) === nameMatch[1] &&
encodeURIComponent(nameMatch[2]) === nameMatch[2]
);
};

/**
* Returns the validation result for the package name.
*
* @param {string[]} warnings - An array of warnings generated during validation.
* @param {string[]} errors - An array of errors generated during validation.
* @returns {ValidationReturn} The result of the validation.
*/
const done = (warnings, errors) => ({
validForNewPackages: !errors.length && !warnings.length,
validForOldPackages: !errors.length,
...(warnings.length && { warnings }),
...(errors.length && { errors }),
});

module.exports = validate;