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,27 +1,3 @@
import toml from '../src/toml'
import type { AppConfig } from "./runtime";
const allKeys = {
"string": ["name", "description", "version", "type", "author_name", "author_email", "license"],
"number": ["schema_version"],
"boolean": ["autoclose_loader"],
"array": ["runtimes", "packages", "paths", "plugins"]
};
const defaultConfig: AppConfig = {
"schema_version": 1,
"type": "app",
"autoclose_loader": true,
"runtimes": [{
"src": "https://cdn.jsdelivr.net/pyodide/v0.21.2/full/pyodide.js",
"name": "pyodide-0.21.2",
"lang": "python"
}],
"packages":[],
"paths":[],
"plugins": []
}
function addClasses(element: HTMLElement, classes: Array<string>) {
for (const entry of classes) {
element.classList.add(entry);
@@ -121,139 +97,15 @@ function inJest(): boolean {
return typeof process === 'object' && process.env.JEST_WORKER_ID !== undefined;
}
function fillUserData(inputConfig: AppConfig, resultConfig: AppConfig): AppConfig
{
for (const key in inputConfig)
{
// fill in all extra keys ignored by the validator
if (!(key in defaultConfig))
{
resultConfig[key] = inputConfig[key];
}
}
return resultConfig;
function globalExport(name: string, obj: any) {
// attach the given object to the global object, so that it is globally
// visible everywhere. Should be used very sparingly!
// `window` in the browser, `global` in node
const _global = (window || global) as any;
_global[name] = obj;
}
function mergeConfig(inlineConfig: AppConfig, externalConfig: AppConfig): AppConfig {
if (Object.keys(inlineConfig).length === 0 && Object.keys(externalConfig).length === 0)
{
return defaultConfig;
}
else if (Object.keys(inlineConfig).length === 0)
{
return externalConfig;
}
else if(Object.keys(externalConfig).length === 0)
{
return inlineConfig;
}
else
{
let merged: AppConfig = {};
for (const keyType in allKeys)
{
const keys = allKeys[keyType];
keys.forEach(function(item: string){
if (keyType === "boolean")
{
merged[item] = (typeof inlineConfig[item] !== "undefined") ? inlineConfig[item] : externalConfig[item];
}
else
{
merged[item] = inlineConfig[item] || externalConfig[item];
}
});
}
// fill extra keys from external first
// they will be overridden by inline if extra keys also clash
merged = fillUserData(externalConfig, merged);
merged = fillUserData(inlineConfig, merged);
return merged;
}
}
function parseConfig(configText: string, configType = "toml") {
let config: object;
if (configType === "toml") {
try {
// TOML parser is soft and can parse even JSON strings, this additional check prevents it.
if (configText.trim()[0] === "{")
{
const errMessage = `config supplied: ${configText} is an invalid TOML and cannot be parsed`;
showError(`<p>${errMessage}</p>`);
throw Error(errMessage);
}
config = toml.parse(configText);
}
catch (err) {
const errMessage: string = err.toString();
showError(`<p>config supplied: ${configText} is an invalid TOML and cannot be parsed: ${errMessage}</p>`);
throw err;
}
}
else if (configType === "json") {
try {
config = JSON.parse(configText);
}
catch (err) {
const errMessage: string = err.toString();
showError(`<p>config supplied: ${configText} is an invalid JSON and cannot be parsed: ${errMessage}</p>`);
throw err;
}
}
else {
showError(`<p>type of config supplied is: ${configType}, supported values are ["toml", "json"].</p>`);
}
return config;
}
function validateConfig(configText: string, configType = "toml") {
const config = parseConfig(configText, configType);
const finalConfig: AppConfig = {}
for (const keyType in allKeys)
{
const keys = allKeys[keyType];
keys.forEach(function(item: string){
if (validateParamInConfig(item, keyType, config))
{
if (item === "runtimes")
{
finalConfig[item] = [];
const runtimes = config[item];
runtimes.forEach(function(eachRuntime: object){
const runtimeConfig: object = {};
for (const eachRuntimeParam in eachRuntime)
{
if (validateParamInConfig(eachRuntimeParam, "string", eachRuntime))
{
runtimeConfig[eachRuntimeParam] = eachRuntime[eachRuntimeParam];
}
}
finalConfig[item].push(runtimeConfig);
});
}
else
{
finalConfig[item] = config[item];
}
}
});
}
return fillUserData(config, finalConfig);
}
function validateParamInConfig(paramName: string, paramType: string, config: object): boolean {
if (paramName in config)
{
return paramType === "array" ? Array.isArray(config[paramName]) : typeof config[paramName] === paramType;
}
return false;
}
export { defaultConfig, addClasses, removeClasses, getLastPath, ltrim, htmlDecode, guidGenerator, showError, handleFetchError, readTextFromPath, inJest, mergeConfig, validateConfig };
export { addClasses, removeClasses, getLastPath, ltrim, htmlDecode, guidGenerator, showError, handleFetchError, readTextFromPath, inJest, globalExport, };