npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

urlify-utils

v1.0.1

Published

A comprehensive, native-like, zero-learning-cost URL toolkit for frontend development

Readme

URLify 设计文档

背景与目标

前端开发中,URL 操作是一个高频且繁琐的场景。原生 URL API 虽然功能完备,但在实际使用中存在以下痛点:

  • 提取困难:获取 query 的某个字段需要写 url.searchParams.get('key'),无法直接拿到对象形式。
  • 拼接繁琐:构建一个带参数的 URL 需要手动拼接字符串,容易出错且代码冗长。
  • 缺乏工具方法:如获取相对路径、路径分段、文件扩展名等常用功能需自行实现。
  • 类型安全:原生 API 缺乏 TypeScript 类型支持,开发体验不佳。

目标:打造一个大而全、类原生、零学习成本的 URL 瑞士军刀工具库(Browser + Node.js)。


架构设计

核心思路

  • 核心类 URLify:继承并增强原生 URL,100% 兼容原生属性与行为。
  • 不可变更新:所有修改操作(如 withQuery, withPath)均返回新实例,避免副作用。
  • 工具函数剥离:静态方法提供纯函数式工具(如 URLify.join, URLify.isValid),便于 tree-shaking。

核心 API 设计

1. 实例属性(兼容原生 + 增强)

原生属性(继承自 URL)

⚠️ 注意:为了支持不可变更新URLify 的所有属性均为 readonly。如果使用者需要修改 URL,请使用 with... 方法返回新实例。这是唯一与原生 URL 不兼容的地方。

| 属性名 | 类型 | 示例 | |--------|------|------| | href | readonly string | 'https://user:[email protected]:8080/path?query=string#hash' | | protocol | string | 'https:' | | host | string | 'example.com:8080' | | hostname | string | 'example.com' | | port | string | '8080' | | pathname | string | '/path' | | search | string | '?query=string' | | hash | string | '#hash' | | username | string | 'user' | | password | string | 'pass' | | origin | string | 'https://example.com:8080' |

增强属性

| 属性名 | 类型 | 说明 | 示例 | |--------|------|------|------| | query | Record<string, string \| string[]> | 解析后的 query 对象 | { query: 'string' } | | queryKeys | string[] | query 中的所有键名 | ['query'] | | pathSegments | string[] | 路径分段数组 | ['path'] | | isAbsolute | boolean | 是否为绝对 URL | true | | hasTrailingSlash | boolean | 路径是否以 / 结尾 | false | | fileExtension | string \| undefined | 文件扩展名 | undefined |

2. 实例方法

| 方法名 | 参数 | 返回 | 说明 | |--------|------|------|------| | toString() | - | string | 返回完整 URL 字符串 | | toJSON() | - | object | 返回包含所有属性的纯对象 | | toRelative() | - | string | 返回相对路径 '/path?query=string#hash' | | clone() | - | URLify | 深拷贝实例 | | getQuery(key) | string | string \| string[] \| undefined | 获取指定 query 参数 | | withQuery(query) | object | URLify | 不可变 合并/覆盖 query,返回新实例 | | withoutQuery(key) | string \| string[] | URLify | 不可变 删除指定 query(支持批量删除),返回新实例 | | setQuery(query) | object | URLify | 不可变 完全替换所有 query 参数,返回新实例 | | clearQuery() | - | URLify | 不可变 清空所有 query 参数,返回新实例 | | withPath(path) | string | URLify | 不可变 替换路径,返回新实例 | | withHash(hash) | string | URLify | 不可变 替换 hash,返回新实例 |

3. 静态工具方法

| 方法名 | 参数 | 返回 | 说明 | |--------|------|------|------| | URLify.parse(url) | string | URLify | 等同 new URLify(url) | | URLify.isValid(url) | string | boolean | 校验 URL 格式是否合法 | | URLify.join(...parts) | ...string[] | string | 安全拼接 URL 路径(自动去重斜杠) | | URLify.fromObject(obj) | object | URLify | 从对象 { protocol, host, pathname, ... } 构建 URL |


TypeScript 类型定义

declare class URLify {
  // 原生属性
  readonly href: string;
  readonly protocol: string;
  readonly host: string;
  readonly hostname: string;
  readonly port: string;
  readonly pathname: string;
  readonly search: string;
  readonly hash: string;
  readonly username: string;
  readonly password: string;
  readonly origin: string;

  // 增强属性
  readonly query: Record<string, string | string[]>;
  readonly queryKeys: string[];
  readonly pathSegments: string[];
  readonly isAbsolute: boolean;
  readonly hasTrailingSlash: boolean;
  readonly fileExtension?: string;

  constructor(url: string | URL, base?: string | URL);

  // 实例方法
  toString(): string;
  toJSON(): URLifyJSON;
  toRelative(): string;
  clone(): URLify;
  getQuery(key: string): string | string[] | undefined;
  withQuery(query: Record<string, string | number | boolean | undefined>): URLify;
  withoutQuery(key: string | string[]): URLify;
  withPath(path: string): URLify;
  withHash(hash: string): URLify;

  // 静态方法
  static parse(url: string): URLify;
  static isValid(url: string): boolean;
  static join(...parts: string[]): string;
  static fromObject(obj: {
    protocol?: string;
    username?: string;
    password?: string;
    hostname?: string;
    port?: string | number;
    pathname?: string;
    search?: string;
    hash?: string;
    query?: Record<string, string | string[]>;
  }): URLify;
}

// toJSON 返回的完整结构
interface URLifyJSON {
  href: string;
  protocol: string;
  host: string;
  hostname: string;
  port: string;
  pathname: string;
  search: string;
  hash: string;
  username: string;
  password: string;
  origin: string;
  query: Record<string, string | string[]>;
  queryKeys: string[];
  pathSegments: string[];
  isAbsolute: boolean;
  hasTrailingSlash: boolean;
  fileExtension?: string;
}

使用示例

import { URLify } from 'urlify';

// 基础解析
const url = new URLify('https://example.com/api/v1/users?page=1&sort=desc&sort=asc');

url.query;           // { page: '1', sort: ['desc', 'asc'] }
url.pathSegments;    // ['api', 'v1', 'users']
url.getQuery('sort'); // ['desc', 'asc']

// 不可变更新
const newUrl = url.withQuery({ page: 2 }).withoutQuery('sort');
newUrl.toString();   // 'https://example.com/api/v1/users?page=2'

// 静态工具
URLify.isValid('not-a-url'); // false
URLify.join('https://example.com/', '/api/', '/users'); // 'https://example.com/api/users'

边界情况与错误处理

  • 非法 URL 输入const url = new URLify('bad-url') → 抛出 TypeError: Invalid URL(与原生 URL 保持一致)。
  • 无协议 URL:需要传入 base 参数,如 new URLify('/path', 'https://example.com')。不支持无 base 的相对 URL,会抛出 TypeError(与原生 URL 保持一致)。
  • isAbsolute 判断:基于传入的 url 字符串是否以协议开头(如 https://)来判断。如果是通过 base 参数构建的 URL,isAbsolutetrue
  • 特殊字符编码:内部使用原生 URLSearchParams 处理,确保与浏览器行为一致。
  • 数组参数query 属性中,同名参数自动聚合为数组。
    • 单个参数:?foo=1{ foo: '1' }
    • 多个同名参数:?foo=1&foo=2{ foo: ['1', '2'] }

构建配置

  • 打包工具:Rollup(输出 ESM + CJS + UMD + .d.ts)
  • 语言:TypeScript
  • 测试:Vitest(覆盖率 > 90%)
  • 代码规范:ESLint + Prettier
  • 包大小:目标 < 5KB (gzip)
  • 导出策略:同时提供 named exports(如 import { join, isValid } from 'urlify')和 class 的 static 方法,确保真正的 Tree-shaking 能力。

依赖

  • 零运行时依赖
  • 开发依赖:TypeScript、Rollup、Vitest、ESLint、Prettier