diff --git a/__tests__/github.test.ts b/__tests__/github.test.ts new file mode 100644 index 00000000..b5904a15 --- /dev/null +++ b/__tests__/github.test.ts @@ -0,0 +1,47 @@ +import { getGitHubMetaWithFallback, getGitHubPublicFork } from '@/admin/github'; +import { TEMPLATE_BASE_OWNER, TEMPLATE_BASE_REPO } from '@/site/config'; + +describe('GitHub', () => { + it('fetches base repo meta', async () => { + const meta = await getGitHubMetaWithFallback({ + owner: TEMPLATE_BASE_OWNER, + repo: TEMPLATE_BASE_REPO, + }); + expect(meta).toBeDefined(); + expect(meta.url).toBeDefined(); + expect(meta.isForkedFromBase).toEqual(false); + expect(meta.label).toBeDefined(); + expect(meta.title).toBeDefined(); + expect(meta.isBehind).toEqual(false); + expect(meta.isBaseRepo).toBe(true); + }); + it('fetches fork meta', async () => { + const fork = await getGitHubPublicFork(); + const metaFork = await getGitHubMetaWithFallback(fork); + expect(metaFork.isForkedFromBase).toEqual(true); + }); + it('handles nonexistent repos', async () => { + const meta = await getGitHubMetaWithFallback({ + owner: 'nonexistent', + repo: 'nonexistent', + }); + expect(meta).toBeDefined(); + expect(meta.url).toBeDefined(); + expect(meta.isForkedFromBase).toEqual(false); + expect(meta.label).toEqual('Unknown'); + expect(meta.title).toEqual('Unknown'); + expect(meta.isBehind).toBeUndefined(); + }); + it('handles fetch errors', async () => { + const meta = await getGitHubMetaWithFallback({ + owner: 'gibberish / / *', + repo: 'bad text for a url.com', + }); + expect(meta).toBeDefined(); + expect(meta.url).toBeDefined(); + expect(meta.isForkedFromBase).toEqual(false); + expect(meta.label).toEqual('Unknown'); + expect(meta.title).toEqual('Unknown'); + expect(meta.isBehind).toBeUndefined(); + }); +}); diff --git a/jest.config.mjs b/jest.config.ts similarity index 67% rename from jest.config.mjs rename to jest.config.ts index b063a90b..eb690b56 100644 --- a/jest.config.mjs +++ b/jest.config.ts @@ -1,4 +1,5 @@ /* eslint-disable max-len */ +import type { Config } from 'jest'; import nextJest from 'next/jest.js'; const createJestConfig = nextJest({ @@ -7,12 +8,10 @@ const createJestConfig = nextJest({ }); // Add any custom config to be passed to Jest -/** @type {import('jest').Config} */ -const config = { - // Add more setup options before each test is run - // setupFilesAfterEnv: ['/jest.setup.js'], - - testEnvironment: 'jest-environment-jsdom', +const config: Config = { + coverageProvider: 'v8', + testEnvironment: 'jsdom', + setupFilesAfterEnv: ['/jest.setup.ts'], }; // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async diff --git a/jest.setup.ts b/jest.setup.ts new file mode 100644 index 00000000..3673ac04 --- /dev/null +++ b/jest.setup.ts @@ -0,0 +1 @@ +import 'cross-fetch/polyfill'; \ No newline at end of file diff --git a/package.json b/package.json index 4f3cf89a..a905be0a 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@next/bundle-analyzer": "15.1.6", "@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/forms": "^0.5.10", + "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", "@types/jest": "^29.5.14", @@ -57,12 +58,14 @@ "@types/sanitize-html": "^2.13.0", "autoprefixer": "10.4.20", "clsx": "^2.1.1", + "cross-fetch": "^4.1.0", "eslint": "9.18.0", "eslint-config-next": "15.1.6", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "postcss": "8.5.1", "tailwindcss": "3.4.17", + "ts-node": "^10.9.2", "typescript": "5.7.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 890b8eb4..5b8add6a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,16 +113,19 @@ importers: version: 15.1.6 '@tailwindcss/container-queries': specifier: ^0.1.1 - version: 0.1.1(tailwindcss@3.4.17) + version: 0.1.1(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3))) '@tailwindcss/forms': specifier: ^0.5.10 - version: 0.5.10(tailwindcss@3.4.17) + version: 0.5.10(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3))) + '@testing-library/dom': + specifier: ^10.4.0 + version: 10.4.0 '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.6.3 '@testing-library/react': specifier: ^16.2.0 - version: 16.2.0(@testing-library/dom@10.1.0)(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@types/jest': specifier: ^29.5.14 version: 29.5.14 @@ -147,6 +150,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + cross-fetch: + specifier: ^4.1.0 + version: 4.1.0 eslint: specifier: 9.18.0 version: 9.18.0(jiti@1.21.7) @@ -155,7 +161,7 @@ importers: version: 15.1.6(eslint@9.18.0(jiti@1.21.7))(typescript@5.7.3) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.10.7) + version: 29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)) jest-environment-jsdom: specifier: ^29.7.0 version: 29.7.0 @@ -164,7 +170,10 @@ importers: version: 8.5.1 tailwindcss: specifier: 3.4.17 - version: 3.4.17 + version: 3.4.17(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@22.10.7)(typescript@5.7.3) typescript: specifier: 5.7.3 version: 5.7.3 @@ -566,6 +575,10 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + '@discoveryjs/json-ext@0.5.7': resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} @@ -847,6 +860,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@next/bundle-analyzer@15.1.6': resolution: {integrity: sha512-hGzQyDqJzFHcHNCyTqM3o05BpVq5tGnRODccZBVJDBf5Miv/26UJPMB0wh9L9j3ylgHC+0/v8BaBnBBek1rC6Q==} @@ -1487,8 +1503,8 @@ packages: peerDependencies: tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1' - '@testing-library/dom@10.1.0': - resolution: {integrity: sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==} + '@testing-library/dom@10.4.0': + resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} '@testing-library/jest-dom@6.6.3': @@ -1514,6 +1530,18 @@ packages: resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -1819,6 +1847,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} @@ -2112,6 +2143,12 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + cross-fetch@4.1.0: + resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} + cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -2250,6 +2287,10 @@ packages: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} @@ -3217,6 +3258,9 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} @@ -3355,6 +3399,15 @@ packages: sass: optional: true + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -4098,6 +4151,9 @@ packages: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@3.0.0: resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} engines: {node: '>=12'} @@ -4114,6 +4170,20 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} @@ -4233,6 +4303,9 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + v8-to-istanbul@9.2.0: resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==} engines: {node: '>=10.12.0'} @@ -4255,6 +4328,9 @@ packages: walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -4276,6 +4352,9 @@ packages: resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} engines: {node: '>=12'} + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -4371,6 +4450,10 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -5121,6 +5204,10 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + '@discoveryjs/json-ext@0.5.7': {} '@emnapi/runtime@1.2.0': @@ -5305,7 +5392,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0': + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -5319,7 +5406,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.10.7) + jest-config: 29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -5475,6 +5562,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@next/bundle-analyzer@15.1.6': dependencies: webpack-bundle-analyzer: 4.10.1 @@ -6169,16 +6261,16 @@ snapshots: dependencies: tslib: 2.8.1 - '@tailwindcss/container-queries@0.1.1(tailwindcss@3.4.17)': + '@tailwindcss/container-queries@0.1.1(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)))': dependencies: - tailwindcss: 3.4.17 + tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)) - '@tailwindcss/forms@0.5.10(tailwindcss@3.4.17)': + '@tailwindcss/forms@0.5.10(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)))': dependencies: mini-svg-data-uri: 1.4.4 - tailwindcss: 3.4.17 + tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)) - '@testing-library/dom@10.1.0': + '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.24.2 '@babel/runtime': 7.24.5 @@ -6199,10 +6291,10 @@ snapshots: lodash: 4.17.21 redent: 3.0.0 - '@testing-library/react@16.2.0(@testing-library/dom@10.1.0)(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@testing-library/react@16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@babel/runtime': 7.24.5 - '@testing-library/dom': 10.1.0 + '@testing-library/dom': 10.4.0 react: 19.0.0 react-dom: 19.0.0(react@19.0.0) optionalDependencies: @@ -6211,6 +6303,14 @@ snapshots: '@tootallnate/once@2.0.0': {} + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + '@types/aria-query@5.0.4': {} '@types/babel__core@7.20.5': @@ -6552,6 +6652,8 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + arg@4.1.3: {} + arg@5.0.2: {} argparse@1.0.10: @@ -6902,13 +7004,13 @@ snapshots: cookie@0.7.1: {} - create-jest@29.7.0(@types/node@22.10.7): + create-jest@29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.10.7) + jest-config: 29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -6917,6 +7019,14 @@ snapshots: - supports-color - ts-node + create-require@1.1.1: {} + + cross-fetch@4.1.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + cross-spawn@7.0.3: dependencies: path-key: 3.1.1 @@ -7029,6 +7139,8 @@ snapshots: diff-sequences@29.6.3: {} + diff@4.0.2: {} + dlv@1.1.3: {} doctrine@2.1.0: @@ -7926,16 +8038,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@22.10.7): + jest-cli@29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)): dependencies: - '@jest/core': 29.7.0 + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.10.7) + create-jest: 29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@22.10.7) + jest-config: 29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -7945,7 +8057,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@22.10.7): + jest-config@29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)): dependencies: '@babel/core': 7.24.5 '@jest/test-sequencer': 29.7.0 @@ -7971,6 +8083,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 22.10.7 + ts-node: 10.9.2(@types/node@22.10.7)(typescript@5.7.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -8205,12 +8318,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@22.10.7): + jest@29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)): dependencies: - '@jest/core': 29.7.0 + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)) '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@22.10.7) + jest-cli: 29.7.0(@types/node@22.10.7)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -8355,6 +8468,8 @@ snapshots: dependencies: semver: 7.6.3 + make-error@1.3.6: {} + makeerror@1.0.12: dependencies: tmpl: 1.0.5 @@ -8465,6 +8580,10 @@ snapshots: - '@babel/core' - babel-plugin-macros + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + node-int64@0.4.0: {} node-releases@2.0.14: {} @@ -8678,12 +8797,13 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.5.1 - postcss-load-config@4.0.2(postcss@8.5.1): + postcss-load-config@4.0.2(postcss@8.5.1)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)): dependencies: lilconfig: 3.1.3 yaml: 2.4.2 optionalDependencies: postcss: 8.5.1 + ts-node: 10.9.2(@types/node@22.10.7)(typescript@5.7.3) postcss-nested@6.2.0(postcss@8.5.1): dependencies: @@ -9194,7 +9314,7 @@ snapshots: symbol-tree@3.2.4: {} - tailwindcss@3.4.17: + tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -9213,7 +9333,7 @@ snapshots: postcss: 8.5.1 postcss-import: 15.1.0(postcss@8.5.1) postcss-js: 4.0.1(postcss@8.5.1) - postcss-load-config: 4.0.2(postcss@8.5.1) + postcss-load-config: 4.0.2(postcss@8.5.1)(ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3)) postcss-nested: 6.2.0(postcss@8.5.1) postcss-selector-parser: 6.1.2 resolve: 1.22.8 @@ -9256,6 +9376,8 @@ snapshots: universalify: 0.2.0 url-parse: 1.5.10 + tr46@0.0.3: {} + tr46@3.0.0: dependencies: punycode: 2.3.1 @@ -9270,6 +9392,24 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.10.7 + acorn: 8.14.0 + acorn-walk: 8.3.2 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.7.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 @@ -9391,6 +9531,8 @@ snapshots: uuid@9.0.1: {} + v8-compile-cache-lib@3.0.1: {} + v8-to-istanbul@9.2.0: dependencies: '@jridgewell/trace-mapping': 0.3.25 @@ -9418,6 +9560,8 @@ snapshots: dependencies: makeerror: 1.0.12 + webidl-conversions@3.0.1: {} + webidl-conversions@7.0.0: {} webpack-bundle-analyzer@4.10.1: @@ -9450,6 +9594,11 @@ snapshots: tr46: 3.0.0 webidl-conversions: 7.0.0 + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -9543,6 +9692,8 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yn@3.1.1: {} + yocto-queue@0.1.0: {} zod-to-json-schema@3.24.1(zod@3.23.8): diff --git a/src/admin/github/GitHubForkStatusBadgeServer.tsx b/src/admin/github/GitHubForkStatusBadgeServer.tsx index 39669026..afb1ebf5 100644 --- a/src/admin/github/GitHubForkStatusBadgeServer.tsx +++ b/src/admin/github/GitHubForkStatusBadgeServer.tsx @@ -4,7 +4,7 @@ import { VERCEL_GIT_REPO_OWNER, VERCEL_GIT_REPO_SLUG, } from '@/site/config'; -import { getGitHubMeta } from '.'; +import { getGitHubMetaWithFallback } from '.'; export default async function GitHubForkStatusBadgeServer() { const owner = VERCEL_GIT_REPO_OWNER; @@ -17,24 +17,14 @@ export default async function GitHubForkStatusBadgeServer() { label, title, isBehind, - } = await getGitHubMeta({ owner, repo, branch }) - .catch(() => { - console.error('Error retrieving GitHub meta', { owner, repo, branch }); - return { - url: undefined, - isForkedFromBase: false, - label: undefined, - title: undefined, - isBehind: undefined, - }; - }); + } = await getGitHubMetaWithFallback({ owner, repo, branch }); return isForkedFromBase ? : null; } diff --git a/src/admin/github/index.ts b/src/admin/github/index.ts index fbad31d0..129f352c 100644 --- a/src/admin/github/index.ts +++ b/src/admin/github/index.ts @@ -6,6 +6,14 @@ import { const DEFAULT_BRANCH = 'main'; +const FALLBACK_TEXT = 'Unknown'; + +// Cache all results for 2 minutes to avoid rate limiting +// GitHub API requests limited to 60 requests per hour +const FETCH_CONFIG: RequestInit = { + next: { revalidate: 120 }, +}; + interface RepoParams { owner?: string repo?: string @@ -39,6 +47,9 @@ const getGitHubApiRepoUrl = ({ const getGitHubApiCommitsUrl = (params?: RepoParams) => `${getGitHubApiRepoUrl(params)}/commits/main`; +const getGitHubApiForksUrl = (params?: RepoParams) => + `${getGitHubApiRepoUrl(params)}/forks`; + const getGitHubApiCompareUrl = ({ owner, repo, @@ -49,14 +60,14 @@ const getGitHubApiCompareUrl = ({ // Requests -const getLatestBaseRepoCommitSha = async () => { - const response = await fetch(getGitHubApiCommitsUrl()); +export const getLatestBaseRepoCommitSha = async () => { + const response = await fetch(getGitHubApiCommitsUrl(), FETCH_CONFIG); const data = await response.json(); - return data.sha.slice(0, 7) as string; + return data.sha ? data.sha.slice(0, 7) as string : undefined; }; const getIsRepoForkedFromBase = async (params: RepoParams) => { - const response = await fetch(getGitHubApiRepoUrl(params)); + const response = await fetch(getGitHubApiRepoUrl(params), FETCH_CONFIG); const data = await response.json(); return ( Boolean(data.fork) && @@ -65,7 +76,7 @@ const getIsRepoForkedFromBase = async (params: RepoParams) => { }; const getGitHubCommitsBehind = async (params?: RepoParams) => { - const response = await fetch(getGitHubApiCompareUrl(params)); + const response = await fetch(getGitHubApiCompareUrl(params), FETCH_CONFIG); const data = await response.json(); return data.behind_by as number; }; @@ -74,36 +85,69 @@ const isRepoBaseRepo = async ({ owner, repo }: RepoParams) => owner?.toLowerCase() === TEMPLATE_BASE_OWNER && repo?.toLowerCase() === TEMPLATE_BASE_REPO; -export const getGitHubMeta = async (params: RepoParams) => { +export const getGitHubPublicFork = async ( + params?: RepoParams, +): Promise => { + const response = await fetch(getGitHubApiForksUrl(params), FETCH_CONFIG); + const fork = (await response.json())[0]; + return { + owner: fork.owner.login, + repo: fork.name, + }; +}; + +const getGitHubMeta = async (params: RepoParams) => { const [ url, isForkedFromBase, isBaseRepo, - latestBaseRepoCommitSha, behindBy, ] = await Promise.all([ getGitHubRepoUrl(params), getIsRepoForkedFromBase(params), isRepoBaseRepo(params), - getLatestBaseRepoCommitSha(), getGitHubCommitsBehind(params), ]); - const isBehind = behindBy > 0; - const label = isBehind ? `${behindBy} Behind` : 'Synced'; - const title = isBehind - // eslint-disable-next-line max-len - ? `This fork is ${behindBy} commit${behindBy === 1 ? '' : 's'} behind. Consider syncing on GitHub for the latest updates.` - : 'This fork is up to date.'; + const isBehind = behindBy === undefined + ? undefined + : behindBy > 0; + + const label = isBehind === undefined + ? FALLBACK_TEXT + : isBehind + ? `${behindBy} Behind` + : 'Synced'; + + const title = isBehind === undefined + ? FALLBACK_TEXT + : isBehind + // eslint-disable-next-line max-len + ? `This fork is ${behindBy} commit${behindBy === 1 ? '' : 's'} behind. Consider syncing on GitHub for the latest updates.` + : 'This fork is up to date.'; return { url, isForkedFromBase, isBaseRepo, - latestBaseRepoCommitSha, behindBy, isBehind, label, title, }; }; + +export const getGitHubMetaWithFallback = (params: RepoParams) => + getGitHubMeta(params) + .catch(e => { + console.error('Error retrieving GitHub meta', { params, error: e }); + return { + url: undefined, + isForkedFromBase: false, + isBaseRepo: undefined, + behindBy: undefined, + isBehind: undefined, + label: FALLBACK_TEXT, + title: FALLBACK_TEXT, + }; + });