From df16d0806afcf53fbf1a13caf8b08bc3b00856be Mon Sep 17 00:00:00 2001 From: Xi Xu Date: Fri, 25 Jul 2025 14:16:26 +0800 Subject: [PATCH] Refactor platform config and unify path transformation Simplifies PLATFORMS to map platform keys to base URLs and introduces a unified transformPath function for path normalization. Updates usage in src/index.js and refactors tests to validate the new structure and container registry support. --- src/config/index.js | 5 +- src/config/platforms.js | 92 +++++++-------- src/index.js | 4 +- test/platforms.test.js | 252 ++++++++++++++++++++++++++-------------- 4 files changed, 213 insertions(+), 140 deletions(-) diff --git a/src/config/index.js b/src/config/index.js index 9ee56d4..b7ad13d 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -1,4 +1,4 @@ -import { PLATFORMS } from './platforms'; +import { PLATFORMS, transformPath } from './platforms'; /** * @typedef {Object} SecurityConfig @@ -28,5 +28,6 @@ export const CONFIG = { ALLOWED_ORIGINS: ['*'], MAX_PATH_LENGTH: 2048 }, - PLATFORMS + PLATFORMS, + transformPath }; diff --git a/src/config/platforms.js b/src/config/platforms.js index 70b44b5..8558240 100644 --- a/src/config/platforms.js +++ b/src/config/platforms.js @@ -1,51 +1,49 @@ /** - * Configuration object for different platform URLs and path transformations - * @type {Object.} + * Configuration object for different platform base URLs + * @type {Object.} */ export const PLATFORMS = { - /** @type {{base: string, transform: function(string): string}} GitHub configuration */ - gh: { - base: 'https://github.com', - transform: path => path.replace(/^\/gh\//, '/') - }, - /** @type {{base: string, transform: function(string): string}} GitLab configuration */ - gl: { - base: 'https://gitlab.com', - transform: path => path.replace(/^\/gl\//, '/') - }, - /** @type {{base: string, transform: function(string): string}} Hugging Face configuration */ - hf: { - base: 'https://huggingface.co', - transform: path => path.replace(/^\/hf\//, '/') - }, - /** @type {{base: string, transform: function(string): string}} npm registry configuration */ - npm: { - base: 'https://registry.npmjs.org', - transform: path => path.replace(/^\/npm\//, '/') - }, - /** @type {{base: string, transform: function(string): string}} PyPI registry configuration */ - pypi: { - base: 'https://pypi.org', - transform: path => path.replace(/^\/pypi\//, '/') - }, - /** @type {{base: string, transform: function(string): string}} PyPI files configuration */ - 'pypi-files': { - base: 'https://files.pythonhosted.org', - transform: path => path.replace(/^\/pypi\/files\//, '/') - }, - /** @type {{base: string, transform: function(string): string}} conda default channels configuration */ - conda: { - base: 'https://repo.anaconda.com', - transform: path => path.replace(/^\/conda\//, '/') - }, - /** @type {{base: string, transform: function(string): string}} conda community channels configuration */ - 'conda-community': { - base: 'https://conda.anaconda.org', - transform: path => path.replace(/^\/conda\/community\//, '/') - } - // /** @type {{base: string, transform: function(string): string}} All platforms */ - // link: { - // base: "https://", - // transform: (path) => path.replace(/^\/link\//, "/"), - // }, + gh: 'https://github.com', + gl: 'https://gitlab.com', + hf: 'https://huggingface.co', + npm: 'https://registry.npmjs.org', + pypi: 'https://pypi.org', + 'pypi-files': 'https://files.pythonhosted.org', + conda: 'https://repo.anaconda.com', + 'conda-community': 'https://conda.anaconda.org', + + // Container Registries + 'cr-docker': 'https://registry-1.docker.io', + 'cr-quay': 'https://quay.io', + 'cr-gcr': 'https://gcr.io', + 'cr-mcr': 'https://mcr.microsoft.com', + 'cr-ecr': 'https://public.ecr.aws', + 'cr-ghcr': 'https://ghcr.io', + 'cr-gitlab': 'https://registry.gitlab.com', + 'cr-redhat': 'https://registry.redhat.io', + 'cr-nvidia': 'https://nvcr.io', + 'cr-oracle': 'https://container-registry.oracle.com', + 'cr-cloudsmith': 'https://docker.cloudsmith.io', + 'cr-digitalocean': 'https://registry.digitalocean.com', + 'cr-vmware': 'https://projects.registry.vmware.com', + 'cr-k8s': 'https://registry.k8s.io', + 'cr-heroku': 'https://registry.heroku.com', + 'cr-suse': 'https://registry.suse.com', + 'cr-opensuse': 'https://registry.opensuse.org', + 'cr-gitpod': 'https://registry.gitpod.io' + // link: "https://", }; + +/** + * Unified path transformation function + * @param {string} path - The original path + * @param {string} platformKey - The platform key + * @returns {string} - The transformed path + */ +export function transformPath(path, platformKey) { + if (!PLATFORMS[platformKey]) { + return path; + } + const prefix = `/${platformKey.replace(/-/g, '/')}/`; + return path.replace(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`), '/'); +} diff --git a/src/index.js b/src/index.js index 8d64654..b9f5ff2 100644 --- a/src/index.js +++ b/src/index.js @@ -157,8 +157,8 @@ async function handleRequest(request, env, ctx) { return Response.redirect(HOME_PAGE_URL, 302); } - // Transform URL based on platform - const targetPath = CONFIG.PLATFORMS[platform].transform(url.pathname); + // Transform URL based on platform using unified logic + const targetPath = CONFIG.transformPath(url.pathname, platform); const targetUrl = `${CONFIG.PLATFORMS[platform].base}${targetPath}${url.search}`; // Check if this is a Git operation diff --git a/test/platforms.test.js b/test/platforms.test.js index 0925cc5..0a48d6e 100644 --- a/test/platforms.test.js +++ b/test/platforms.test.js @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { PLATFORMS } from '../src/config/platforms.js'; +import { PLATFORMS, transformPath } from '../src/config/platforms.js'; describe('Platform Configuration', () => { describe('Platform Definitions', () => { @@ -13,167 +13,241 @@ describe('Platform Configuration', () => { }); it('should have valid base URLs for all platforms', () => { - Object.entries(PLATFORMS).forEach(([key, config]) => { - expect(config.base).toBeDefined(); - expect(config.base).toMatch(/^https?:\/\/.+/); + Object.values(PLATFORMS).forEach(baseUrl => { + expect(baseUrl).toBeDefined(); + expect(baseUrl).toMatch(/^https?:\/\/.+/); }); }); - it('should have transform functions for all platforms', () => { - Object.entries(PLATFORMS).forEach(([key, config]) => { - expect(config.transform).toBeDefined(); - expect(typeof config.transform).toBe('function'); - }); + it('should have unified transform function', () => { + expect(transformPath).toBeDefined(); + expect(typeof transformPath).toBe('function'); }); }); - describe('GitHub Platform', () => { + describe('Unified Transform Function', () => { it('should transform GitHub paths correctly', () => { - const transform = PLATFORMS.gh.transform; - - expect(transform('/gh/microsoft/vscode/archive/main.zip')).toBe( + expect(transformPath('/gh/microsoft/vscode/archive/main.zip', 'gh')).toBe( '/microsoft/vscode/archive/main.zip' ); - expect(transform('/gh/user/repo.git')).toBe('/user/repo.git'); + expect(transformPath('/gh/user/repo.git', 'gh')).toBe('/user/repo.git'); }); - it('should have correct base URL', () => { - expect(PLATFORMS.gh.base).toBe('https://github.com'); - }); - }); - - describe('GitLab Platform', () => { it('should transform GitLab paths correctly', () => { - const transform = PLATFORMS.gl.transform; - - expect(transform('/gl/gitlab-org/gitlab/-/archive/master/gitlab-master.zip')).toBe( + expect(transformPath('/gl/gitlab-org/gitlab/-/archive/master/gitlab-master.zip', 'gl')).toBe( '/gitlab-org/gitlab/-/archive/master/gitlab-master.zip' ); }); - it('should have correct base URL', () => { - expect(PLATFORMS.gl.base).toBe('https://gitlab.com'); - }); - }); - - describe('Hugging Face Platform', () => { it('should transform Hugging Face paths correctly', () => { - const transform = PLATFORMS.hf.transform; - - expect(transform('/hf/microsoft/DialoGPT-medium/resolve/main/config.json')).toBe( + expect(transformPath('/hf/microsoft/DialoGPT-medium/resolve/main/config.json', 'hf')).toBe( '/microsoft/DialoGPT-medium/resolve/main/config.json' ); - expect(transform('/hf/datasets/squad/resolve/main/train.json')).toBe( + expect(transformPath('/hf/datasets/squad/resolve/main/train.json', 'hf')).toBe( '/datasets/squad/resolve/main/train.json' ); }); - it('should have correct base URL', () => { - expect(PLATFORMS.hf.base).toBe('https://huggingface.co'); - }); - }); - - describe('npm Platform', () => { it('should transform npm paths correctly', () => { - const transform = PLATFORMS.npm.transform; - - expect(transform('/npm/react/-/react-18.2.0.tgz')).toBe('/react/-/react-18.2.0.tgz'); - - expect(transform('/npm/lodash')).toBe('/lodash'); + expect(transformPath('/npm/react/-/react-18.2.0.tgz', 'npm')).toBe( + '/react/-/react-18.2.0.tgz' + ); + expect(transformPath('/npm/lodash', 'npm')).toBe('/lodash'); }); - it('should have correct base URL', () => { - expect(PLATFORMS.npm.base).toBe('https://registry.npmjs.org'); - }); - }); - - describe('PyPI Platform', () => { it('should transform PyPI paths correctly', () => { - const transform = PLATFORMS.pypi.transform; - - expect(transform('/pypi/packages/source/r/requests/requests-2.31.0.tar.gz')).toBe( + expect(transformPath('/pypi/packages/source/r/requests/requests-2.31.0.tar.gz', 'pypi')).toBe( '/packages/source/r/requests/requests-2.31.0.tar.gz' ); - expect(transform('/pypi/simple/requests/')).toBe('/simple/requests/'); + expect(transformPath('/pypi/simple/requests/', 'pypi')).toBe('/simple/requests/'); }); - it('should have correct base URL', () => { - expect(PLATFORMS.pypi.base).toBe('https://pypi.org'); + it('should transform PyPI files paths correctly', () => { + expect( + transformPath('/pypi/files/packages/source/r/requests/requests-2.31.0.tar.gz', 'pypi-files') + ).toBe('/packages/source/r/requests/requests-2.31.0.tar.gz'); }); - }); - describe('conda Platform', () => { it('should transform conda default channel paths correctly', () => { - const transform = PLATFORMS.conda.transform; - - expect(transform('/conda/pkgs/main/linux-64/numpy-1.24.3.conda')).toBe( + expect(transformPath('/conda/pkgs/main/linux-64/numpy-1.24.3.conda', 'conda')).toBe( '/pkgs/main/linux-64/numpy-1.24.3.conda' ); }); it('should transform conda community channel paths correctly', () => { - const transform = PLATFORMS.conda.transform; - - expect(transform('/conda/community/conda-forge/linux-64/repodata.json')).toBe( - '/conda-forge/linux-64/repodata.json' - ); + expect( + transformPath('/conda/community/conda-forge/linux-64/repodata.json', 'conda-community') + ).toBe('/conda-forge/linux-64/repodata.json'); }); - it('should have correct base URLs', () => { - expect(PLATFORMS.conda.base).toBe('https://repo.anaconda.com'); - expect(PLATFORMS.conda.communityBase).toBe('https://conda.anaconda.org'); + it('should transform container registry paths correctly', () => { + expect(transformPath('/cr/docker/v2/library/nginx/manifests/latest', 'cr-docker')).toBe( + '/v2/library/nginx/manifests/latest' + ); + + expect(transformPath('/cr/ghcr/v2/microsoft/vscode/manifests/latest', 'cr-ghcr')).toBe( + '/v2/microsoft/vscode/manifests/latest' + ); + + expect(transformPath('/cr/gcr/v2/google-containers/pause/manifests/3.9', 'cr-gcr')).toBe( + '/v2/google-containers/pause/manifests/3.9' + ); + }); + }); + + describe('Platform Base URLs', () => { + it('should have correct GitHub base URL', () => { + expect(PLATFORMS.gh).toBe('https://github.com'); + }); + + it('should have correct GitLab base URL', () => { + expect(PLATFORMS.gl).toBe('https://gitlab.com'); + }); + + it('should have correct Hugging Face base URL', () => { + expect(PLATFORMS.hf).toBe('https://huggingface.co'); + }); + + it('should have correct npm base URL', () => { + expect(PLATFORMS.npm).toBe('https://registry.npmjs.org'); + }); + + it('should have correct PyPI base URL', () => { + expect(PLATFORMS.pypi).toBe('https://pypi.org'); + }); + + it('should have correct PyPI files base URL', () => { + expect(PLATFORMS['pypi-files']).toBe('https://files.pythonhosted.org'); + }); + + it('should have correct conda base URLs', () => { + expect(PLATFORMS.conda).toBe('https://repo.anaconda.com'); + expect(PLATFORMS['conda-community']).toBe('https://conda.anaconda.org'); + }); + + it('should have correct container registry base URLs', () => { + expect(PLATFORMS['cr-docker']).toBe('https://registry-1.docker.io'); + expect(PLATFORMS['cr-ghcr']).toBe('https://ghcr.io'); + expect(PLATFORMS['cr-gcr']).toBe('https://gcr.io'); + expect(PLATFORMS['cr-mcr']).toBe('https://mcr.microsoft.com'); + expect(PLATFORMS['cr-nvidia']).toBe('https://nvcr.io'); }); }); describe('Path Transformation Edge Cases', () => { it('should handle empty paths gracefully', () => { - Object.entries(PLATFORMS).forEach(([key, config]) => { - expect(() => config.transform('')).not.toThrow(); + Object.keys(PLATFORMS).forEach(key => { + expect(() => transformPath('', key)).not.toThrow(); }); }); it('should handle paths without platform prefix', () => { - Object.entries(PLATFORMS).forEach(([key, config]) => { + Object.keys(PLATFORMS).forEach(key => { const testPath = '/some/random/path'; - expect(() => config.transform(testPath)).not.toThrow(); + expect(() => transformPath(testPath, key)).not.toThrow(); }); }); - it('should handle paths with query parameters', () => { - const transform = PLATFORMS.gh.transform; + it('should handle unknown platform keys', () => { + const testPath = '/unknown/test/path'; + expect(transformPath(testPath, 'unknown')).toBe(testPath); + }); - expect(transform('/gh/user/repo/file.txt?ref=main')).toBe('/user/repo/file.txt?ref=main'); + it('should handle paths with query parameters', () => { + expect(transformPath('/gh/user/repo/file.txt?ref=main', 'gh')).toBe( + '/user/repo/file.txt?ref=main' + ); }); it('should handle paths with fragments', () => { - const transform = PLATFORMS.gh.transform; - - expect(transform('/gh/user/repo/README.md#section')).toBe('/user/repo/README.md#section'); + expect(transformPath('/gh/user/repo/README.md#section', 'gh')).toBe( + '/user/repo/README.md#section' + ); }); }); describe('URL Construction', () => { it('should construct valid URLs for all platforms', () => { - Object.entries(PLATFORMS).forEach(([key, config]) => { - const testPath = `/${key}/test/path`; - const transformedPath = config.transform(testPath); - const fullUrl = config.base + transformedPath; + Object.entries(PLATFORMS).forEach(([key, baseUrl]) => { + const testPath = `/${key.replace('-', '/')}/test/path`; + const transformedPath = transformPath(testPath, key); + const fullUrl = baseUrl + transformedPath; expect(() => new URL(fullUrl)).not.toThrow(); }); }); - it('should handle conda community URLs correctly', () => { - const config = PLATFORMS.conda; - const communityPath = '/conda/community/conda-forge/test'; - const transformedPath = config.transform(communityPath); - const fullUrl = config.communityBase + transformedPath; + it('should handle container registry URL construction', () => { + const testPath = '/cr/docker/v2/library/nginx/manifests/latest'; + const transformedPath = transformPath(testPath, 'cr-docker'); + const fullUrl = PLATFORMS['cr-docker'] + transformedPath; expect(() => new URL(fullUrl)).not.toThrow(); - expect(fullUrl).toContain('conda.anaconda.org'); + expect(fullUrl).toBe('https://registry-1.docker.io/v2/library/nginx/manifests/latest'); + }); + }); + + describe('Container Registry Support', () => { + it('should have all major container registries defined', () => { + const containerRegistries = [ + 'cr-docker', + 'cr-quay', + 'cr-gcr', + 'cr-mcr', + 'cr-ecr', + 'cr-ghcr', + 'cr-gitlab', + 'cr-redhat', + 'cr-nvidia', + 'cr-oracle', + 'cr-cloudsmith', + 'cr-digitalocean', + 'cr-vmware', + 'cr-k8s', + 'cr-heroku', + 'cr-suse', + 'cr-opensuse', + 'cr-gitpod' + ]; + + containerRegistries.forEach(registry => { + expect(PLATFORMS).toHaveProperty(registry); + expect(PLATFORMS[registry]).toBeDefined(); + expect(typeof PLATFORMS[registry]).toBe('string'); + }); + }); + + it('should transform all container registry paths correctly', () => { + const containerRegistries = [ + 'cr-docker', + 'cr-quay', + 'cr-gcr', + 'cr-mcr', + 'cr-ecr', + 'cr-ghcr', + 'cr-gitlab', + 'cr-redhat', + 'cr-nvidia', + 'cr-oracle', + 'cr-cloudsmith', + 'cr-digitalocean', + 'cr-vmware', + 'cr-k8s', + 'cr-heroku', + 'cr-suse', + 'cr-opensuse', + 'cr-gitpod' + ]; + + containerRegistries.forEach(registry => { + const prefix = registry.replace('cr-', 'cr/'); + const testPath = `/${prefix}/v2/test/image/manifests/latest`; + const transformedPath = transformPath(testPath, registry); + expect(transformedPath).toBe('/v2/test/image/manifests/latest'); + }); }); }); });