Skip to content

Commit a6fd7f6

Browse files
committed
Initial commit
0 parents  commit a6fd7f6

12 files changed

+642
-0
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules
2+
dist
3+
4+
\.idea/

.npmrc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package-lock=false

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2019 Interactive.Training
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# unpkg-cloudflare-worker
2+
3+
## WIP
4+
5+
A self-hosted version of unpkg developed specifically to run using Cloudflare Workers

package.json

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "unpkg-cloudflare-worker",
3+
"version": "0.0.1",
4+
"description": "",
5+
"main": "src/index.ts",
6+
"private": true,
7+
"scripts": {
8+
"build": "webpack --progress --mode production",
9+
"build.dev": "webpack --progress --mode none"
10+
},
11+
"author": "Interactive Training",
12+
"license": "MIT",
13+
"devDependencies": {
14+
"@types/node": "^12.7.1",
15+
"@types/pako": "^1.0.1",
16+
"@udacity/types-service-worker-mock": "^1.0.1",
17+
"cloudflare-worker-mock": "^1.0.1",
18+
"ts-loader": "^6.0.4",
19+
"types-cloudflare-worker": "^1.0.1",
20+
"typescript": "^3.5.3",
21+
"webpack": "^4.39.1",
22+
"webpack-cli": "^3.3.6"
23+
},
24+
"dependencies": {
25+
"pako": "^1.0.10",
26+
"mime-types": "^2.1.24"
27+
}
28+
}

src/gunzip.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as pako from 'pako';
2+
3+
function concatArrayBuffer(arrayBufferList = []) {
4+
const resultTypedArray = new Uint8Array(arrayBufferList.reduce((o, arrayBuffer) => o + arrayBuffer.byteLength, 0));
5+
let byteOffset = 0;
6+
arrayBufferList.forEach((arrayBuffer) => {
7+
const {byteLength} = arrayBuffer;
8+
resultTypedArray.set(new Uint8Array(arrayBuffer), byteOffset);
9+
byteOffset += byteLength
10+
});
11+
return resultTypedArray.buffer
12+
}
13+
14+
// https://github.com/nodeca/pako/issues/35#issuecomment-437341187
15+
export function gunzipAll(arrayBuffer: ArrayBuffer) {
16+
const arrayBufferList = [];
17+
let byteOffset = 0;
18+
let byteLeft = 1;
19+
while (byteLeft > 0) {
20+
const inflator = new pako.Inflate() as any;
21+
inflator.push(new Uint8Array(arrayBuffer, byteOffset));
22+
if (inflator.err) throw inflator.msg;
23+
arrayBufferList.push(inflator.result);
24+
byteOffset += inflator.strm.total_in;
25+
byteLeft = inflator.strm.avail_in
26+
}
27+
return concatArrayBuffer(arrayBufferList)
28+
}

src/helpers.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import {IPackageParams} from './interfaces';
2+
import {untarBuffer} from './tar';
3+
import {gunzipAll} from './gunzip';
4+
5+
6+
export function getRegistryOptions(): RequestInit {
7+
let options: RequestInit = {};
8+
9+
// if (process.env.NPM_TOKEN && process.env.NPM_TOKEN.trim().length > 0) {
10+
// options.headers = {authorization: `Bearer ${process.env.NPM_TOKEN.trim()}`};
11+
// } else {
12+
// options.auth = `${process.env.NPM_USER}:${process.env.NPM_PASSWORD}`;
13+
// }
14+
15+
return options;
16+
}
17+
18+
export function getPackageUrl(pkg: IPackageParams): string {
19+
return `https://registry.npmjs.com/${(pkg.scope) ? `${pkg.scope}/` : ''}${pkg.package}`;
20+
}
21+
22+
export async function getLatestVersion(pkg: IPackageParams): Promise<string> {
23+
return new Promise(async (resolve, reject) => {
24+
let res = await fetch(getPackageUrl(pkg), getRegistryOptions());
25+
let body = await res.json();
26+
const tags = body['dist-tags'];
27+
resolve(tags[pkg.version] || tags['latest']);
28+
});
29+
}
30+
31+
32+
export async function downloadFile(pkg: IPackageParams): Promise<string> {
33+
let url = `${getPackageUrl(pkg)}/-/${pkg.package}-${pkg.version}.tgz`;
34+
console.log('download', url);
35+
let response = await fetch(url, getRegistryOptions());
36+
37+
let arrayBuffer = await response.arrayBuffer();
38+
let gunzipBuffer = gunzipAll(arrayBuffer);
39+
40+
return untarBuffer(gunzipBuffer, pkg['0']);
41+
}

src/index.ts

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import CloudflareWorkerGlobalScope from 'types-cloudflare-worker';
2+
import {IPackageParams} from './interfaces';
3+
import {downloadFile, getLatestVersion} from './helpers';
4+
import * as mime from 'mime-types';
5+
6+
declare var self: CloudflareWorkerGlobalScope;
7+
8+
export class Worker {
9+
public async handle(event: FetchEvent) {
10+
let url = new URL(event.request.url);
11+
let path = url.pathname;
12+
let scope: string;
13+
let pkg: string;
14+
let version: string;
15+
let file: string = path.slice(1).split('/').map(el => el.split('@')).slice(2).join("/");
16+
const splits = path.slice(1).split('/').map(el => el.split('@')).slice(0, 2);
17+
18+
if (splits[0] instanceof Array && splits[0][0].length === 0) {
19+
scope = splits[0][1];
20+
pkg = splits[1][0];
21+
version = splits[1][1];
22+
} else {
23+
pkg = splits[0][0];
24+
version = splits[0][1];
25+
file = (file && file.length > 0) ? [splits[1][0], file].join("/") : splits[1][0];
26+
}
27+
28+
let params: IPackageParams = {
29+
['package']: pkg,
30+
version: version,
31+
scope: (scope && scope.length > 0) ? `@${scope}` : undefined,
32+
'0': file
33+
};
34+
35+
if (!params.version) {
36+
params.version = 'latest';
37+
}
38+
39+
if (!params.version.includes('.')) {
40+
params.version = await getLatestVersion(params);
41+
const redirectUrl = `${url.origin}/${(params.scope) ? `${params.scope}/` : ``}${params.package}@${params.version}/${params['0']}`;
42+
console.log('redirect', redirectUrl);
43+
return Response.redirect(redirectUrl, 302);
44+
}
45+
46+
// if file path is empty - respond with directory listing
47+
if (params['0'] === '') {
48+
//await getPackageFileList(params);
49+
return new Response(JSON.stringify({route: 'browse'}), {
50+
status: 200,
51+
headers: {
52+
'Content-Type': 'application/json',
53+
'Cache-Control': 'public, max-age=31536000'
54+
}
55+
});
56+
} else {
57+
return new Response(await downloadFile(params), {
58+
status: 200,
59+
headers: {
60+
'Content-Type': mime.lookup(params['0']),
61+
'Cache-Control': 'public, max-age=31536000'
62+
}
63+
});
64+
}
65+
}
66+
67+
68+
public async handleTwo(event: FetchEvent) {
69+
return new Response(JSON.stringify({test: 'route-2'}), {status: 200});
70+
}
71+
}
72+
73+
self.addEventListener('fetch', (event: FetchEvent) => {
74+
const worker = new Worker();
75+
// const url = new URL(event.request.url);
76+
//
77+
// const route = (): keyof Worker => {
78+
// if (url.host === 'test.example.com') {
79+
// return 'handle';
80+
// } else if (url.host === 'test2.example.com') {
81+
// return 'handleTwo';
82+
// }
83+
// };
84+
//
85+
// event.respondWith(worker[route()](event));
86+
87+
event.respondWith(worker.handle(event));
88+
});

src/interfaces.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
export interface IPackageParams {
2+
scope?: string;
3+
package: string;
4+
version?: string;
5+
0?: string;
6+
}
7+
8+
export interface ITarFile {
9+
name: string;
10+
mode: string;
11+
mtime: number;
12+
uid: any;
13+
gid: any;
14+
size: number;
15+
checksum: number;
16+
type: string;
17+
linkname: string;
18+
ustarFormat: string;
19+
version: string;
20+
uname: string;
21+
gname: string;
22+
devmajor: number;
23+
devminor: number;
24+
namePrefix: string;
25+
buffer: ArrayBuffer;
26+
content: string;
27+
}
28+
29+
export interface IPAXField {
30+
name: string;
31+
value: string;
32+
}

0 commit comments

Comments
 (0)