Skip to content
Merged
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"ignore": [],
"ignore": ["pathnorm-test"],
"linked": [],
"updateInternalDependencies": "patch"
}
5 changes: 5 additions & 0 deletions .changeset/public-baboons-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@knide/pathnorm": major
---

First release
5 changes: 0 additions & 5 deletions .github/CODEOWNERS

This file was deleted.

5 changes: 5 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# These are supported funding model platforms

github: stacknide # https://github.com/sponsors/stacknide
# open_collective: fs-prober # https://opencollective.com/fs-prober
buy_me_a_coffee: ashuvssut # https://www.buymeacoffee.com/ashuvssut
2 changes: 1 addition & 1 deletion .github/MAINTAINERS
Original file line number Diff line number Diff line change
@@ -1 +1 @@
your-org/your-username
ashuvssut
5 changes: 4 additions & 1 deletion .github/workflows/changeset-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ jobs:
run: yarn install --frozen-lockfile

- name: Build
run: yarn workspace @knide/fs-prober build
run: yarn build

- name: Test
run: yarn test

- name: Create Release Pull Request or Publish
id: changesets
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/pull_request_code_quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ jobs:
- name: Run Package Build
working-directory: package
run: yarn build

- name: Run Workspace Tests
run: yarn test
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2024 Stacknide
Copyright (c) 2026 Stacknide

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# pathnorm
# @knide/pathnorm

Normalize and join path/URL segments. Full Win32, UNC, POSIX, and URL support.

Expand All @@ -8,6 +8,9 @@ Read the [`README.md`](./package/README.md) documentation for more details.

```sh
yarn install
yarn dev

# Build and test
yarn build
yarn test
```
Expand Down
5 changes: 5 additions & 0 deletions lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ pre-commit:
npx @biomejs/biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files} # `--no-errors-on-unmatched` silents possible errors in case no files are processed.
stage_fixed: true # stage_fixed: true adds again the staged files.

pre-push:
commands:
test:
run: yarn test

# After updating the above configuration, run `yarn lefthook install` to install the hooks.
# EXAMPLE USAGE:
#
Expand Down
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
"version": "1.0.0",
"scripts": {
"publish": "yarn changeset publish",
"version": "yarn changeset",
"dev": "yarn workspace pathnorm dev",
"build": "yarn workspace pathnorm build",
"bump": "yarn changeset",
"dev": "yarn workspace @knide/pathnorm dev",
"build": "yarn workspace @knide/pathnorm build",
"test": "yarn workspace pathnorm-test test"
},
"devDependencies": {
Expand All @@ -20,7 +20,8 @@
"author": "Ashutosh Khanduala <https://github.com/ashuvssut>",
"license": "MIT",
"workspaces": [
"package"
"package",
"test"
],
"packageManager": "yarn@4.12.0"
}
32 changes: 16 additions & 16 deletions package/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# pathnorm
# @knide/pathnorm

Normalize and join path or URL segments into a single clean string.

Expand All @@ -10,38 +10,38 @@ Normalize and join path or URL segments into a single clean string.
## Installation

```sh
npm install pathnorm
npm install @knide/pathnorm
# or
yarn add pathnorm
yarn add @knide/pathnorm
# or
pnpm add pathnorm
pnpm add @knide/pathnorm
```

## Two Entry Points

| Import | Win32 | UNC | Namespace | URLs | POSIX |
|---|---|---|---|---|---|
| `pathnorm` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `pathnorm/posix` | | ❌ | ❌ | ✅ | ✅ |
| Import | Win32 | UNC | Namespace | URLs | POSIX |
|-------------------------|-------|-----|-----------|------|-------|
| `@knide/pathnorm` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `@knide/pathnorm/posix` | | ❌ | ❌ | ✅ | ✅ |

Use `pathnorm/posix` in browser or web server contexts where Win32 paths will never appear — it's lighter and purpose-built for URLs and POSIX paths.
Use `@knide/pathnorm/posix` in browser or web server contexts where Win32 paths will never appear — it's lighter and purpose-built for URLs and POSIX paths.

---

## `pathnorm`
## `@knide/pathnorm`

Exports `np` and `unixNp`.

```ts
import { np, unixNp } from 'pathnorm'
import { np, unixNp } from '@knide/pathnorm'
```

### `np(...parts)`

Joins and normalizes path or URL segments. Detects and handles URLs, POSIX paths, Win32 drive letters, UNC paths, and Win32 namespace paths automatically.

```ts
import { np } from 'pathnorm'
import { np } from '@knide/pathnorm'
// URLs
np("https://abc.def//212/", "dw//we", "23123")
// → "https://abc.def/212/dw/we/23123"
Expand Down Expand Up @@ -81,7 +81,7 @@ np("C:\\foo//bar\\\\baz")
Like `np`, but always returns a Unix-style path. Useful when working with Win32 paths but the consumer expects forward slashes.

```ts
import { unixNp } from 'pathnorm'
import { unixNp } from '@knide/pathnorm'
unixNp("C:\\foo\\\\bar", "baz")
// → "C:/foo/bar/baz"

Expand All @@ -98,20 +98,20 @@ unixNp("/foo//bar", "baz")

---

## `pathnorm/posix`
## `@knide/pathnorm/posix`

Exports `np`. No Win32 support — ideal for browser and web server environments.

```ts
import { np } from 'pathnorm/posix'
import { np } from '@knide/pathnorm/posix'
```

### `np(...parts)` (POSIX + URLs)

Joins and normalizes URL or POSIX path segments.

```ts
import { np } from 'pathnorm/posix'
import { np } from '@knide/pathnorm/posix'
// URLs
np("https://abc.def//212/", "dw//we", "23123")
// → "https://abc.def/212/dw/we/23123"
Expand Down
2 changes: 1 addition & 1 deletion package/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "pathnorm",
"name": "@knide/pathnorm",
"version": "0.0.0",
"description": "Normalize and join path and URL segments. Supports URLs, POSIX paths, Win32 drive letters, UNC, and namespace paths.",
"source": "src/index.ts",
Expand Down
2 changes: 1 addition & 1 deletion package/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* - Win32 namespace paths (`\\?\C:\foo` or `\\.\C:\foo`)
*
* For browser or web-only contexts where Win32 paths are never needed,
* prefer `pathnorm/posix` instead — it's lighter and purpose-built for
* prefer `@knide/pathnorm/posix` instead — it's lighter and purpose-built for
* URLs and POSIX paths.
*
* @param parts - One or more path/URL segments to join and normalize
Expand Down
123 changes: 123 additions & 0 deletions test/np.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { np, unixNp } from '@knide/pathnorm'

const cases = [
{
args: ['https://a', 'bc.def//212/', 'dw//we', '23123', 'https://a'],
expected: 'https://a/bc.def/212/dw/we/23123/https:/a',
name: 'URL with scheme + trailing URL-like segment',
},
{
args: ['https://abc.def//212/', 'dw//we', '23123'],
expected: 'https://abc.def/212/dw/we/23123',
name: 'URL with scheme',
},
{
args: ['abc.def//212/', 'dwwe', '23123'],
expected: 'abc.def/212/dwwe/23123',
name: 'POSIX path (no scheme, no leading slash)',
},
{
args: ['foo//bar', '/baz'],
expected: '/foo/bar/baz',
name: 'POSIX path',
},
{
args: ['/foo//bar', 'baz'],
expected: '/foo/bar/baz',
name: 'POSIX path with leading slash',
},
{
args: ['exp://abc.def//212/', 'dw/we'],
expected: 'exp://abc.def/212/dw/we',
name: 'Custom scheme',
},
{
args: [String.raw`C:\foo\\bar`, 'baz'],
expected: String.raw`C:\foo\bar\baz`,
name: 'Win32 drive letter',
},
{
args: [String.raw`C:\foo//bar\\baz`],
expected: String.raw`C:\foo\bar\baz`,
name: 'Win32 mixed slashes',
},
{
args: [String.raw`\\server\share\\folder`, 'file.txt'],
expected: '//server/share/folder/file.txt',
name: 'UNC path',
},
{
args: [String.raw`\\?\C:\foo\\bar`, 'baz'],
expected: '//?/C:/foo/bar/baz',
name: 'Win32 namespace path',
},
{
args: ['exp://', 'sdfasdf', 'abc.def//212/', 'dw//we', '23123', 'https://'],
expected: 'exp://sdfasdf/abc.def/212/dw/we/23123/https:/',
name: 'Multiple URL-like segments (only first scheme treated as prefix)',
},
{ args: [], expected: '', name: 'Empty input list' },
{ args: [''], expected: '', name: 'Single empty segment' },
{ args: ['', 'foo'], expected: '/foo', name: 'Empty then segment' },
{ args: ['foo', ''], expected: 'foo/', name: 'Segment then empty' },
{ args: ['', '', ''], expected: '/', name: 'All empty segments' },
{ args: ['foo/bar/'], expected: 'foo/bar/', name: 'Trailing slash preserved in path' },
{ args: ['https://a.com/'], expected: 'https://a.com/', name: 'Trailing slash preserved in URL' },
{ args: ['foo'], expected: 'foo', name: 'Single plain segment' },
{ args: ['/foo'], expected: '/foo', name: 'Single leading-slash segment' },
{ args: ['https://foo.com'], expected: 'https://foo.com', name: 'Single URL segment' },
{
args: ['foo', 'bar', '/baz'],
expected: '/foo/bar/baz',
name: 'Leading slash introduced by third segment',
},
{ args: ['https://', 'foo', 'bar'], expected: 'https://foo/bar', name: 'URL with empty host' },
{
args: ['foo', '///', 'bar'],
expected: '/foo/bar',
name: 'Middle segment contains only slashes',
},
{ args: ['foo', '//'], expected: '/foo/', name: 'Trailing segment contains only slashes' },
]

for (const { name, args, expected } of cases) {
test(`np: ${name}`, () => {
const output = np(...args)
assert.equal(
output,
expected,
`args=${JSON.stringify(args)} expected=${expected} got=${output}`
)
})
}

const unixNpCases = [
{
args: [String.raw`C:\foo\\bar`, 'baz'],
expected: 'C:/foo/bar/baz',
name: 'Converts Win32 drive path to unix separators',
},
{
args: [String.raw`\\server\share`, 'file.txt'],
expected: '//server/share/file.txt',
name: 'Converts UNC path to unix separators',
},
{
args: ['/already/unix//path'],
expected: '/already/unix/path',
name: 'Normalizes existing unix path',
},
]

for (const { name, args, expected } of unixNpCases) {
test(`unixNp: ${name}`, () => {
const output = unixNp(...args)
assert.equal(
output,
expected,
`args=${JSON.stringify(args)} expected=${expected} got=${output}`
)
})
}
12 changes: 12 additions & 0 deletions test/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "pathnorm-test",
"private": true,
"type": "module",
"version": "0.0.0",
"scripts": {
"test": "node --test ./*.test.mjs"
},
"dependencies": {
"@knide/pathnorm": "workspace:*"
}
}
Loading
Loading