Refactor py-config and the general initialization logic of the page (#806)

This PR is the first step to improve and rationalize the life-cycle of a pyscript app along the lines of what I described in #763 .
It is not a complete solution, more PRs will follow.
Highlights:

- py-config is no longer a web component: the old code relied on PyConfig.connectedCallback to do some logic, but then if no <py-config> tag was present, we had to introduce a dummy one with the sole goal of activating the callback. Now the logic is much more linear.

- the new pyconfig.ts only contains the code which is needed to parse the config; I also moved some relevant code from utils.ts because it didn't really belong to it

- the old PyConfig class did much more than dealing with the config: in particular, it contained the code to initialize the env and the runtime. Now this logic has been moved directly into main.ts, inside the new PyScriptApp class. I plan to refactor the initialization code in further PRs

- the current code relies too much on global state and global variables, they are everywhere. This PR is a first step to solve the problem by introducing a PyScriptApp class, which will hold all the mutable state of the page. Currently only config is stored there, but eventually I will migrate more state to it, until we will have only one global singleton, globalApp

- thanks to what I described above, I could kill the appConfig svelte store: one less store to kill :).
This commit is contained in:
Antonio Cuni
2022-10-04 14:26:12 +02:00
committed by GitHub
parent 4011a51013
commit c75f885cb4
12 changed files with 526 additions and 463 deletions

View File

@@ -1,31 +1,104 @@
import './styles/pyscript_base.css';
import { loadConfigFromElement } from './pyconfig';
import type { AppConfig } from './pyconfig';
import type { Runtime } from './runtime';
import { PyScript } from './components/pyscript';
import { PyEnv } from './components/pyenv';
import { PyLoader } from './components/pyloader';
import { PyConfig } from './components/pyconfig';
import { PyodideRuntime } from './pyodide';
import { getLogger } from './logger';
import { globalLoader } from './stores';
import { globalLoader, runtimeLoaded, addInitializer } from './stores';
import { handleFetchError, globalExport } from './utils'
const logger = getLogger('pyscript/main');
/* eslint-disable @typescript-eslint/no-unused-vars */
const xPyScript = customElements.define('py-script', PyScript);
const xPyLoader = customElements.define('py-loader', PyLoader);
const xPyConfig = customElements.define('py-config', PyConfig);
const xPyEnv = customElements.define('py-env', PyEnv);
/* eslint-disable @typescript-eslint/no-unused-vars */
// XXX this should be killed eventually
let runtimeSpec: Runtime;
runtimeLoaded.subscribe(value => {
runtimeSpec = value;
});
class PyScriptApp {
config: AppConfig;
main() {
this.loadConfig();
this.initialize();
/* eslint-disable @typescript-eslint/no-unused-vars */
const xPyScript = customElements.define('py-script', PyScript);
const xPyLoader = customElements.define('py-loader', PyLoader);
const xPyEnv = customElements.define('py-env', PyEnv);
/* eslint-disable @typescript-eslint/no-unused-vars */
// add loader to the page body
logger.info('add py-loader');
const loader = <PyLoader>document.createElement('py-loader');
document.body.append(loader);
globalLoader.set(loader);
}
loadConfig() {
// find the <py-config> tag. If not found, we get null which means
// "use the default config"
// XXX: we should actively complain if there are multiple <py-config>
// and show a big error. PRs welcome :)
logger.info('searching for <py-config>');
const el = document.querySelector('py-config');
this.config = loadConfigFromElement(el);
logger.info('config loaded:\n' + JSON.stringify(this.config, null, 2));
}
initialize() {
addInitializer(this.loadPackages);
addInitializer(this.loadPaths);
this.loadRuntimes();
}
loadPackages = async () => {
logger.info("Packages to install: ", this.config.packages);
await runtimeSpec.installPackage(this.config.packages);
}
loadPaths = async () => {
const paths = this.config.paths;
logger.info("Paths to load: ", paths)
for (const singleFile of paths) {
logger.info(` loading path: ${singleFile}`);
try {
await runtimeSpec.loadFromFile(singleFile);
} catch (e) {
//Should we still export full error contents to console?
handleFetchError(<Error>e, singleFile);
}
}
logger.info("All paths loaded");
}
loadRuntimes() {
logger.info('Initializing runtimes');
for (const runtime of this.config.runtimes) {
const runtimeObj: Runtime = new PyodideRuntime(this.config, runtime.src,
runtime.name, runtime.lang);
const script = document.createElement('script'); // create a script DOM node
script.src = runtimeObj.src; // set its src to the provided URL
script.addEventListener('load', () => {
void runtimeObj.initialize();
});
document.head.appendChild(script);
}
}
// As first thing, loop for application configs
logger.info('checking for py-config');
const config: PyConfig = document.querySelector('py-config');
if (!config) {
const loader = document.createElement('py-config');
document.body.append(loader);
}
// add loader to the page body
logger.info('add py-loader');
const loader = <PyLoader>document.createElement('py-loader');
document.body.append(loader);
globalLoader.set(loader);
function pyscript_get_config() {
return globalApp.config;
}
globalExport('pyscript_get_config', pyscript_get_config);
// main entry point of execution
const globalApp = new PyScriptApp();
globalApp.main();