diff --git a/app/port/controller/PackageVersionFileController.ts b/app/port/controller/PackageVersionFileController.ts index af277525..6e918086 100644 --- a/app/port/controller/PackageVersionFileController.ts +++ b/app/port/controller/PackageVersionFileController.ts @@ -145,10 +145,22 @@ export class PackageVersionFileController extends AbstractController { } const file = await this.packageVersionFileService.showPackageVersionFile(packageVersion, path); + const hasMeta = typeof meta === 'string'; + if (!file) { + const possibleFile = await this.#searchPossibleEntries(packageVersion, path); + + if (possibleFile) { + const route = `/${fullname}/${versionSpec}/files${possibleFile.path}${hasMeta ? '?meta' : ''}`; + + ctx.redirect(route); + + return; + } + throw new NotFoundError(`File ${fullname}@${versionSpec}${path} not found`); } - const hasMeta = typeof meta === 'string'; + if (hasMeta) { ctx.set('cache-control', META_CACHE_CONTROL); return formatFileItem(file); @@ -161,6 +173,26 @@ export class PackageVersionFileController extends AbstractController { return await this.distRepository.getDistStream(file.dist); } + /** + * compatibility with unpkg + * 1. try to match alias entry. e.g. accessing `index.js` or `index.json` using /index + * 2. if given path is directory and has `index.js` file, redirect to it. e.g. using `lib` alias to access `lib/index.js` or `lib/index.json` + * @param {PackageVersion} packageVersion packageVersion + * @param {String} path filepath + * @return {Promise} return packageVersionFile or null + */ + async #searchPossibleEntries(packageVersion: PackageVersion, path: string) { + const possiblePath = [ `${path}.js`, `${path}.json`, `${path}/index.js`, `${path}/index.json` ]; + + for (const pathItem of possiblePath) { + const file = await this.packageVersionFileService.showPackageVersionFile(packageVersion, pathItem); + + if (file) { + return file; + } + } + } + async #getPackageVersion(ctx: EggContext, fullname: string, scope: string, name: string, versionSpec: string) { const { blockReason, packageVersion } = await this.packageManagerService.showPackageVersionByVersionOrTag( scope, name, versionSpec); diff --git a/test/fixtures/@cnpm/cnpm-test-find-entry-1.0.0.tgz b/test/fixtures/@cnpm/cnpm-test-find-entry-1.0.0.tgz new file mode 100644 index 00000000..75ff07c3 Binary files /dev/null and b/test/fixtures/@cnpm/cnpm-test-find-entry-1.0.0.tgz differ diff --git a/test/port/controller/PackageVersionFileController/listFiles.test.ts b/test/port/controller/PackageVersionFileController/listFiles.test.ts index 392baede..8c27596e 100644 --- a/test/port/controller/PackageVersionFileController/listFiles.test.ts +++ b/test/port/controller/PackageVersionFileController/listFiles.test.ts @@ -3,6 +3,8 @@ import { setTimeout } from 'node:timers/promises'; import { app, mock } from 'egg-mock/bootstrap'; import { TestUtil } from '../../../../test/TestUtil'; import { PackageVersionFileService } from '../../../../app/core/service/PackageVersionFileService'; +import { calculateIntegrity } from '../../../../app/common/PackageUtil'; + describe('test/port/controller/PackageVersionFileController/listFiles.test.ts', () => { let publisher; @@ -352,5 +354,50 @@ describe('test/port/controller/PackageVersionFileController/listFiles.test.ts', assert.equal(called, 1); assert.equal(resList.filter(res => res.status === 409 && res.body.error === '[CONFLICT] Package version file sync is currently in progress. Please try again later.').length, 1); }); + it('should redirect to possible entry', async () => { + const tarball = await TestUtil.readFixturesFile('@cnpm/cnpm-test-find-entry-1.0.0.tgz'); + const { integrity } = await calculateIntegrity(tarball); + const pkg = await TestUtil.getFullPackage({ + name: '@cnpm/test-find-entry', + version: '1.0.0', + versionObject: { + description: 'test find entry description', + }, + attachment: { + data: tarball.toString('base64'), + length: tarball.length, + }, + dist: { + integrity, + }, + }); + + await app.httpRequest() + .put(`/${pkg.name}`) + .set('authorization', publisher.authorization) + .set('user-agent', publisher.ua) + .send(pkg) + .expect(201); + + await app.httpRequest() + .get(`/${pkg.name}/1.0.0/files/es/array/at`) + .expect(302) + .expect('location', `/${pkg.name}/1.0.0/files/es/array/at.js`); + + await app.httpRequest() + .get(`/${pkg.name}/1.0.0/files/es/array`) + .expect(302) + .expect('location', `/${pkg.name}/1.0.0/files/es/array/index.js`); + + await app.httpRequest() + .get(`/${pkg.name}/1.0.0/files/es/json/test`) + .expect(302) + .expect('location', `/${pkg.name}/1.0.0/files/es/json/test.json`); + + await app.httpRequest() + .get(`/${pkg.name}/1.0.0/files/es/json`) + .expect(302) + .expect('location', `/${pkg.name}/1.0.0/files/es/json/index.json`); + }); }); });