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
4 changes: 4 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions src/packument/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,10 @@ export class PackumentClient extends BaseHTTPClient {
async request(packageName, options = {}) {
await this.ensureInitialized();

// Build URL
const url = new URL(`/${encodeURIComponent(packageName)}`, this.origin);

// Build URL - append package name to existing path (preserves /javascript, etc.)
const url = new URL(this.origin);
const basePath = url.pathname.endsWith('/') ? url.pathname : url.pathname + '/';
url.pathname = basePath + encodeURIComponent(packageName);
const {
signal,
staleWhileRevalidate = true,
Expand Down
6 changes: 5 additions & 1 deletion src/packument/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@
"debug": "^4.4.1",
"p-map": "catalog:"
},
"devDependencies": {
"rimraf": "^6.0.1"
},
"author": "Charlie Robbins <npm@charlie.dev>",
"license": "Apache-2.0",
"scripts": {
"lint": "xo",
"lint:fix": "xo --fix"
"lint:fix": "xo --fix",
"test": "node --test test/*.test.js"
},
"bugs": {
"url": "https://github.com/indexzero/_all_docs/issues"
Expand Down
172 changes: 172 additions & 0 deletions src/packument/test/client.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { describe, it } from 'node:test';
import { join } from 'node:path';
import { ok, equal, match } from 'node:assert/strict';
import { rimraf } from 'rimraf';
import { PackumentClient } from '../client.js';

const fixtures = join(import.meta.dirname, 'fixtures');

describe('PackumentClient', () => {
describe('URL construction', () => {
it('should construct URL correctly for standard registry', async () => {
const client = new PackumentClient({
origin: 'https://registry.npmjs.org',
env: {
RUNTIME: 'node',
CACHE_DIR: fixtures
}
});

// Access the internal URL construction logic by checking origin
const url = new URL(client.origin);
const basePath = url.pathname.endsWith('/') ? url.pathname : url.pathname + '/';
url.pathname = basePath + encodeURIComponent('lodash');

equal(url.href, 'https://registry.npmjs.org/lodash');
});

it('should preserve path segments in origin URL', async () => {
const client = new PackumentClient({
origin: 'https://packages.example.com/javascript',
env: {
RUNTIME: 'node',
CACHE_DIR: fixtures
}
});

// Simulate the URL construction logic from client.request()
const url = new URL(client.origin);
const basePath = url.pathname.endsWith('/') ? url.pathname : url.pathname + '/';
url.pathname = basePath + encodeURIComponent('lodash');

equal(url.href, 'https://packages.example.com/javascript/lodash');
});

it('should preserve path with trailing slash', async () => {
const client = new PackumentClient({
origin: 'https://packages.example.com/javascript/',
env: {
RUNTIME: 'node',
CACHE_DIR: fixtures
}
});

const url = new URL(client.origin);
const basePath = url.pathname.endsWith('/') ? url.pathname : url.pathname + '/';
url.pathname = basePath + encodeURIComponent('lodash');

equal(url.href, 'https://packages.example.com/javascript/lodash');
});

it('should handle scoped packages with path segments', async () => {
const client = new PackumentClient({
origin: 'https://packages.example.com/javascript',
env: {
RUNTIME: 'node',
CACHE_DIR: fixtures
}
});

const url = new URL(client.origin);
const basePath = url.pathname.endsWith('/') ? url.pathname : url.pathname + '/';
url.pathname = basePath + encodeURIComponent('@babel/core');

equal(url.href, 'https://packages.example.com/javascript/%40babel%2Fcore');
});

it('should handle deep path segments', async () => {
const client = new PackumentClient({
origin: 'https://registry.example.com/api/v2/npm',
env: {
RUNTIME: 'node',
CACHE_DIR: fixtures
}
});

const url = new URL(client.origin);
const basePath = url.pathname.endsWith('/') ? url.pathname : url.pathname + '/';
url.pathname = basePath + encodeURIComponent('express');

equal(url.href, 'https://registry.example.com/api/v2/npm/express');
});
});

describe('request', () => {
it('.request(lodash) returns a valid packument', async () => {
const client = new PackumentClient({
origin: 'https://registry.npmjs.org',
env: {
RUNTIME: 'node',
CACHE_DIR: fixtures
}
});

const entry = await client.request('lodash');

ok(entry, 'entry should exist');
ok(entry.body, 'entry should have body');
equal(entry.body.name, 'lodash');
ok(entry.body.versions, 'packument should have versions');
ok(Object.keys(entry.body.versions).length > 0, 'should have at least one version');
});

it('.request(@babel/core) handles scoped packages', async () => {
const client = new PackumentClient({
origin: 'https://registry.npmjs.org',
env: {
RUNTIME: 'node',
CACHE_DIR: fixtures
}
});

const entry = await client.request('@babel/core');

ok(entry, 'entry should exist');
ok(entry.body, 'entry should have body');
equal(entry.body.name, '@babel/core');
ok(entry.body.versions, 'packument should have versions');
});

it('.request(nonexistent-package-xyz-123) returns null for 404', async () => {
const client = new PackumentClient({
origin: 'https://registry.npmjs.org',
env: {
RUNTIME: 'node',
CACHE_DIR: fixtures
}
});

const entry = await client.request('nonexistent-package-xyz-123-definitely-not-real');

equal(entry, null, 'should return null for 404');
});
});

describe('requestAll', () => {
it('.requestAll([...packages]) returns multiple packuments', async () => {
const client = new PackumentClient({
origin: 'https://registry.npmjs.org',
env: {
RUNTIME: 'node',
CACHE_DIR: fixtures
}
});

const entries = await client.requestAll(['debug', 'semver']);

equal(entries.length, 2);
for (const entry of entries) {
ok(entry, 'entry should exist');
ok(entry.body, 'entry should have body');
ok(entry.body.versions, 'packument should have versions');
}
});
});

it('(cleanup) delete the local packument cache', async () => {
await rimraf(join(fixtures, 'packuments'), {
maxRetries: 1,
retryDelay: 1
});
});
});