diff --git a/.changeset/config.json b/.changeset/config.json index e8a5f2f..b2c83ab 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -5,7 +5,7 @@ "changelog": "@changesets/cli/changelog", "commit": false, "fixed": [], - "ignore": [], + "ignore": ["pathnorm-test"], "linked": [], "updateInternalDependencies": "patch" } diff --git a/.changeset/public-baboons-tan.md b/.changeset/public-baboons-tan.md new file mode 100644 index 0000000..5ea995d --- /dev/null +++ b/.changeset/public-baboons-tan.md @@ -0,0 +1,5 @@ +--- +"@knide/pathnorm": major +--- + +First release diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 056f4ed..0000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,5 +0,0 @@ -# Learn how to add code owners here - - - -* @your-org/your-username diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..6c00645 --- /dev/null +++ b/.github/FUNDING.yml @@ -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 diff --git a/.github/MAINTAINERS b/.github/MAINTAINERS index 15b7d29..30993ad 100644 --- a/.github/MAINTAINERS +++ b/.github/MAINTAINERS @@ -1 +1 @@ -your-org/your-username +ashuvssut diff --git a/.github/workflows/changeset-publish.yml b/.github/workflows/changeset-publish.yml index f3b1943..af61a6f 100644 --- a/.github/workflows/changeset-publish.yml +++ b/.github/workflows/changeset-publish.yml @@ -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 diff --git a/.github/workflows/pull_request_code_quality.yml b/.github/workflows/pull_request_code_quality.yml index 7c41358..5bca73d 100644 --- a/.github/workflows/pull_request_code_quality.yml +++ b/.github/workflows/pull_request_code_quality.yml @@ -44,3 +44,6 @@ jobs: - name: Run Package Build working-directory: package run: yarn build + + - name: Run Workspace Tests + run: yarn test diff --git a/LICENSE b/LICENSE index ab242de..dfa07b5 100644 --- a/LICENSE +++ b/LICENSE @@ -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 diff --git a/README.md b/README.md index 65682b6..84b71d0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# pathnorm +# @knide/pathnorm Normalize and join path/URL segments. Full Win32, UNC, POSIX, and URL support. @@ -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 ``` diff --git a/lefthook.yml b/lefthook.yml index 79160d5..cbaa1d8 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -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: # diff --git a/package.json b/package.json index 1253f26..7c20674 100644 --- a/package.json +++ b/package.json @@ -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": { @@ -20,7 +20,8 @@ "author": "Ashutosh Khanduala ", "license": "MIT", "workspaces": [ - "package" + "package", + "test" ], "packageManager": "yarn@4.12.0" } diff --git a/package/README.md b/package/README.md index 4b33f05..d065d66 100644 --- a/package/README.md +++ b/package/README.md @@ -1,4 +1,4 @@ -# pathnorm +# @knide/pathnorm Normalize and join path or URL segments into a single clean string. @@ -10,30 +10,30 @@ 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)` @@ -41,7 +41,7 @@ import { np, unixNp } from 'pathnorm' 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" @@ -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" @@ -98,12 +98,12 @@ 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) @@ -111,7 +111,7 @@ import { np } from 'pathnorm/posix' 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" diff --git a/package/package.json b/package/package.json index 5a47191..e9dd1b3 100644 --- a/package/package.json +++ b/package/package.json @@ -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", diff --git a/package/src/index.ts b/package/src/index.ts index e429f37..4abc199 100644 --- a/package/src/index.ts +++ b/package/src/index.ts @@ -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 diff --git a/test/np.test.mjs b/test/np.test.mjs new file mode 100644 index 0000000..9055318 --- /dev/null +++ b/test/np.test.mjs @@ -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}` + ) + }) +} diff --git a/test/package.json b/test/package.json new file mode 100644 index 0000000..f720dab --- /dev/null +++ b/test/package.json @@ -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:*" + } +} diff --git a/test/posix.test.mjs b/test/posix.test.mjs new file mode 100644 index 0000000..6cbff95 --- /dev/null +++ b/test/posix.test.mjs @@ -0,0 +1,74 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { np as npPosix } from '@knide/pathnorm/posix' + +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: ['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(`posix: ${name}`, () => { + const output = npPosix(...args) + assert.equal( + output, + expected, + `args=${JSON.stringify(args)} expected=${expected} got=${output}` + ) + }) +} diff --git a/yarn.lock b/yarn.lock index e80ed3b..3539ddf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -608,6 +608,17 @@ __metadata: languageName: node linkType: hard +"@knide/pathnorm@workspace:*, @knide/pathnorm@workspace:package": + version: 0.0.0-use.local + resolution: "@knide/pathnorm@workspace:package" + dependencies: + "@biomejs/biome": "npm:2.4.4" + lefthook: "npm:^2.1.1" + tsup: "npm:^8.5.1" + typescript: "npm:^5.9.3" + languageName: unknown + linkType: soft + "@manypkg/find-root@npm:^1.1.0": version: 1.1.0 resolution: "@manypkg/find-root@npm:1.1.0" @@ -2243,18 +2254,7 @@ __metadata: version: 0.0.0-use.local resolution: "pathnorm-test@workspace:test" dependencies: - pathnorm: "workspace:*" - languageName: unknown - linkType: soft - -"pathnorm@workspace:*, pathnorm@workspace:package": - version: 0.0.0-use.local - resolution: "pathnorm@workspace:package" - dependencies: - "@biomejs/biome": "npm:2.4.4" - lefthook: "npm:^2.1.1" - tsup: "npm:^8.5.1" - typescript: "npm:^5.9.3" + "@knide/pathnorm": "workspace:*" languageName: unknown linkType: soft