File size: 5,741 Bytes
bc20498 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
/**
* This file parses a generic <py-config> or config attribute
* to use as base config for all py-script elements, importing
* also a queue of plugins *before* the interpreter (if any) resolves.
*/
import { $$ } from "basic-devtools";
import TYPES from "./types.js";
import allPlugins from "./plugins.js";
import { robustFetch as fetch, getText } from "./fetch.js";
import { ErrorCode } from "./exceptions.js";
const { BAD_CONFIG, CONFLICTING_CODE } = ErrorCode;
const badURL = (url, expected = "") => {
let message = `(${BAD_CONFIG}): Invalid URL: ${url}`;
if (expected) message += `\nexpected ${expected} content`;
throw new Error(message);
};
/**
* Given a string, returns its trimmed content as text,
* fetching it from a file if the content is a URL.
* @param {string} config either JSON, TOML, or a file to fetch
* @param {string?} type the optional type to enforce
* @returns {{json: boolean, toml: boolean, text: string}}
*/
const configDetails = async (config, type) => {
let text = config?.trim();
// we only support an object as root config
let url = "",
toml = false,
json = /^{/.test(text) && /}$/.test(text);
// handle files by extension (relaxing urls parts after)
if (!json && /\.(\w+)(?:\?\S*)?$/.test(text)) {
const ext = RegExp.$1;
if (ext === "json" && type !== "toml") json = true;
else if (ext === "toml" && type !== "json") toml = true;
else badURL(text, type);
url = text;
text = (await fetch(url).then(getText)).trim();
}
return { json, toml: toml || (!json && !!text), text, url };
};
const conflictError = (reason) => new Error(`(${CONFLICTING_CODE}): ${reason}`);
const syntaxError = (type, url, { message }) => {
let str = `(${BAD_CONFIG}): Invalid ${type}`;
if (url) str += ` @ ${url}`;
return new SyntaxError(`${str}\n${message}`);
};
const configs = new Map();
for (const [TYPE] of TYPES) {
/** @type {Promise<[...any]>} A Promise wrapping any plugins which should be loaded. */
let plugins;
/** @type {any} The PyScript configuration parsed from the JSON or TOML object*. May be any of the return types of JSON.parse() or toml-j0.4's parse() ( {number | string | boolean | null | object | Array} ) */
let parsed;
/** @type {Error | undefined} The error thrown when parsing the PyScript config, if any.*/
let error;
/** @type {string | undefined} The `configURL` field to normalize all config operations as opposite of guessing it once resolved */
let configURL;
let config,
type,
pyElement,
pyConfigs = $$(`${TYPE}-config`),
attrConfigs = $$(
[
`script[type="${TYPE}"][config]:not([worker])`,
`${TYPE}-script[config]:not([worker])`,
].join(","),
);
// throw an error if there are multiple <py-config> or <mpy-config>
if (pyConfigs.length > 1) {
error = conflictError(`Too many ${TYPE}-config`);
} else {
// throw an error if there are <x-config> and config="x" attributes
if (pyConfigs.length && attrConfigs.length) {
error = conflictError(
`Ambiguous ${TYPE}-config VS config attribute`,
);
} else if (pyConfigs.length) {
[pyElement] = pyConfigs;
config = pyElement.getAttribute("src") || pyElement.textContent;
type = pyElement.getAttribute("type");
} else if (attrConfigs.length) {
[pyElement, ...attrConfigs] = attrConfigs;
config = pyElement.getAttribute("config");
// throw an error if dirrent scripts use different configs
if (
attrConfigs.some((el) => el.getAttribute("config") !== config)
) {
error = conflictError(
"Unable to use different configs on main",
);
}
}
}
// catch possible fetch errors
if (!error && config) {
try {
const { json, toml, text, url } = await configDetails(config, type);
if (url) configURL = new URL(url, location.href).href;
config = text;
if (json || type === "json") {
try {
parsed = JSON.parse(text);
} catch (e) {
error = syntaxError("JSON", url, e);
}
} else if (toml || type === "toml") {
try {
const { parse } = await import(
/* webpackIgnore: true */ "./3rd-party/toml.js"
);
parsed = parse(text);
} catch (e) {
error = syntaxError("TOML", url, e);
}
}
} catch (e) {
error = e;
}
}
// parse all plugins and optionally ignore only
// those flagged as "undesired" via `!` prefix
const toBeAwaited = [];
for (const [key, value] of Object.entries(allPlugins)) {
if (error) {
if (key === "error") {
// show on page the config is broken, meaning that
// it was not possible to disable error plugin neither
// as that part wasn't correctly parsed anyway
value().then(({ notify }) => notify(error.message));
}
} else if (!parsed?.plugins?.includes(`!${key}`)) {
toBeAwaited.push(value().then(({ default: p }) => p));
}
}
// assign plugins as Promise.all only if needed
plugins = Promise.all(toBeAwaited);
configs.set(TYPE, { config: parsed, configURL, plugins, error });
}
export default configs;
|