-
Notifications
You must be signed in to change notification settings - Fork 292
/
extensions.ts
162 lines (144 loc) · 4.91 KB
/
extensions.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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
/**
* Fava extensions might contain their own Javascript code, this module
* contains the functionality to handle them.
*/
import { get as store_get } from "svelte/store";
import { getUrlPath, urlFor } from "./helpers";
import { fetch } from "./lib/fetch";
import { log_error } from "./log";
import { extensions } from "./stores";
/** Helpers to make requests. */
export class ExtensionApi {
constructor(private readonly name: string) {}
/** Send a request to an extension endpoint. */
async request(
endpoint: string,
method: "GET" | "PUT" | "POST" | "DELETE",
params?: Record<string, string | number>,
body?: unknown,
output: "json" | "string" | "raw" = "json",
): Promise<unknown> {
const url = urlFor(`extension/${this.name}/${endpoint}`, params, false);
let opts = {};
if (body != null) {
opts =
body instanceof FormData
? { body }
: {
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
};
}
const response = await fetch(url, { method, ...opts });
if (output === "json") {
return response.json();
}
if (output === "string") {
return response.text();
}
return response;
}
/** GET an endpoint with parameters and return JSON. */
async get(
endpoint: string,
params: Record<string, string>,
): Promise<unknown> {
return this.request(endpoint, "GET", params, undefined);
}
/** GET an endpoint with a body and return JSON. */
async put(endpoint: string, body?: unknown): Promise<unknown> {
return this.request(endpoint, "PUT", undefined, body);
}
/** POST to an endpoint with a body and return JSON. */
async post(endpoint: string, body?: unknown): Promise<unknown> {
return this.request(endpoint, "POST", undefined, body);
}
/** DELETE an endpoint and return JSON. */
async delete(endpoint: string): Promise<unknown> {
return this.request(endpoint, "DELETE");
}
}
/** The context that an extensions handlers are called with. */
export interface ExtensionContext {
/** Helpers to make requests. */
api: ExtensionApi;
}
/**
* The Javascript code of a Fava extension should export an object of this type.
*
* The extension will be initialised when Fava loads by a call to init(). It can also
* provider handlers that are run on each subsequent page load (either all or just
* pages of the extension itself).
*/
export interface ExtensionModule {
/** Initialise this Javascript module / run some code on the initial load. */
init?: (c: ExtensionContext) => void | Promise<void>;
/** Run some code after any Fava page has loaded. */
onPageLoad?: (c: ExtensionContext) => void;
/** Run some code after a page for this extension has loaded. */
onExtensionPageLoad?: (c: ExtensionContext) => void;
}
class ExtensionData {
constructor(
private readonly extension: ExtensionModule,
private readonly context: ExtensionContext,
) {}
async init(): Promise<void> {
await this.extension.init?.(this.context);
}
onPageLoad(): void {
this.extension.onPageLoad?.(this.context);
}
onExtensionPageLoad(): void {
this.extension.onExtensionPageLoad?.(this.context);
}
}
async function loadExtensionModule(name: string): Promise<ExtensionData> {
const url = urlFor(`extension_js_module/${name}.js`, undefined, false);
const mod = await (import(url) as Promise<{ default?: ExtensionModule }>);
if (typeof mod.default === "object") {
return new ExtensionData(mod.default, { api: new ExtensionApi(name) });
}
throw new Error(
`Error importing module for extension ${name}: module must export "default" object`,
);
}
/** A map of all extensions modules that have been (requested to be) loaded already. */
const loaded_extensions = new Map<string, Promise<ExtensionData>>();
/** Get the extensions module - if it has not been imported yet, initialise it. */
async function getOrInitExtension(name: string): Promise<ExtensionData> {
const loaded_ext = loaded_extensions.get(name);
if (loaded_ext) {
return loaded_ext;
}
const ext_promise = loadExtensionModule(name);
loaded_extensions.set(name, ext_promise);
await (await ext_promise).init();
return ext_promise;
}
/**
* On page load, run check if the new page is an extension report page and run hooks.
*/
export function handleExtensionPageLoad(): void {
const exts = store_get(extensions).filter((e) => e.has_js_module);
for (const { name } of exts) {
// Run the onPageLoad handler for all pages.
getOrInitExtension(name)
.then((m) => {
m.onPageLoad();
})
.catch(log_error);
}
const path = getUrlPath(window.location) ?? "";
if (path.startsWith("extension/")) {
for (const { name } of exts) {
if (path.startsWith(`extension/${name}`)) {
getOrInitExtension(name)
.then((m) => {
m.onExtensionPageLoad();
})
.catch(log_error);
}
}
}
}