import 'urlpattern-polyfill'; // only for local dev import { html, HTMLResponse } from '@worker-tools/html'; import { ok, badRequest, permanentRedirect, forbidden } from '@worker-tools/response-creators'; import { WorkerRouter } from '@worker-tools/router'; import { provides } from '@worker-tools/middleware'; import { StorageArea } from '@worker-tools/cloudflare-kv-storage'; import { dedent } from 'ts-dedent' import { stylesHandler } from './styles'; const navigator = self.navigator || { userAgent: 'Cloudflare Workers' } // only for local dev const defaultBranchStorage = new StorageArea('default-branches'); const defaultPathStorage = new StorageArea('default-path'); const layout = (title, content) => html` ${title}

GHUC.CC

ghuc.cc = GitHub User Content Carbon Copy
Your friendly neighborhood redirection service for Deno 🦕 to import code directly from GitHub.


${content}
` const mkGHUC_href = ({ user, repo, branchOrTag, path }) => `https://raw.githubusercontent.com/${user}/${repo}/${branchOrTag}/${path}` const mkPage = ({ user, repo, branchOrTag, path }, url) => { return new HTMLResponse(layout('ghuc.cc', html`
📦 ${new URL(new URL(url).pathname, 'https://ghuc.cc')}
➡️ ${mkGHUC_href({ user, repo, branchOrTag, path })}

`)) } const mkInfo = (response) => { const ghucV = (self.GITHUB_SHA || '2c877da').substring(0, 7); return new HTMLResponse(layout('ghuc.cc', html`
Needs to match pattern /:user/:repo{\@:version}?{/:path(.*)}?. Examples:
`), response) } const mkError = (response) => { return new HTMLResponse(layout('ghuc.cc', html`
Something went wrong: ${response.text().then(x => `${response.status}: ${x}`)}.
`), response) } const ghAPI = (href, request) => { return fetch(new URL(href, 'https://api.github.com').href, { headers: { 'Accept': 'application/vnd.github.v3+json', 'User-Agent': navigator.userAgent, ...request.headers.has('authorization') ? { 'Authorization': request.headers.get('authorization') } : {}, }, }) } const fetchHEAD = (href, request) => { return fetch(href, { method: 'HEAD', headers: { 'User-Agent': navigator.userAgent, ...request.headers.has('authorization') ? { 'Authorization': request.headers.get('authorization') } : {}, } }); } async function inferDefaultBranch({ user, repo, maybePath }, { request }) { // In case there's also no path we use .gitignore as a last resort to test for a file that is likely to be present. const path = maybePath && !maybePath.endsWith('/') ? maybePath : '.gitignore' for (const maybeBranch of ['master', 'main']) { const res = await fetchHEAD(mkGHUC_href({ user, repo, branchOrTag: maybeBranch, path }), request); if (res.ok) return maybeBranch } throw forbidden(dedent`ghuc.cc reached GitHub API's rate limit and cannot infer the default branch via heuristics. You can provide the default branch via @ specifier after the repository name, or you can draw from your own rate limit by providing a 'Authorization' headers with a GitHub personal access token. In Deno this is achieved via the DENO_AUTH_TOKENS environment variable. For more, see: https://deno.land/manual/linking_to_external_code/private#deno_auth_tokens`) } async function getBranchOrTag({ user, repo, version, maybePath }, { request, waitUntil }) { if (version) { return version.match(/^\d+\.\d+\.\d+/) ? version.endsWith('!') ? version.substring(0, version.length - 1) : `v${version}` : version; } else { let defaultBranch = await defaultBranchStorage.get([user, repo]) if (!defaultBranch) { const gh = await ghAPI(`/repos/${user}/${repo}`, request) if (gh.ok) { defaultBranch = (await gh.json()).default_branch; } else { if (gh.status === 403 && gh.headers.get('x-ratelimit-remaining') === '0') { defaultBranch = await inferDefaultBranch({ user, repo, maybePath }, { request }) } else { throw new Response(`Response from GitHub not ok: ${gh.status}`, gh) } } waitUntil(defaultBranchStorage.set([user, repo], defaultBranch, { expirationTtl: 60 * 60 * 24 * 30 * 3 })) } return defaultBranch; } } const stripLast = s => s.substring(0, s.length - 1) async function getPath({ user, repo, branchOrTag, maybePath }, { request, waitUntil }) { if (maybePath && !maybePath.endsWith('/')) return maybePath; const storageKey = [user, repo, ...maybePath ? [stripLast(maybePath)] : []]; let path = await defaultPathStorage.get(storageKey) if (!path) { const dir = maybePath || ''; for (path of ['index.ts', 'mod.ts', 'index.js', 'mod.js'].map(p => dir + p)) { const res = await fetchHEAD(mkGHUC_href({ user, repo, branchOrTag, path }), request); if (res.ok) { waitUntil(defaultPathStorage.set(storageKey, path, { expirationTtl: 60 * 60 * 24 * 30 * 3 })) return path } } throw badRequest('Couldn\'t determine file path. Provide a full path or ensure index.ts/mod.ts exists in the root') } return path; } const mw = provides(['text/html', '*/*']); const assetsRouter = new WorkerRouter() .get('/index.css', stylesHandler) .recover('*', mw, (_, { type, response }) => { if (type === 'text/html') return mkError(response) return response; }) const router = new WorkerRouter(mw) .get('/favicon.ico', () => ok()) // TODO .use('/_public/*', assetsRouter) .get('/:handle/:repo(@?[^@/]+){@:version([^/]+)}?{/:path(.*)}?', async (request, { match, type, waitUntil }) => { const { pathname: { groups: { handle, repo, version, path: maybePath } } } = match; const user = handle.startsWith('@') ? handle.substring(1) : handle const branchOrTag = await getBranchOrTag({ user, repo, version, maybePath }, { request, waitUntil }) const path = await getPath({ user, repo, branchOrTag, maybePath }, { request, waitUntil }) if (type === 'text/html') return mkPage({ user, repo, branchOrTag, path }, request.url) return permanentRedirect(mkGHUC_href({ user, repo, branchOrTag, path })); }) .get('/', (_, { type }) => { if (type === 'text/html') return mkInfo() return ok("Needs to match pattern '/:user/:repo{\@:version}?{/:path(.*)}?'") }) .any('*', (_, { type }) => { if (type === 'text/html') return mkInfo(badRequest()) return badRequest("Needs to match pattern '/:user/:repo{\@:version}?{/:path(.*)}?'") }) .recover('*', mw, (_, { type, response }) => { if (type === 'text/html') return mkError(response) return response; }) self.addEventListener('fetch', router);