@cdxgen/cdx-purl

中文文档
0.0.6发布于 2026/09/30MITES Module零依赖构建来源已验证近 30 天下载 5万

Pure JavaScript package-url parser and builder

原始使用文档
文档目录18 节
  1. Install
  2. Quick start
  3. API
  4. Validation behavior (strict by design)
  5. Reserved characters must be percent-encoded
  6. Unknown qualifier keys are rejected
  7. Only explicitly allowed multivalue qualifiers are accepted
  8. Checksum values are validated by algorithm
  9. Type-specific required and prohibited fields are enforced
  10. Go vanity module paths are accepted without a namespace
  11. Subpath validation in builder and object flows
  12. Supported purl types
  13. Normalization opcode model
  14. Known normalization limitation
  15. Test commands
  16. Deterministic fuzz controls
  17. Support policy
  18. License

cdx-purl

Strict, definition-driven package-url parser/builder for Node.js.

  • Pure JavaScript
  • No runtime dependencies
  • ABNF-first strict parsing
  • Type rules are generated from specification/types/*-definition.json into generated/type-rules.js
  • Deterministic canonicalization and roundtrip behavior

AI-DECLARATION: auto

Install

pnpm add @cdxgen/cdx-purl
npm install @cdxgen/cdx-purl
bun add @cdxgen/cdx-purl
deno add npm:@cdxgen/cdx-purl

Quick start

import {
  parse,
  build,
  roundTrip,
  Purl,
  NpmPurl,
  MavenPurl,
  TypedPurlBuilders,
  TypedPurls,
} from "@cdxgen/cdx-purl";

const parsed = parse("pkg:npm/%40angular/[email protected]");
const built = build(parsed);
const canonical = roundTrip(built);

const npmPurl = NpmPurl.builder()
  .setScope("@angular")
  .setName("animation")
  .setVersion("12.3.1")
  .build()
  .toString();

const MavenBuilder = TypedPurlBuilders.maven;
const mvn = new MavenBuilder()
  .setNamespace("org.apache.commons")
  .setName("io")
  .buildString();

console.log(parsed, canonical, npmPurl, mvn, !!TypedPurls.swift);

API

  • parse(input) parse and validate strict ABNF + type rules
  • build(parts) canonical build with strict validation
  • roundTrip(input) parse then build canonical output
  • Purl immutable strict value object
  • PurlBuilder fluent generic builder
  • TypedPurlBuilders generated builder classes keyed by type
  • TypedPurls generated typed classes keyed by type
  • getTypedPurlBuilder(type) and getTypedPurlClass(type) lookup helpers

Runtime note: index.js imports generated TYPE_RULES_SOURCE from generated/type-rules.js; it does not read specification/types JSON files at execution time.

All named typed exports (for example NpmPurlBuilder, RpmPurl, VscodeExtensionPurlBuilder) are concrete generated classes, not placeholders.

Validation behavior (strict by design)

Reserved characters must be percent-encoded

Outside the separators, only letters, digits, ., -, _, ~ and : may appear literally in a purl component (specification/purl-proposed-grammar.abnf). parse rejects anything else instead of guessing what was meant, including the + of semver build metadata and of Debian or Go versions. Pass raw values to build (or a builder) and it escapes them; parse decodes them again.

import { build, parse } from "@cdxgen/cdx-purl";

build({ type: "npm", name: "app", version: "1.0.0+build.5" });
// "pkg:npm/[email protected]%2Bbuild.5"
parse("pkg:npm/[email protected]%2Bbuild.5").version; // "1.0.0+build.5"

parse("pkg:npm/[email protected]+build.5");
// throws E_INVALID_CHARACTER:
// Invalid character "+" in version at index 5; percent-encode it as %2B

E_INVALID_CHARACTER and E_DISALLOWED_PERCENT_ENCODING errors carry the offending component (name, version, namespace, subpath or qualifier <key>), the index within that component's raw text, the character (or escape) found there, and for E_INVALID_CHARACTER the encoded form to write instead. An escape of a character that must be written literally (%41 for A) and an escaped separator (%2F in a namespace or subpath) are rejected with E_DISALLOWED_PERCENT_ENCODING.

The upstream test suite has a few parse and roundtrip cases whose input leaves a + unescaped (NSData+zlib, Enterprise+Server). cdx-purl rejects those inputs; their escaped forms produce exactly the upstream expectations (test/reserved-character-errors.test.js).

Unknown qualifier keys are rejected

import { build } from "@cdxgen/cdx-purl";

build({
  type: "maven",
  namespace: "org.apache.commons",
  name: "io",
  version: "1.3.4",
  qualifiers: { mykey: "value" }, // throws E_UNKNOWN_QUALIFIER
  subpath: null,
});

Only explicitly allowed multivalue qualifiers are accepted

import { build } from "@cdxgen/cdx-purl";

// throws E_MULTIVALUE_QUALIFIER
build({
  type: "maven",
  namespace: "org.apache.commons",
  name: "io",
  version: "1.3.4",
  qualifiers: { classifier: "sources,docs" },
  subpath: null,
});

// allowed: checksum supports comma-delimited values
build({
  type: "generic",
  namespace: null,
  name: "openssl",
  version: "1.1.1w",
  qualifiers: {
    checksum:
      "sha1:da39a3ee5e6b4b0d3255bfef95601890afd80709,sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  },
  subpath: null,
});

Checksum values are validated by algorithm

Checksum entries must use algorithm:digest form. The library validates each entry against an internal algorithm map and rejects digests that are missing an algorithm, non-hex, or wrong length for the selected algorithm.

import { parse } from "@cdxgen/cdx-purl";

// success: valid lengths for each algorithm
parse(
  "pkg:generic/[email protected]?checksum=" +
    "sha1:da39a3ee5e6b4b0d3255bfef95601890afd80709," +
    "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
);

// throws E_CHECKSUM_MISSING_ALGORITHM
parse(
  "pkg:generic/[email protected]?checksum=da39a3ee5e6b4b0d3255bfef95601890afd80709",
);

// throws E_CHECKSUM_INVALID_DIGEST_FOR_ALGORITHM (wrong length)
parse("pkg:generic/[email protected]?checksum=sha256:abc123");

// throws E_CHECKSUM_INVALID_DIGEST_FOR_ALGORITHM (non-hex)
parse(
  "pkg:generic/[email protected]?checksum=" +
    "sha1:zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
);

Type-specific required and prohibited fields are enforced

import { build } from "@cdxgen/cdx-purl";

// swift namespace is required and must contain host/owner
build({
  type: "swift",
  namespace: null, // throws E_REQUIRED_COMPONENT / E_SWIFT_NAMESPACE
  name: "alamofire",
  version: null,
  qualifiers: null,
  subpath: null,
});

Go vanity module paths are accepted without a namespace

The purl golang definition predates Go modules and marks the namespace as required. Single-segment vanity module paths such as go.opencensus.io are valid Go modules with no namespace, so they are accepted when the name is host-like (dot-separated labels). Bare names that are not host-like still require a namespace.

import { parse } from "@cdxgen/cdx-purl";

parse("pkg:golang/[email protected]"); // ok, namespace is null
parse("pkg:golang/k8s.io"); // ok
parse("pkg:golang/context"); // throws E_REQUIRED_COMPONENT

Subpath validation in builder and object flows

When you provide purl parts directly (build, Purl.from, typed builders), subpath must be relative. Absolute-style values are rejected with E_INVALID_SUBPATH and an actionable message. This includes leading / and Windows absolute forms like C:\\docs\\api or \\\\server\\share\\docs.

import { build, parse } from "@cdxgen/cdx-purl";

// throws E_INVALID_SUBPATH
build({
  type: "generic",
  namespace: null,
  name: "openssl",
  version: "1.1.1w",
  qualifiers: null,
  subpath: "/docs/api",
});

// accepted: relative subpath
build({
  type: "generic",
  namespace: null,
  name: "openssl",
  version: "1.1.1w",
  qualifiers: null,
  subpath: "docs/api",
});

// parser compatibility: raw purl subpaths are canonicalized
// `//docs///api/` -> `docs/api`
parse("pkg:generic/[email protected]#//docs///api/"); // => { type: "generic", namespace: null, name: "openssl", version: "1.1.1w", qualifiers: null, subpath: "docs/api" }

Supported purl types

The library generates typed classes/builders for every type definition at generation time from specification/types/*-definition.json:

alpm, apk, bazel, bitbucket, bitnami, cargo, chrome-extension, cocoapods, composer, conan, conda, cpan, cran, deb, docker, gem, generic, github, golang, hackage, hex, huggingface, julia, luarocks, maven, mlflow, npm, nuget, oci, opam, otp, pub, pypi, qpkg, rpm, swid, swift, vscode-extension, yocto.

Requirement areaOfficial spec sourceWhat this library enforcesPrimary error codes
Canonical type and qualifier key casingspecification/purl-proposed-grammar.abnf + type definitionsType and qualifier keys canonicalized to lowercase; deterministic qualifier orderingE_INVALID_TYPE, E_INVALID_QUALIFIER_KEY, E_DUPLICATE_QUALIFIER
ABNF strict parse rulesspecification/purl-proposed-grammar.abnfScheme must be pkg; strict separator usage; strict percent-encoding acceptance and decodingE_INVALID_SCHEME, E_INVALID_QUALIFIERS, E_BAD_PERCENT_ENCODING, E_DISALLOWED_PERCENT_ENCODING, E_INVALID_SUBPATH
Component requirements (namespace, name, version, subpath)specification/types/*-definition.json (*_definition.requirement)Enforces required/prohibited/optional semantics per typeE_REQUIRED_COMPONENT, E_PROHIBITED_COMPONENT, E_MISSING_NAME, E_MISSING_VERSION
Character constraints and normalizationspecification/types/*-definition.json (permitted_characters, normalization_rules, case_sensitive)Applies case and normalization rules before validation and canonical buildE_PERMITTED_CHARACTERS, E_INVALID_CHARACTER
Qualifier allow-list per typespecification/types/*-definition.json (qualifiers_definition)Rejects unknown qualifier keys except global/spec-compat policy keysE_UNKNOWN_QUALIFIER
Qualifier value cardinalitystrict policyMulti-value qualifiers rejected unless explicitly allowed (checksum)E_MULTIVALUE_QUALIFIER, E_INVALID_QUALIFIER_VALUE, E_CHECKSUM_MISSING_ALGORITHM, E_CHECKSUM_INVALID_DIGEST_FOR_ALGORITHM
Required qualifiersspecification/types/*-definition.json (qualifiers_definition.requirement)Missing required qualifier is rejected in parse and build flowsE_REQUIRED_QUALIFIER
Type-specific semantic rulestype specs and implementation policyCPAN uppercase author namespace and no :: in distribution name; Swift host/owner namespace shapeE_CPAN_NAMESPACE, E_CPAN_NAME, E_SWIFT_NAMESPACE
Typed class safetygenerated type registry from definitionsType-locked parse for each typed class with mismatch detectionE_TYPE_MISMATCH

Normalization opcode model

Normalization rule free text from specification/types/*-definition.json is compiled into opcode arrays in generated/type-rules.js. index.js applies these opcodes in order via applyNormalizationRules(...) and rejects unknown opcodes with E_UNKNOWN_NORMALIZATION_OP.

Current opcode semantics:

  • to_lowercase: lowercase the full value.
  • apply_kebab_case: collapse non-alphanumeric runs to - and trim outer dashes (preserves letter case).
  • replace_underscore_with_dash: replace all _ with -.
  • replace_dot_with_underscore: replace all . with _.
  • replace_non_alnum_with_underscore: lowercase then replace each non [a-z0-9] character with _.

Opcode precedence is resolved in the generator (scripts/generate-type-rules.mjs) so emitted arrays are deterministic across regeneration. For conflicting rules, precedence is applied before runtime. Example for PyPI names:

  1. replace_dot_with_underscore
  2. replace_underscore_with_dash

This guarantees canonical idempotence for mixed forms like A_B-C.D~x -> a-b-c-d~x.

Known normalization limitation

  • alpm.version includes a normalization rule that references vercmp(8) semantics.
  • This library currently keeps alpm support without applying that version normalization logic.
  • Parsing and validation still run for alpm, but no additional vercmp-based rewrite is applied to version values.

Test commands

pnpm test
pnpm test:base
pnpm test:typed
pnpm test:typed:all
pnpm test:mutation
pnpm test:fuzz

Deterministic fuzz controls

PURL_FUZZ_SEED=1337
PURL_FUZZ_CASES=300
PURL_FUZZ_HOPS=10
PURL_FUZZ_MUTATION_CASES=200
PURL_TYPED_FUZZ_SEED=4242
PURL_TYPED_FUZZ_CASES=240
PURL_TYPED_FUZZ_HOPS=8
pnpm test:fuzz

Coverage includes:

  • base fixture compatibility tests
  • typed class and builder coverage across all registered types
  • adversarial strict-policy tests
  • mutation-driven negative tests generated from type definitions
  • deterministic fuzz and mutation fuzz with reproducible seeds

Support policy

  • Runtime support: Node >=20, Bun >=1.0.0, Deno >=1.40.0
  • CI test matrix (Node): 20.x, 22.x, 24.x, 25.x
  • CI runtime smoke coverage: Bun and Deno
  • Build and publish path: Node 24.x with npm trusted publishing + provenance

License

MIT License. See LICENSE for details.

版本与发布记录

@cdxgen/cdx-purl 共发布 5 个版本,首次发布于 2026/04/26;过去 12 个月发布了 5 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.0.6latest2026/09/301 个月前
0.0.52026/08/053 个月前
0.0.42026/07/313 个月前
0.0.32026/04/266 个月前
0.0.12026/04/266 个月前

关于 @cdxgen/cdx-purl 的常见问题

@cdxgen/cdx-purl 是什么?

Pure JavaScript package-url parser and builder

如何安装 @cdxgen/cdx-purl?

在项目目录运行 npm install @cdxgen/cdx-purl,或使用 pnpm add / yarn add / bun add。

@cdxgen/cdx-purl 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/cdxgen__cdx-purl 是否存在。

@cdxgen/cdx-purl 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@cdxgen/cdx-purl 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@cdxgen/cdx-purl 可以商用吗?

@cdxgen/cdx-purl 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@cdxgen/cdx-purl 的下载量有多少?

根据 npm 官方下载统计,@cdxgen/cdx-purl 在 2026/10/10 – 2026/11/08 这 30 天内被下载 50,258 次,日均约 1,675 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@cdxgen/cdx-purl 还在维护吗?

最新版本 0.0.6 发布于 2026/09/30(1 个月前);至今共发布 5 个版本,其中过去 12 个月发布了 5 个;首次发布于 2026/04/26。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。