More plugins: splashscreen and importmap (#938)

This PR move codes from main.ts into two new plugins:

- splashscreen (formerly known as py-loader)
- importmap

The old setting config.autoclose_loader is still supported but deprecated; the new setting is config.splashscreen.autoclose.

Moreover, it does a small refactoring around UserError: now UserErrors are correctly caught even if they are raised from within afterRuntimeLoad.
This commit is contained in:
Antonio Cuni
2022-11-16 18:08:17 +01:00
committed by GitHub
parent b79ceea7a8
commit 41ebaaf366
19 changed files with 498 additions and 284 deletions

View File

@@ -0,0 +1,56 @@
import type { Runtime } from '../runtime';
import { showWarning } from '../utils';
import { Plugin } from '../plugin';
import { getLogger } from '../logger';
const logger = getLogger('plugins/importmap');
type ImportType = { [key: string]: unknown };
type ImportMapType = {
imports: ImportType | null;
};
export class ImportmapPlugin extends Plugin {
async afterSetup(runtime: Runtime) {
// make importmap ES modules available from python using 'import'.
//
// XXX: this code can probably be improved because errors are silently
// ignored.
//
// Moreover, it's also wrong because it's async and currently we don't
// await the module to be fully registered before executing the code
// inside py-script. It's also unclear whether we want to wait or not
// (or maybe only wait only if we do an actual 'import'?)
for (const node of document.querySelectorAll("script[type='importmap']")) {
const importmap: ImportMapType = (() => {
try {
return JSON.parse(node.textContent) as ImportMapType;
}
catch(error) {
showWarning("Failed to parse import map: " + error.message);
}
})();
if (importmap?.imports == null) continue;
for (const [name, url] of Object.entries(importmap.imports)) {
if (typeof name != 'string' || typeof url != 'string') continue;
let exports: object;
try {
// XXX: pyodide doesn't like Module(), failing with
// "can't read 'name' of undefined" at import time
exports = { ...(await import(url)) } as object;
} catch {
logger.warn(`failed to fetch '${url}' for '${name}'`);
continue;
}
logger.info("Registering JS module", name);
runtime.registerJsModule(name, exports);
}
}
}
}

View File

@@ -1,5 +1,6 @@
import type { PyScriptApp } from '../main';
import type { AppConfig } from '../pyconfig';
import type { Runtime } from '../runtime';
import { Plugin } from '../plugin';
import { UserError } from "../exceptions"
import { getLogger } from '../logger';
@@ -46,7 +47,7 @@ export class PyTerminalPlugin extends Plugin {
}
}
afterSetup() {
afterSetup(runtime: Runtime) {
// the Python interpreter has been initialized and we are ready to
// execute user code:
//

View File

@@ -0,0 +1,103 @@
import type { PyScriptApp } from '../main';
import type { AppConfig } from '../pyconfig';
import type { UserError } from '../exceptions';
import type { Runtime } from '../runtime';
import { showWarning } from '../utils';
import { Plugin } from '../plugin';
import { getLogger } from '../logger';
const logger = getLogger('py-splashscreen');
const AUTOCLOSE_LOADER_DEPRECATED = `
The setting autoclose_loader is deprecated. Please use the
following instead:<br>
<pre>
&lt;py-config&gt;
[splashscreen]
autoclose = false
&lt;/py-config&gt;
</pre>`;
export class SplashscreenPlugin extends Plugin {
elem: PySplashscreen;
autoclose: boolean;
configure(config: AppConfig) {
// the officially supported setting is config.splashscreen.autoclose,
// but we still also support the old config.autoclose_loader (with a
// deprecation warning)
this.autoclose = true;
if ("autoclose_loader" in config) {
this.autoclose = config.autoclose_loader;
showWarning(AUTOCLOSE_LOADER_DEPRECATED);
}
if (config.splashscreen) {
this.autoclose = config.splashscreen.autoclose ?? true;
}
}
beforeLaunch(config: AppConfig) {
// add the splashscreen to the DOM
logger.info('add py-splashscreen');
customElements.define('py-splashscreen', PySplashscreen);
this.elem = <PySplashscreen>document.createElement('py-splashscreen');
document.body.append(this.elem);
document.addEventListener("py-status-message", (e: CustomEvent) => {
const msg = e.detail;
this.elem.log(msg);
});
}
afterStartup(runtime: Runtime) {
if (this.autoclose) {
this.elem.close();
}
}
onUserError(error: UserError) {
if (this.elem !== undefined) {
// Remove the splashscreen so users can see the banner better
this.elem.close();
}
}
}
export class PySplashscreen extends HTMLElement {
widths: string[];
label: string;
mount_name: string;
details: HTMLElement;
operation: HTMLElement;
constructor() {
super();
}
connectedCallback() {
this.innerHTML = `<div id="pyscript_loading_splash" class="py-overlay">
<div class="py-pop-up">
<div class="smooth spinner"></div>
<div id="pyscript-loading-label" class="label">
<div id="pyscript-operation-details">
</div>
</div>
</div>
</div>`;
this.mount_name = this.id.split('-').join('_');
this.operation = document.getElementById('pyscript-operation');
this.details = document.getElementById('pyscript-operation-details');
}
log(msg: string) {
const newLog = document.createElement('p');
newLog.innerText = msg;
this.details.appendChild(newLog);
}
close() {
logger.info('Closing');
this.remove();
}
}