/
AstExplorerResolver.ts
98 lines (78 loc) · 2.32 KB
/
AstExplorerResolver.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import * as fs from 'fs';
import { URL } from 'whatwg-url';
import NetworkResolver from './NetworkResolver';
import { promisify } from 'util';
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
const EDITOR_HASH_PATTERN = /^#\/gist\/(\w+)(?:\/(\w+))?$/;
/**
* Resolves plugins from AST Explorer transforms.
*
* astexplorer.net uses GitHub gists to save and facilitate sharing. This
* resolver accepts either the editor URL or the gist API URL.
*/
export default class AstExplorerResolver extends NetworkResolver {
constructor(
private readonly baseURL: URL = new URL('https://astexplorer.net/')
) {
super();
}
async canResolve(source: string): Promise<boolean> {
if (await super.canResolve(source)) {
const url = new URL(await this.normalize(source));
return (
this.matchesHost(url) &&
/^\/api\/v1\/gist\/[a-f0-9]+(\/[a-f0-9]+)?$/.test(url.pathname)
);
}
return false;
}
async resolve(source: string): Promise<string> {
const filename = await super.resolve(await this.normalize(source));
const text = await readFile(filename, { encoding: 'utf8' });
let data;
try {
data = JSON.parse(text);
} catch {
throw new Error(
`data loaded from ${source} is not JSON: ${text.slice(0, 100)}`
);
}
if (
!data ||
!data.files ||
!data.files['transform.js'] ||
!data.files['transform.js'].content
) {
throw new Error(
"'transform.js' could not be found, perhaps transform is disabled"
);
}
await writeFile(filename, data.files['transform.js'].content, {
encoding: 'utf8'
});
return filename;
}
async normalize(source: string): Promise<string> {
const url = new URL(source);
if (!this.matchesHost(url)) {
return source;
}
const match = url.hash.match(EDITOR_HASH_PATTERN);
if (!match) {
return source;
}
let path = `/api/v1/gist/${match[1]}`;
if (match[2]) {
path += `/${match[2]}`;
}
return new URL(path, this.baseURL).toString();
}
private matchesHost(url: URL): boolean {
if (url.host !== this.baseURL.host) {
return false;
}
// use SSL even if the URL doesn't use it
return url.protocol === this.baseURL.protocol || url.protocol === 'http:';
}
}