/**
* 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
*
* ```
*/
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
*
* ```
*/
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();