Skip to content

Commit

Permalink
lite
Browse files Browse the repository at this point in the history
  • Loading branch information
bollwyvl committed Sep 3, 2023
1 parent 971d47f commit 7469be0
Show file tree
Hide file tree
Showing 184 changed files with 5,942 additions and 8 deletions.
877 changes: 877 additions & 0 deletions _static/SHA256SUMS

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions _static/api/translations/all.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"data": {
"en": {
"displayName": "English",
"nativeName": "English"
}
},
"message": ""
}
4 changes: 4 additions & 0 deletions _static/api/translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"data": {},
"message": ""
}
93 changes: 93 additions & 0 deletions _static/bootstrap.js
Original file line number Diff line number Diff line change
@@ -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();
})();
267 changes: 267 additions & 0 deletions _static/config-utils.js
Original file line number Diff line number Diff line change
@@ -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
* <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();
14 changes: 14 additions & 0 deletions _static/doc/workspaces/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<script>
// redirect to /lab by default
(function () {
window.location.href = window.location.href.replace(
/(\/|\/index.html)?$/,
'/../../lab/index.html'
);
}.call(this));
</script>
</head>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"packageManager": "python",
"packageName": "jupyterlab_widgets",
"uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyterlab_widgets"
}
Loading

0 comments on commit 7469be0

Please sign in to comment.