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