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;