/** * Fairly exhaustive, excruciatingly pedantic _type-level_ helpers for * representing and validating **Canonical Names** and npm package names. * * A {@link CanonicalName | Canonical Name} is a string containing one or more * npm package names (scoped or unscoped) delimited by a `>` character. * * The following rules about npm package names are enforced: * * - ✅ Length > 0 * - ✅ Can contain hyphens * - ✅ No leading `.` or `_` * - ✅ No spaces * - ✅ No `~)('!*` characters * * The following rules are not enforced: * * - ❌ All lowercase - Not feasible due to recursion limits & legacy package * names * - ❌ Not a reserved name - unmaintainable list of node builtin module names * - ❌ Length ≤ 214 - Not feasible due to recursion limits & legacy package * names * * "Legacy" package names may contain uppercase letters and be longer than 214 * characters. * * @module * @see {@link https://www.npmjs.com/package/validate-npm-package-name} */ /** * Characters that are explicitly forbidden in npm package names. These include: * ` `, `~`, `)`, `(`, `'`, `!`, `*` * * We check each one individually because TypeScript's template literal types * can detect if a string contains a specific substring. * * Returns `true` if the string contains a forbidden character, `false` * otherwise. */ type ContainsForbiddenChar = S extends | `${string} ${string}` | `${string}~${string}` | `${string})${string}` | `${string}(${string}` | `${string}'${string}` | `${string}!${string}` | `${string}*${string}` ? true : false; /** * Validates that a string doesn't start with `.` or `_`. * * Returns `true` if the string doesn't start with `.` or `_`, `false` * otherwise. */ type HasValidStart = S extends `.${string}` | `_${string}` ? false : true; /** * Validates that a string is non-empty. * * Returns `true` if the string is non-empty, `false` otherwise. */ type IsNonEmpty = S extends '' ? false : true; /** * Combines all validation checks for a package name segment. * * Returns `true` if the string passes all checks, `false` otherwise. */ type IsValidPackageNameSegment = IsNonEmpty extends false ? false : HasValidStart extends false ? false : ContainsForbiddenChar extends true ? false : true; // ============================================================================ // Scoped and Unscoped Package Names // ============================================================================ /** A scoped npm package name, like "@scope/pkg" */ export type ScopedPackageName = S extends `@${infer Scope}/${infer Name}` ? IsValidPackageNameSegment extends true ? IsValidPackageNameSegment extends true ? S : never : never : never; /** * An unscoped npm package name. * * Must pass all validation checks and must not contain a `/` (which would * indicate a scoped package or a subpath). * * Note: Package names containing uppercase letters are technically invalid per * npm rules, but they exist in the wild. TypeScript cannot reliably validate * case at the type level, so we don't enforce this. */ export type UnscopedPackageName = S extends `${string}/${string}` ? never : IsValidPackageNameSegment extends true ? S : never; /** * A scoped or unscoped npm package name. */ export type NpmPackageName = S extends `@${string}/${string}` ? ScopedPackageName : UnscopedPackageName; /** * Split a string on `>`—the canonical name delimiter—into a tuple of segments. */ export type SplitOnDelimiter = S extends `${infer Head}>${infer Tail}` ? [Head, ...SplitOnDelimiter] : [S]; /** * Validate that every element in a tuple of strings is a valid npm package * name. */ export type AllValidPackageNames = Parts extends [ infer Head extends string, ...infer Tail extends readonly string[], ] ? NpmPackageName extends never ? never : AllValidPackageNames : Parts; /** * A Canonical Name string comprised of one or more npm package names separated * by `>` (e.g., `foo`, `@scope/foo>bar`, `foo>@scope/bar>baz`). * * When given a string literal type, invalid shapes narrow to `never`. */ export type CanonicalName = AllValidPackageNames> extends never ? never : S;