chocola 1.1.20 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) SadGabi.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,428 @@
1
+ # 🍫 Chocola JS
2
+
3
+ > A Sweet Taste of Reactive Web Development
4
+
5
+ Chocola is a lightweight, reactive component-based web framework that brings simplicity and modularity to modern web development. Build dynamic components with reactivity, global state management, and a developer experience as sweet as chocolate.
6
+
7
+ ## ✨ Features
8
+
9
+ - **🧩 Component-Based Architecture** - Build reusable, modular components with ease
10
+ - **⚡ Reactive Runtime** - Components automatically update when reactive variables change
11
+ - **🔄 Reactive Variables** - Mutable state with `&{sfx.var}` syntax and automatic re-rendering
12
+ - **🎯 Context System** - Pass data to components using intuitive `ctx.*` attributes
13
+ - **🌐 Global State Management** - Share state across components with `sfx` variables
14
+ - **📡 Variable Subscription** - Subscribe to state changes across your application
15
+ - **🎭 Conditional Rendering** - Show/hide components dynamically with `chif` attribute
16
+ - **🔌 Component Lifecycle API** - Public APIs for mounting, manipulating, and removing components
17
+ - **📦 Built-in Bundler** - Automatic compilation and optimization
18
+ - **🔥 Hot Reload Development** - See changes instantly with the dev server
19
+ - **🎨 Template Syntax** - Clean HTML templates with `${}` and `&{}` interpolation
20
+ - **⚙️ Zero Config** - Works out of the box with sensible defaults
21
+
22
+ ## 🚀 Quick Start
23
+
24
+ ### Installation
25
+
26
+ ```bash
27
+ npm install chocola
28
+ ```
29
+
30
+ ### Project Structure
31
+
32
+ ```
33
+ my-chocola-app/
34
+ ├── src/
35
+ │ ├── lib/
36
+ │ │ ├── Counter.js
37
+ │ │ ├── TodoItem.js
38
+ │ │ └── html/
39
+ │ │ ├── counter.body.html
40
+ │ │ └── todoItem.body.html
41
+ │ ├── styles/
42
+ │ │ └── mainStyle.css
43
+ │ └── index.html
44
+ ├── chocola.config.json
45
+ ├── chocola.server.js
46
+ └── index.js
47
+ ```
48
+
49
+ ### Configuration
50
+
51
+ Create a `chocola.config.json` file:
52
+
53
+ ```json
54
+ {
55
+ "bundle": {
56
+ "srcDir": "/src",
57
+ "outDir": "/dist",
58
+ "libDir": "/lib",
59
+ "emptyOutDir": true
60
+ },
61
+ "dev": {
62
+ "hostname": "localhost",
63
+ "port": 3000
64
+ }
65
+ }
66
+ ```
67
+
68
+ ## 📝 Creating Components
69
+
70
+ ### 1. Define Your HTML Template
71
+
72
+ > NOTE: Reactivity, component APIs and global variables are not implemented yet.
73
+ > Any of this features displayed here are for future references and may be modified.
74
+
75
+ Create `src/lib/html/counter.body.html`:
76
+
77
+ ```html
78
+ <div class="counter">
79
+ <h2>${ctx.title}</h2>
80
+ <!-- Use & instead of $ to set reactivity -->
81
+ <p>Count: &{sfx.count}</p>
82
+ <button class="increment">+</button>
83
+ <button class="decrement">-</button>
84
+ </div>
85
+ ```
86
+
87
+ ### 2. Create the Component Logic
88
+
89
+ Create `src/lib/Counter.js`:
90
+
91
+ ```javascript
92
+ import { lib } from "chocola";
93
+ import HTML from "./html/counter.body.html";
94
+
95
+ function RUNTIME(self, ctx) {
96
+ // Acces to component effects
97
+ let sfx = self.sfx;
98
+
99
+ // Event handlers
100
+ self.querySelector(".increment").addEventListener("click", () => {
101
+ sfx.incrementCount(); // Automatically updates the UI
102
+ });
103
+
104
+ self.querySelector(".decrement").addEventListener("click", () => {
105
+ sfx.decrementCount(); // Automatically updates the UI
106
+ });
107
+ }
108
+
109
+ function EFFECTS(self, sfx) {
110
+ // Component lifecycle effects
111
+ sfx.onMount: () => {
112
+ console.log("Counter mounted with count:", sfx.count);
113
+ },
114
+ sfx.onUpdate: () => {
115
+ // Log updated variables
116
+ console.log("Counter updated:", sfx.diff);
117
+ },
118
+ sfx.onRemove: () => {
119
+ console.log("Counter removed");
120
+ };
121
+
122
+ // Create the component public API
123
+ const api = lib.api(self, sfx);
124
+
125
+ // Create methods to expose
126
+ api.incrementCount = () => {
127
+ sfx.count++
128
+ }
129
+ api.decrementCount = () => {
130
+ sfx.count--
131
+ }
132
+
133
+ // Expose the component API
134
+ return api
135
+ }
136
+
137
+ export default function Counter() {
138
+ return {
139
+ body: HTML,
140
+ script: RUNTIME,
141
+ effects: EFFECTS
142
+ };
143
+ }
144
+ ```
145
+
146
+ ### 3. Use the Component
147
+
148
+ In your `src/index.html`:
149
+
150
+ ```html
151
+ <html>
152
+ <head>
153
+ <title>Chocola Counter App</title>
154
+ </head>
155
+ <body>
156
+ <app>
157
+ <!-- Set mutable sfx variables with & -->
158
+ <Counter ctx.title="My Counter" sfx.&initialCount="0"></Counter>
159
+ </app>
160
+ </body>
161
+ </html>
162
+ ```
163
+
164
+ ## 🎭 Component Anatomy
165
+
166
+ ### Template (`body`)
167
+ - Standard HTML with template variables
168
+ - **Static context**: `${ctx.propertyName}`, `${sfx.propertyName}` - rendered once at initialization
169
+ - **Reactive state**: `&{sfx.propertyName}` - automatically updates on change
170
+ - Clean separation of markup and logic
171
+
172
+ ### Runtime (`script`) - Optional
173
+ - Function that receives `self` (component API, properties and DOM element), `ctx` (static context), and `sfx` (dynamic context)
174
+ - Initialize runtime script and event handlers
175
+ - Full access to DOM APIs and browser features
176
+ - Executes when the component mounts
177
+
178
+ ### Effects (`effects`) - Optional
179
+ - Function that receives `self` and `sfx`
180
+ - Returns lifecycle hooks: `onMount`, `onUpdate`, `onRemove`
181
+ - Manage side effects and component lifecycle
182
+ - Clean up resources when component is removed
183
+
184
+ ## ⚡ Reactivity System
185
+
186
+ ### Mutable Reactive Variables
187
+
188
+ Use `&{sfx.varName}` in templates for automatic reactivity:
189
+
190
+ ```html
191
+ <div>
192
+ <p>Username: &{sfx.username}</p>
193
+ <p>Online: &{sfx.isOnline}</p>
194
+ </div>
195
+ ```
196
+
197
+ ```javascript
198
+ function RUNTIME(self, ctx) {
199
+ let sfx = self.sfx;
200
+
201
+ // Changes automatically update the UI
202
+ setTimeout(() => {
203
+ sfx.username = "Alice";
204
+ sfx.isOnline = true;
205
+ }, 2000);
206
+ }
207
+ ```
208
+
209
+ ### Variable Subscription
210
+
211
+ Subscribe to changes in reactive variables across components:
212
+
213
+ ```javascript
214
+ import { app } from "chocola";
215
+
216
+ function RUNTIME(self, ctx) {
217
+ let sfx = self.sfx;
218
+
219
+ // Subscribe to global state
220
+ app.watch("globalCounter", (newValue) => {
221
+ console.log("Global counter changed to:", newValue);
222
+ sfx.localCount = newValue * 2;
223
+ });
224
+ }
225
+ ```
226
+
227
+ ### Conditional Rendering
228
+
229
+ Use the `chif` attribute to conditionally render components:
230
+
231
+ ```html
232
+ <app>
233
+ <!-- Based on ctx variable (static) -->
234
+ <LoginForm chif="ctx.isLoggedOut"></LoginForm>
235
+
236
+ <!-- Based on sfx variable (reactive with &) -->
237
+ <Dashboard chif="sfx.&isAuthenticated"></Dashboard>
238
+ <LoadingSpinner chif="sfx.&isLoading"></LoadingSpinner>
239
+ </app>
240
+ ```
241
+
242
+ ```javascript
243
+ function RUNTIME(self, ctx) {
244
+ let sfx = self.sfx;
245
+
246
+ // Simulate authentication
247
+ setTimeout(() => {
248
+ sfx.isLoading = false;
249
+ sfx.isAuthenticated = true;
250
+ // Dashboard appears, LoadingSpinner disappears
251
+ }, 2000);
252
+ }
253
+ ```
254
+
255
+ ## 🌐 Global State Management
256
+
257
+ Share state across your entire application:
258
+
259
+ ```javascript
260
+ // In any component
261
+ import * as globals from "path/to/globals.js";
262
+
263
+ function RUNTIME(self, ctx, sfx) {
264
+ // Set global variables
265
+ globals.userTheme = "dark";
266
+ globals.notifications = [];
267
+
268
+ // Access global variables from other components
269
+ console.log(globals.userTheme);
270
+ }
271
+ ```
272
+
273
+ ## 🔌 Component Public API
274
+
275
+ ### Mounting Components Dynamically
276
+
277
+ ```javascript
278
+ // In any component
279
+ import { lib } from "chocola";
280
+ import Counter from "./lib/Counter.js";
281
+
282
+ // Mount a component programmatically in RUNTIME and/or EFFECTS
283
+ function RUNTIME(self. ctx) {
284
+ const counterInstance = lib.mount(Counter)
285
+ .defCtx({
286
+ title: "Dynamic Counter",
287
+ initialCount: "10"
288
+ })
289
+ .render() // Render accepts a DOM element or component
290
+ // instance as a target parameter; default
291
+ // target is set as your <app> element
292
+ }
293
+ ```
294
+
295
+ ### Manipulating Components
296
+
297
+ ```javascript
298
+ // Acces and modify variables
299
+ console.log(counterInstance.ctx.title);
300
+ counterInstance.sfx.count = 1;
301
+
302
+ // Call component API methods
303
+ counterInstance.reset();
304
+ console.log(counterInstance.getCount())
305
+ ```
306
+
307
+ ### Removing Components
308
+
309
+ ```javascript
310
+ // Remove and clean up
311
+ counterInstance.remove();
312
+ ```
313
+
314
+ ## 🔧 Development
315
+
316
+ ### Start Development Server
317
+
318
+ Create `chocola.server.js`:
319
+
320
+ ```javascript
321
+ import { dev } from "chocola";
322
+ import path from "path";
323
+ import { fileURLToPath } from "url";
324
+
325
+ const __filename = fileURLToPath(import.meta.url);
326
+ const __dirname = path.dirname(__filename);
327
+
328
+ dev.server(__dirname, "src", "dist");
329
+ ```
330
+
331
+ Run:
332
+ ```bash
333
+ node chocola.server.js
334
+ ```
335
+
336
+ Visit `http://localhost:3000` to see your app with hot reload enabled.
337
+
338
+ ### Build for Production
339
+
340
+ Create `index.js`:
341
+
342
+ ```javascript
343
+ import { app } from "chocola";
344
+ import path from "path";
345
+ import { fileURLToPath } from "url";
346
+
347
+ const __filename = fileURLToPath(import.meta.url);
348
+ const __dirname = path.dirname(__filename);
349
+
350
+ app.build(__dirname);
351
+ ```
352
+
353
+ Build:
354
+ ```bash
355
+ node index.js
356
+ ```
357
+
358
+ Your optimized app will be in the `dist/` directory.
359
+
360
+ ---
361
+
362
+ ## 🏗️ Build Output
363
+
364
+ Chocola automatically:
365
+ - ✅ Compiles components into optimized JavaScript
366
+ - ✅ Generates unique component IDs (`chid`)
367
+ - ✅ Bundles scripts with proper dependency resolution
368
+ - ✅ Processes templates and injects runtime code
369
+ - ✅ Sets up reactivity system for `&{sfx.*}` variables
370
+ - ✅ Optimizes conditional rendering with `chif` attributes
371
+ - ✅ Generates component API wrappers for mounting/unmounting
372
+ - ✅ Optimizes assets for production
373
+
374
+ ## 🎯 Best Practices
375
+
376
+ ### When to Use `ctx` vs `sfx`
377
+
378
+ - **Use `ctx`** for static configuration that won't change (titles, labels, initial values)
379
+ - **Use `sfx`** for dynamic data that updates over time and its configurations (counts, user input, API responses)
380
+
381
+ ### Component Lifecycle
382
+
383
+ ```javascript
384
+ function EFFECTS(self, sfx) {
385
+ return {
386
+ onMount: () => {
387
+ // Initialize, fetch data, set up subscriptions
388
+ },
389
+ onUpdate: () => {
390
+ // React to specific state changes
391
+ // Only runs when sfx variables change
392
+ },
393
+ onRemove: () => {
394
+ // Clean up timers, subscriptions, event listeners
395
+ }
396
+ };
397
+ }
398
+ ```
399
+
400
+ ### Conditional Rendering Performance
401
+
402
+ - Use `chif` with reactive variables for dynamic show/hide
403
+ - Use `chif` with static variables for static conditional rendering
404
+ - Components with `chif="false"` are not mounted at all (performance optimization)
405
+
406
+ ## 🤝 Contributing
407
+
408
+ Contributions are welcome! Chocola is in active development and we'd love your input.
409
+
410
+ ## 📄 License
411
+
412
+ MIT License - feel free to use Chocola in your projects!
413
+
414
+ ## 🍫 Why Chocola?
415
+
416
+ - **Simple**: No complex build configurations or CLI tools to learn
417
+ - **Reactive**: Built-in reactivity without the complexity of larger frameworks
418
+ - **Fast**: Minimal runtime overhead with efficient reactive updates
419
+ - **Flexible**: Use as much or as little as you need
420
+ - **Modern**: Built with ES modules and modern JavaScript features
421
+ - **Powerful**: Global state, subscriptions, and lifecycle hooks out of the box
422
+ - **Sweet**: Developer experience that's actually enjoyable
423
+
424
+ ---
425
+
426
+ Made with 🍫 and ❤️
427
+
428
+ **Start building sweet reactive web apps today!**
@@ -0,0 +1,95 @@
1
+ import { JSDOM } from "jsdom";
2
+ import { extractContextFromElement } from "./dom-processor.js";
3
+ import { genRandomId, incrementAlfabet } from "./utils.js";
4
+ import chalk from "chalk";
5
+
6
+ /**
7
+ * Processes a single component element and inserts it into the DOM
8
+ * @param {Element} element
9
+ * @param {Map} loadedComponents
10
+ * @param {Array} runtimeChunks
11
+ * @param {Array} compIdColl
12
+ * @param {object} letterState - { value: string }
13
+ * @returns {boolean} - true if component was processed, false if not found
14
+ */
15
+ export function processComponentElement(
16
+ element,
17
+ loadedComponents,
18
+ runtimeChunks,
19
+ compIdColl,
20
+ letterState
21
+ ) {
22
+ const tagName = element.tagName.toLowerCase();
23
+ const compName = tagName + ".js";
24
+ const ctx = extractContextFromElement(element);
25
+
26
+ const instance = loadedComponents.get(compName);
27
+ if (!instance || instance === undefined) return false;
28
+
29
+ if (instance && instance.body) {
30
+ let body = instance.body;
31
+ body = body.replace(/\$\{ctx\.(\w+)\}/g, (_, key) => ctx[key] || "");
32
+ const fragment = JSDOM.fragment(body);
33
+ const firstChild = fragment.firstChild;
34
+
35
+ if (firstChild && firstChild.nodeType === 1) {
36
+ if (instance.script || instance.effects) {
37
+ const compId = "chid-" + genRandomId(compIdColl);
38
+ firstChild.setAttribute("chid", compId);
39
+
40
+ let script = instance.script && instance.script.toString();
41
+ const letter = getNextLetter(letterState);
42
+
43
+ script = script.replace(/RUNTIME/g, `${letter}RUNTIME`);
44
+
45
+ runtimeChunks.push(`
46
+ const ${letter} = document.querySelector('[chid="${compId}"]');
47
+ ${script}
48
+ ${letter}RUNTIME(${letter}, ${JSON.stringify(ctx)});`);
49
+ }
50
+ }
51
+ element.replaceWith(fragment);
52
+ return true;
53
+ }
54
+
55
+ console.warn(chalk.yellow(`${compName} component could not be loaded`));
56
+ return false;
57
+ }
58
+
59
+ /**
60
+ * Processes all components in the app container
61
+ * @param {Element[]} appElements
62
+ * @param {Map} loadedComponents
63
+ * @returns {{
64
+ * runtimeScript: string,
65
+ * hasComponents: boolean
66
+ * }}
67
+ */
68
+ export function processAllComponents(appElements, loadedComponents) {
69
+ const runtimeChunks = [];
70
+ const compIdColl = [];
71
+ const letterState = { value: null };
72
+
73
+ appElements.forEach(el => {
74
+ processComponentElement(el, loadedComponents, runtimeChunks, compIdColl, letterState);
75
+ });
76
+
77
+ const runtimeScript = runtimeChunks.join("\n");
78
+ const hasComponents = runtimeChunks.length > 0;
79
+
80
+ return { runtimeScript, hasComponents };
81
+ }
82
+
83
+ /**
84
+ * Gets the next letter in sequence or starts with 'a'
85
+ * @param {object} letterState - { value: string }
86
+ * @returns {string}
87
+ */
88
+ function getNextLetter(letterState) {
89
+ if (!letterState.value) {
90
+ letterState.value = "a";
91
+ } else {
92
+ letterState.value = incrementAlfabet(letterState.value);
93
+ }
94
+ return letterState.value;
95
+ }
@@ -0,0 +1,66 @@
1
+ import path from "path";
2
+ import chalk from "chalk";
3
+ import { getChocolaConfig } from "../utils.js";
4
+
5
+ /**
6
+ * Loads and merges Chocola configuration with defaults
7
+ * @param {import("fs").PathLike} rootDir
8
+ * @returns {Promise<{
9
+ * srcDir: string,
10
+ * outDir: string,
11
+ * libDir: string,
12
+ * emptyOutDir: boolean
13
+ * }>}
14
+ */
15
+ export async function loadConfig(rootDir) {
16
+ const config = await getChocolaConfig(rootDir);
17
+ const bundleConfig = config.bundle || {};
18
+
19
+ const srcDir = bundleConfig.srcDir || "src";
20
+ const outDir = bundleConfig.outDir || "dist";
21
+ const libDir = bundleConfig.libDir || "lib";
22
+ const emptyOutDir = bundleConfig.emptyOutDir !== false;
23
+
24
+ logConfigWarnings(bundleConfig, emptyOutDir);
25
+
26
+ return { srcDir, outDir, libDir, emptyOutDir };
27
+ }
28
+
29
+ function logConfigWarnings(bundleConfig, emptyOutDir) {
30
+ if (!bundleConfig.srcDir) {
31
+ console.warn(
32
+ chalk.bold.yellow("WARNING!"),
33
+ 'srcDir not defined in chocola.config.json file: using default "src" directory.'
34
+ );
35
+ }
36
+
37
+ if (!bundleConfig.outDir) {
38
+ console.warn(
39
+ chalk.bold.yellow("WARNING!"),
40
+ 'outDir not defined in chocola.config.json file: using default "dist" directory.'
41
+ );
42
+ }
43
+
44
+ if (!bundleConfig.libDir) {
45
+ console.warn(
46
+ chalk.bold.yellow("WARNING!"),
47
+ 'libDir not defined in chocola.config.json file: using default "lib" directory.'
48
+ );
49
+ }
50
+
51
+ console.log(`> using emptyOutDir = ${emptyOutDir}`);
52
+ }
53
+
54
+ /**
55
+ * Resolves all path directories based on configuration
56
+ * @param {import("fs").PathLike} rootDir
57
+ * @param {object} config
58
+ * @returns {object}
59
+ */
60
+ export function resolvePaths(rootDir, config) {
61
+ return {
62
+ outDir: path.join(rootDir, config.outDir),
63
+ src: path.join(rootDir, config.srcDir),
64
+ components: path.join(rootDir, config.srcDir, config.libDir),
65
+ };
66
+ }
@@ -0,0 +1,96 @@
1
+ import { JSDOM } from "jsdom";
2
+ import { promises as fs } from "fs";
3
+ import path from "path";
4
+ import { throwError } from "./utils.js";
5
+ import { readMyFile } from "./fs.js";
6
+
7
+ /**
8
+ * Creates a JSDOM instance from source index file
9
+ * @param {string} srcIndexContent
10
+ * @returns {JSDOM}
11
+ */
12
+ export function createDOM(srcIndexContent) {
13
+ return new JSDOM(srcIndexContent);
14
+ }
15
+
16
+ /**
17
+ * Validates that the index file has an <app> root element
18
+ * @param {Document} doc
19
+ * @throws {Error} if <app> element not found
20
+ */
21
+ export function validateAppContainer(doc) {
22
+ const appContainer = doc.querySelector("app");
23
+ if (!appContainer) {
24
+ throwError("Index page must have an <app> element");
25
+ }
26
+ return appContainer;
27
+ }
28
+
29
+ /**
30
+ * Extracts all child elements from app container
31
+ * @param {Element} appContainer
32
+ * @returns {Element[]}
33
+ */
34
+ export function getAppElements(appContainer) {
35
+ return Array.from(appContainer.querySelectorAll("*"));
36
+ }
37
+
38
+ /**
39
+ * Extracts context attributes from element (ctx.* attributes)
40
+ * @param {Element} element
41
+ * @returns {object}
42
+ */
43
+ export function extractContextFromElement(element) {
44
+ const ctx = {};
45
+ for (const attr of element.attributes) {
46
+ if (attr.name.startsWith("ctx.")) {
47
+ const key = attr.name.slice(4);
48
+ ctx[key] = attr.value;
49
+ }
50
+ }
51
+ return ctx;
52
+ }
53
+
54
+ /**
55
+ * Serializes and formats DOM to pretty HTML
56
+ * @param {JSDOM} dom
57
+ * @returns {string}
58
+ */
59
+ export async function serializeDOM(dom) {
60
+ const beautify = (await import("js-beautify")).default;
61
+ const finalHtml = dom.serialize();
62
+ return beautify.html(finalHtml, { indent_size: 2 });
63
+ }
64
+
65
+ /**
66
+ * Writes the final HTML to output directory
67
+ * @param {string} html
68
+ * @param {import("fs").PathLike} outDirPath
69
+ */
70
+ export async function writeHTMLOutput(html, outDirPath) {
71
+ await fs.writeFile(path.join(outDirPath, "index.html"), html);
72
+ }
73
+
74
+ /**
75
+ * Gets all stylesheet and icon links from document
76
+ * @param {Document} doc
77
+ * @returns {{stylesheets: HTMLLinkElement[], icons: HTMLLinkElement[]}}
78
+ */
79
+ export function getAssetLinks(doc) {
80
+ const docLinks = Array.from(doc.querySelectorAll("link"));
81
+ const stylesheets = docLinks.filter(link => link.rel === "stylesheet");
82
+ const icons = docLinks.filter(link => link.rel === "icon");
83
+ return { stylesheets, icons };
84
+ }
85
+
86
+ /**
87
+ * Appends a script element to document body
88
+ * @param {Document} doc
89
+ * @param {string} filename
90
+ */
91
+ export function appendRuntimeScript(doc, filename) {
92
+ const runtimeScriptEl = doc.createElement("script");
93
+ runtimeScriptEl.type = "module";
94
+ runtimeScriptEl.src = "./" + filename;
95
+ doc.body.appendChild(runtimeScriptEl);
96
+ }