diff --git a/jup/api/contents/all.json b/jup/api/contents/all.json new file mode 100644 index 000000000..5d1285d40 --- /dev/null +++ b/jup/api/contents/all.json @@ -0,0 +1,43 @@ +{ + "content": [ + { + "content": null, + "created": "2024-07-24T23:29:38.303699Z", + "format": null, + "hash": null, + "hash_algorithm": null, + "last_modified": "2024-07-24T23:11:58Z", + "mimetype": "image/fits", + "name": "pixel_num.fits", + "path": "pixel_num.fits", + "size": 0, + "type": "file", + "writable": true + }, + { + "content": null, + "created": "2024-07-24T23:29:38.304699Z", + "format": null, + "hash": null, + "hash_algorithm": null, + "last_modified": "2024-07-24T23:06:12.000704Z", + "mimetype": null, + "name": "visualize_map.ipynb", + "path": "visualize_map.ipynb", + "size": 2380, + "type": "notebook", + "writable": true + } + ], + "created": "2024-07-24T23:29:38.304699Z", + "format": "json", + "hash": null, + "hash_algorithm": null, + "last_modified": "2024-07-24T23:29:38.304699Z", + "mimetype": null, + "name": "", + "path": "", + "size": null, + "type": "directory", + "writable": true +} \ No newline at end of file diff --git a/jup/api/translations/all.json b/jup/api/translations/all.json new file mode 100644 index 000000000..0f1a90ee5 --- /dev/null +++ b/jup/api/translations/all.json @@ -0,0 +1,9 @@ +{ + "data": { + "en": { + "displayName": "English", + "nativeName": "English" + } + }, + "message": "" +} \ No newline at end of file diff --git a/jup/api/translations/en.json b/jup/api/translations/en.json new file mode 100644 index 000000000..2d378ee6a --- /dev/null +++ b/jup/api/translations/en.json @@ -0,0 +1,4 @@ +{ + "data": {}, + "message": "" +} \ No newline at end of file diff --git a/jup/bootstrap.js b/jup/bootstrap.js new file mode 100644 index 000000000..917b4f59c --- /dev/null +++ b/jup/bootstrap.js @@ -0,0 +1,93 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) Jupyter Development Team. +| Distributed under the terms of the Modified BSD License. +|----------------------------------------------------------------------------*/ + +// We copy some of the pageconfig parsing logic in @jupyterlab/coreutils +// below, since this must run before any other files are loaded (including +// @jupyterlab/coreutils). + +/** + * Get global configuration data for the Jupyter application. + * + * @param name - The name of the configuration option. + * + * @returns The config value or an empty string if not found. + * + * #### Notes + * All values are treated as strings. For browser based applications, it is + * assumed that the page HTML includes a script tag with the id + * `jupyter-config-data` containing the configuration as valid JSON. + */ + +let _CONFIG_DATA = null; +function getOption(name) { + if (_CONFIG_DATA === null) { + let configData = {}; + // Use script tag if available. + if (typeof document !== 'undefined' && document) { + const el = document.getElementById('jupyter-config-data'); + + if (el) { + configData = JSON.parse(el.textContent || '{}'); + } + } + _CONFIG_DATA = configData; + } + + return _CONFIG_DATA[name] || ''; +} + +// eslint-disable-next-line no-undef +__webpack_public_path__ = getOption('fullStaticUrl') + '/'; + +function loadScript(url) { + return new Promise((resolve, reject) => { + const newScript = document.createElement('script'); + newScript.onerror = reject; + newScript.onload = resolve; + newScript.async = true; + document.head.appendChild(newScript); + newScript.src = url; + }); +} + +async function loadComponent(url, scope) { + await loadScript(url); + + // From https://webpack.js.org/concepts/module-federation/#dynamic-remote-containers + await __webpack_init_sharing__('default'); + const container = window._JUPYTERLAB[scope]; + // Initialize the container, it may provide shared modules and may need ours + await container.init(__webpack_share_scopes__.default); +} + +void (async function bootstrap() { + // This is all the data needed to load and activate plugins. This should be + // gathered by the server and put onto the initial page template. + const extension_data = getOption('federated_extensions'); + + // We first load all federated components so that the shared module + // deduplication can run and figure out which shared modules from all + // components should be actually used. We have to do this before importing + // and using the module that actually uses these components so that all + // dependencies are initialized. + let labExtensionUrl = getOption('fullLabextensionsUrl'); + const extensions = await Promise.allSettled( + extension_data.map(async data => { + await loadComponent(`${labExtensionUrl}/${data.name}/${data.load}`, data.name); + }) + ); + + extensions.forEach(p => { + if (p.status === 'rejected') { + // There was an error loading the component + console.error(p.reason); + } + }); + + // Now that all federated containers are initialized with the main + // container, we can import the main function. + let main = (await import('./index.js')).main; + void main(); +})(); diff --git a/jup/config-utils.js b/jup/config-utils.js new file mode 100644 index 000000000..cfbb51a1d --- /dev/null +++ b/jup/config-utils.js @@ -0,0 +1,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 + * + * ``` + */ +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(); diff --git a/jup/consoles/favicon.ico b/jup/consoles/favicon.ico new file mode 100644 index 000000000..97fcfd543 Binary files /dev/null and b/jup/consoles/favicon.ico differ diff --git a/jup/consoles/index.html b/jup/consoles/index.html new file mode 100644 index 000000000..314c34ff5 --- /dev/null +++ b/jup/consoles/index.html @@ -0,0 +1,37 @@ + + +
+