-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathconfig-utils.js
267 lines (237 loc) · 7.02 KB
/
config-utils.js
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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
/**
* configuration utilities for jupyter-lite
*
* this file may not import anything else, and exposes no API
*/
/*
* An `index.html` should `await import('../config-utils.js')` after specifying
* the key `script` tags...
*
* ```html
* <script id="jupyter-config-data" type="application/json" data-jupyter-lite-root="..">
* {}
* </script>
* ```
*/
const JUPYTER_CONFIG_ID = 'jupyter-config-data';
/*
* The JS-mangled name for `data-jupyter-lite-root`
*/
const LITE_ROOT_ATTR = 'jupyterLiteRoot';
/**
* The well-known filename that contains `#jupyter-config-data` and other goodies
*/
const LITE_FILES = ['jupyter-lite.json', 'jupyter-lite.ipynb'];
/**
* And this link tag, used like so to load a bundle after configuration.
*
* ```html
* <link
* id="jupyter-lite-main"
* rel="preload"
* href="../build/bundle.js?_=bad4a54"
* main="index"
* as="script"
* />
* ```
*/
const LITE_MAIN = 'jupyter-lite-main';
/**
* The current page, with trailing server junk stripped
*/
const HERE = `${window.location.origin}${window.location.pathname.replace(
/(\/|\/index.html)?$/,
'',
)}/`;
/**
* The computed composite configuration
*/
let _JUPYTER_CONFIG;
/**
* A handle on the config script, must exist, and will be overridden
*/
const CONFIG_SCRIPT = document.getElementById(JUPYTER_CONFIG_ID);
/**
* The relative path to the root of this JupyterLite
*/
const RAW_LITE_ROOT = CONFIG_SCRIPT.dataset[LITE_ROOT_ATTR];
/**
* The fully-resolved path to the root of this JupyterLite
*/
const FULL_LITE_ROOT = new URL(RAW_LITE_ROOT, HERE).toString();
/**
* Paths that are joined with baseUrl to derive full URLs
*/
const UNPREFIXED_PATHS = ['licensesUrl', 'themesUrl'];
/* a DOM parser for reading html files */
const parser = new DOMParser();
/**
* Merge `jupyter-config-data` on the current page with:
* - the contents of `.jupyter-lite#/jupyter-config-data`
* - parent documents, and their `.jupyter-lite#/jupyter-config-data`
* ...up to `jupyter-lite-root`.
*/
async function jupyterConfigData() {
/**
* Return the value if already cached for some reason
*/
if (_JUPYTER_CONFIG != null) {
return _JUPYTER_CONFIG;
}
let parent = new URL(HERE).toString();
let promises = [getPathConfig(HERE)];
while (parent != FULL_LITE_ROOT) {
parent = new URL('..', parent).toString();
promises.unshift(getPathConfig(parent));
}
const configs = (await Promise.all(promises)).flat();
let finalConfig = configs.reduce(mergeOneConfig);
// apply any final patches
finalConfig = dedupFederatedExtensions(finalConfig);
// hoist to cache
_JUPYTER_CONFIG = finalConfig;
return finalConfig;
}
/**
* Merge a new configuration on top of the existing config
*/
function mergeOneConfig(memo, config) {
for (const [k, v] of Object.entries(config)) {
switch (k) {
// this list of extension names is appended
case 'disabledExtensions':
case 'federated_extensions':
memo[k] = [...(memo[k] || []), ...v];
break;
// these `@org/pkg:plugin` are merged at the first level of values
case 'litePluginSettings':
case 'settingsOverrides':
if (!memo[k]) {
memo[k] = {};
}
for (const [plugin, defaults] of Object.entries(v || {})) {
memo[k][plugin] = { ...(memo[k][plugin] || {}), ...defaults };
}
break;
default:
memo[k] = v;
}
}
return memo;
}
function dedupFederatedExtensions(config) {
const originalList = Object.keys(config || {})['federated_extensions'] || [];
const named = {};
for (const ext of originalList) {
named[ext.name] = ext;
}
let allExtensions = [...Object.values(named)];
allExtensions.sort((a, b) => a.name.localeCompare(b.name));
return config;
}
/**
* Load jupyter config data from (this) page and merge with
* `jupyter-lite.json#jupyter-config-data`
*/
async function getPathConfig(url) {
let promises = [getPageConfig(url)];
for (const fileName of LITE_FILES) {
promises.unshift(getLiteConfig(url, fileName));
}
return Promise.all(promises);
}
/**
* The current normalized location
*/
function here() {
return window.location.href.replace(/(\/|\/index.html)?$/, '/');
}
/**
* Maybe fetch an `index.html` in this folder, which must contain the trailing slash.
*/
export async function getPageConfig(url = null) {
let script = CONFIG_SCRIPT;
if (url != null) {
const text = await (await window.fetch(`${url}index.html`)).text();
const doc = parser.parseFromString(text, 'text/html');
script = doc.getElementById(JUPYTER_CONFIG_ID);
}
return fixRelativeUrls(url, JSON.parse(script.textContent));
}
/**
* Fetch a jupyter-lite JSON or Notebook in this folder, which must contain the trailing slash.
*/
export async function getLiteConfig(url, fileName) {
let text = '{}';
let config = {};
const liteUrl = `${url || HERE}${fileName}`;
try {
text = await (await window.fetch(liteUrl)).text();
const json = JSON.parse(text);
const liteConfig = fileName.endsWith('.ipynb')
? json['metadata']['jupyter-lite']
: json;
config = liteConfig[JUPYTER_CONFIG_ID] || {};
} catch (err) {
console.warn(`failed get ${JUPYTER_CONFIG_ID} from ${liteUrl}`);
}
return fixRelativeUrls(url, config);
}
export function fixRelativeUrls(url, config) {
let urlBase = new URL(url || here()).pathname;
for (const [k, v] of Object.entries(config)) {
config[k] = fixOneRelativeUrl(k, v, url, urlBase);
}
return config;
}
export function fixOneRelativeUrl(key, value, url, urlBase) {
if (key === 'litePluginSettings' || key === 'settingsOverrides') {
// these are plugin id-keyed objects, fix each plugin
return Object.entries(value || {}).reduce((m, [k, v]) => {
m[k] = fixRelativeUrls(url, v);
return m;
}, {});
} else if (
!UNPREFIXED_PATHS.includes(key) &&
key.endsWith('Url') &&
value.startsWith('./')
) {
// themesUrls, etc. are joined in code with baseUrl, leave as-is: otherwise, clean
return `${urlBase}${value.slice(2)}`;
} else if (key.endsWith('Urls') && Array.isArray(value)) {
return value.map((v) => (v.startsWith('./') ? `${urlBase}${v.slice(2)}` : v));
}
return value;
}
/**
* Update with the as-configured favicon
*/
function addFavicon(config) {
const favicon = document.createElement('link');
favicon.rel = 'icon';
favicon.type = 'image/x-icon';
favicon.href = config.faviconUrl;
document.head.appendChild(favicon);
}
/**
* The main entry point.
*/
async function main() {
const config = await jupyterConfigData();
if (config.baseUrl === new URL(here()).pathname) {
window.location.href = config.appUrl.replace(/\/?$/, '/index.html');
return;
}
// rewrite the config
CONFIG_SCRIPT.textContent = JSON.stringify(config, null, 2);
addFavicon(config);
const preloader = document.getElementById(LITE_MAIN);
const bundle = document.createElement('script');
bundle.src = preloader.href;
bundle.main = preloader.attributes.main;
document.head.appendChild(bundle);
}
/**
* TODO: consider better pattern for invocation.
*/
await main();