@quietsapa/qsl 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,74 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-09-20
10
+
11
+ First public release. The runtime is extracted from a private codebase, so
12
+ this entry records the differences from that internal version rather than a
13
+ list of new features.
14
+
15
+ ### Fixed
16
+
17
+ - **`media:` trigger never worked.** The handler referenced an undeclared
18
+ variable instead of the query string. Because `typeof` on an undeclared
19
+ identifier is safe, it silently took the "skip" branch every time rather than
20
+ throwing. The query is now read from the option and passed to `matchMedia`.
21
+ - **`visible:` and `appears:` triggers hung the run.** Both listened for DOM
22
+ events named `intersection` and `mutation`, which do not exist. When the
23
+ target element was present, the callback never fired, the flow stayed paused
24
+ and the load never completed. They now use `IntersectionObserver` and
25
+ `MutationObserver`. `appears:` also waits for elements that are inserted
26
+ later, instead of firing immediately when the selector does not match yet.
27
+ - **Selectors containing a colon were truncated.** `hover:`, `visible:` and
28
+ `appears:` parsed their argument with `split(':')[1]`, which broke
29
+ `hover:.btn:first-child` and any media query. Arguments are now read by
30
+ prefix length.
31
+ - **`domready` trigger could hang.** It only fired immediately when
32
+ `readyState` was `complete`; at `interactive`, `DOMContentLoaded` had already
33
+ been dispatched, so the listener it added never ran.
34
+ - **`shadow` type threw when its container was missing.** A local
35
+ `const error = new Error(...)` shadowed the module-level `error()` helper, so
36
+ the error path raised a `TypeError` instead of reporting the failure. A
37
+ missing or absent `container` now resolves with an `Error`.
38
+ - **`fire('SKIPPED', ...)` was a silent no-op.** There was no `SKIPPED` entry in
39
+ the event map. Added as `QSL:skipped`.
40
+ - **`registerTypes()` rejected the object form** documented in its JSDoc, and
41
+ both `registerType` and `registerTypes` returned `undefined` on invalid input,
42
+ breaking the chaining contract. The same applied to `runFlow`, `pauseGroup`
43
+ and `runGroup`.
44
+
45
+ ### Changed
46
+
47
+ - **`reset()` no longer clears plugin registrations.** It previously wiped
48
+ registered types, condition handlers, trigger handlers and every lifecycle
49
+ hook along with the run state. Since `reset()` runs automatically once all
50
+ flows complete, the runtime was effectively dead for any process added
51
+ afterwards. `reset()` now clears run state only; the new `destroy()` performs
52
+ the full teardown.
53
+ - **`reset()` no longer skips its work when a logger is set.** Debugging
54
+ behaviour is controlled by the new `autoReset` property instead.
55
+ - Identifiers interpolated into the `inline-script` module wrapper are
56
+ serialised with `JSON.stringify`, so a hostile `flowId` or process id cannot
57
+ break out of its string literal.
58
+
59
+ ### Added
60
+
61
+ - `destroy()` for a full teardown, and `autoReset` to keep flow state after a
62
+ run.
63
+ - Vitest test suite covering the fixes above.
64
+ - Three published builds: ESM library, full browser bundle, slim browser
65
+ bundle.
66
+
67
+ ### Removed
68
+
69
+ - The service-specific build pipeline: manifest generation, CDN deployment and
70
+ the internal bundle-composition map. Those stay in the private repository;
71
+ this one ships only the runtime.
72
+
73
+ [Unreleased]: https://github.com/Quietsapa/qsl/compare/v0.1.0...HEAD
74
+ [0.1.0]: https://github.com/Quietsapa/qsl/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,73 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
10
+
11
+ "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
12
+
13
+ "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
14
+
15
+ "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
16
+
17
+ "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
18
+
19
+ "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
20
+
21
+ "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
22
+
23
+ "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
24
+
25
+ "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
26
+
27
+ "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
28
+
29
+ 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
30
+
31
+ 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
32
+
33
+ 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
34
+
35
+ (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
36
+
37
+ (b) You must cause any modified files to carry prominent notices stating that You changed the files; and
38
+
39
+ (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
40
+
41
+ (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
42
+
43
+ You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
44
+
45
+ 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
46
+
47
+ 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
48
+
49
+ 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
50
+
51
+ 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
52
+
53
+ 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
54
+
55
+ END OF TERMS AND CONDITIONS
56
+
57
+ APPENDIX: How to apply the Apache License to your work.
58
+
59
+ To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
60
+
61
+ Copyright [yyyy] [name of copyright owner]
62
+
63
+ Licensed under the Apache License, Version 2.0 (the "License");
64
+ you may not use this file except in compliance with the License.
65
+ You may obtain a copy of the License at
66
+
67
+ http://www.apache.org/licenses/LICENSE-2.0
68
+
69
+ Unless required by applicable law or agreed to in writing, software
70
+ distributed under the License is distributed on an "AS IS" BASIS,
71
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
72
+ See the License for the specific language governing permissions and
73
+ limitations under the License.
package/NOTICE ADDED
@@ -0,0 +1,20 @@
1
+ QSL (@quietsapa/qsl)
2
+ Copyright 2026 Quietsapa
3
+
4
+ This product includes software developed as part of the Quietsapa project.
5
+
6
+ "Quietsapa" and "QSL" are names used by the Quietsapa project. This license
7
+ does not grant permission to use them, except as required for describing the
8
+ origin of the work (see section 6 of the License).
9
+
10
+ Licensed under the Apache License, Version 2.0 (the "License");
11
+ you may not use this file except in compliance with the License.
12
+ You may obtain a copy of the License at
13
+
14
+ http://www.apache.org/licenses/LICENSE-2.0
15
+
16
+ Unless required by applicable law or agreed to in writing, software
17
+ distributed under the License is distributed on an "AS IS" BASIS,
18
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
+ See the License for the specific language governing permissions and
20
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,317 @@
1
+ # QSL
2
+
3
+ [![CI](https://github.com/Quietsapa/qsl/actions/workflows/ci.yml/badge.svg)](https://github.com/Quietsapa/qsl/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/@quietsapa/qsl.svg)](https://www.npmjs.com/package/@quietsapa/qsl)
5
+ [![license](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
6
+
7
+ A dependency-aware orchestration runtime for everything a page loads besides its
8
+ own code: analytics tags, chat widgets, A/B testing snippets, tracking pixels,
9
+ stylesheets and custom elements.
10
+
11
+ Instead of scattering `<script>` tags through a template and hoping the order
12
+ works out, you describe what should load, when it should load, and what it
13
+ depends on. QSL resolves the graph and runs it.
14
+
15
+ ```js
16
+ core
17
+ .add({ id: 'analytics', type: 'script', src: '/a.js' }, 'metrics')
18
+ .add({ id: 'heatmap', type: 'script', src: '/h.js', depends: ['analytics'], trigger: 'idle' }, 'metrics')
19
+ .add({ id: 'chat', type: 'script', src: '/c.js', trigger: 'visible:#footer' }, 'widgets');
20
+
21
+ core.setFlowOptions({ ordered: true }, 'metrics');
22
+
23
+ await core.load();
24
+ ```
25
+
26
+ It is deliberately small, has no runtime dependencies, and does not talk to any
27
+ server. Configuration comes from wherever you want: a static object, your CMS,
28
+ or an API.
29
+
30
+ ## Status
31
+
32
+ Pre-1.0. The API described here is what ships today, but it may still change
33
+ between minor versions. Pin an exact version if that matters to you.
34
+
35
+ ## Installation
36
+
37
+ ```sh
38
+ npm install @quietsapa/qsl
39
+ ```
40
+
41
+ Or load a prebuilt bundle straight from a CDN:
42
+
43
+ ```html
44
+ <!-- everything: all types, conditions, triggers, logging -->
45
+ <script src="https://cdn.jsdelivr.net/npm/@quietsapa/qsl/dist/qsl.min.js"></script>
46
+
47
+ <!-- just ordered script loading -->
48
+ <script src="https://cdn.jsdelivr.net/npm/@quietsapa/qsl/dist/qsl.slim.min.js"></script>
49
+ ```
50
+
51
+ Both browser bundles register their types and call `init()` for you, then
52
+ expose the instance as `window.__QSL__`.
53
+
54
+ | Build | Entry | Size (gzip) | Contents |
55
+ | --- | --- | --- | --- |
56
+ | `dist/qsl.mjs` | `src/index.js` | — | ESM, nothing registered, nothing started |
57
+ | `dist/qsl.min.js` | `src/presets/full.js` | ~8.3 kB | All types, conditions, triggers, logger, events |
58
+ | `dist/qsl.slim.min.js` | `src/presets/default.js` | ~4.4 kB | The `script` type only |
59
+
60
+ ## Quick start
61
+
62
+ The ESM entry is side-effect free: importing it registers nothing. You compose
63
+ what you need and call `init()` yourself.
64
+
65
+ ```js
66
+ import core, { Script, Stylesheet, triggers, conditions } from '@quietsapa/qsl';
67
+
68
+ await core
69
+ .registerTypes([Script, Stylesheet])
70
+ .use(triggers)
71
+ .use(conditions)
72
+ .init();
73
+
74
+ core.add({ id: 'gtm', type: 'script', src: 'https://example.com/gtm.js' });
75
+ core.add({ id: 'widget', type: 'script', src: '/widget.js', trigger: 'interaction' });
76
+
77
+ await core.load();
78
+ ```
79
+
80
+ With a CDN bundle the registration is already done:
81
+
82
+ ```html
83
+ <script src="https://cdn.jsdelivr.net/npm/@quietsapa/qsl/dist/qsl.min.js"></script>
84
+ <script>
85
+ var qsl = window.__QSL__;
86
+ qsl.add({ id: 'gtm', type: 'script', src: 'https://example.com/gtm.js' });
87
+ qsl.load();
88
+ </script>
89
+ ```
90
+
91
+ ## Concepts
92
+
93
+ **Process** — one thing to load: a script, a stylesheet, a pixel, an element.
94
+ Described by a plain object whose `type` selects the handler.
95
+
96
+ **Flow** — a named group of processes. Flows run in parallel with each other.
97
+ Inside a flow, processes run in parallel too, unless the flow is `ordered`, in
98
+ which case each one waits for the previous to finish.
99
+
100
+ Two flow names are special: `default` is where processes go when you pass no
101
+ flow, and `ordered` is a ready-made sequential flow. `core.add(config, true)`
102
+ is shorthand for the latter.
103
+
104
+ **Trigger** — when a flow or process is allowed to start. Until the trigger
105
+ fires, it waits.
106
+
107
+ **Condition** — whether it should run at all. A failing condition skips it.
108
+
109
+ **Dependency** — `depends` makes a process or flow wait for others to finish.
110
+ Circular dependencies are detected by the `circ` plugin and broken rather than
111
+ deadlocking; a dependency that does not exist is logged and ignored.
112
+
113
+ ## API
114
+
115
+ ### `init()`
116
+
117
+ Registers internal listeners and runs `initActions` from plugins. Returns a
118
+ promise resolving to the instance. Safe to call more than once.
119
+
120
+ If the script element that loaded QSL has `?async=true` in its URL, `init()`
121
+ also calls `window.QSLReady()` when it finishes, or a different global named by
122
+ the `callback` query parameter.
123
+
124
+ ### `add(config, flowId = null)`
125
+
126
+ Adds a process. `flowId` may be a string, `true` (the `ordered` flow), or
127
+ omitted (the `default` flow). Returns the instance.
128
+
129
+ `config.id` is optional; a random one is generated. Note that ids are stored
130
+ with a `qsl-` prefix internally, but `depends` is written with the unprefixed id
131
+ you passed.
132
+
133
+ ### `load(options = {})`
134
+
135
+ Starts everything. Returns a promise that resolves once every flow has
136
+ completed. `options.between` sets a default delay in milliseconds between
137
+ consecutive processes.
138
+
139
+ Calling `load()` a second time while a run is in progress does nothing.
140
+
141
+ ### `setFlowOptions(options, flowId = null)`
142
+
143
+ Sets options on a flow, creating it if needed. Merges with previous options.
144
+
145
+ | Option | Default | Meaning |
146
+ | --- | --- | --- |
147
+ | `ordered` | `false` | Run processes sequentially |
148
+ | `delay` | `0` | Milliseconds to wait before the flow starts |
149
+ | `between` | `null` | Milliseconds between consecutive processes |
150
+ | `priority` | `0` | Higher runs earlier; flows with a trigger always go last |
151
+ | `trigger` | `null` | See [Triggers](#triggers) |
152
+ | `condition` | `null` | See [Conditions](#conditions) |
153
+ | `depends` | `[]` | Flow ids to wait for. Setting this pauses the flow |
154
+ | `group` | `null` | Group name for `pauseGroup` / `runGroup` |
155
+ | `paused` | `false` | Hold the flow until `runFlow()` |
156
+ | `preload` | `false` | Emit `<link rel=preload>` for scripts and styles |
157
+ | `fireEvents` | `true` | Let the `events` plugin re-dispatch lifecycle events |
158
+
159
+ ### Other methods
160
+
161
+ - `use(plugin, ...args)` — run a plugin function against the instance.
162
+ - `registerType(type, handler)` / `registerTypes(list)` — add resource types.
163
+ `registerTypes` accepts `[{ type, handler }]` or `{ type: handler }`.
164
+ - `runFlow(flowId, withTrigger = false)` — start a paused flow.
165
+ - `pauseGroup(name)` / `runGroup(name)` — act on every flow in a group.
166
+ - `setLogger(logger)` — supply an object with `log()` and `error()`.
167
+ - `setOnAllComplete(fn)` — callback for the end of a run.
168
+ - `useEvents()` — enable DOM lifecycle events.
169
+ - `reset()` — clear run state. Registered types and plugin handlers survive, so
170
+ a later `add()` and `load()` behave the same as the first run.
171
+ - `destroy()` — clear everything, including types and plugins. `init()` must be
172
+ called again afterwards.
173
+ - `autoReset` — set to `false` to keep flow state after a run, for debugging.
174
+
175
+ ## Types
176
+
177
+ Every type is a `{ type, handler }` pair. Handlers receive the process config
178
+ and return a promise.
179
+
180
+ | Type | Key fields |
181
+ | --- | --- |
182
+ | `script` | `src`, `module`, `async`, `defer`, `crossOrigin`, `integrity`, `bypassCache` |
183
+ | `inline-script` | `code`, `module` |
184
+ | `stylesheet` | `href`, `crossOrigin`, `bypassCache` |
185
+ | `style` | `code` |
186
+ | `pixel` | `src`, `dom`, `style` |
187
+ | `html` | `tag`, `html`, `id`, `className`, `style` |
188
+ | `shadow` | `tag`, `shadowData: { container, position, hidden }` |
189
+ | `console` | `message` — built in, used as the default type |
190
+
191
+ Common fields across types: `id`, `type`, `depends`, `condition`, `trigger`,
192
+ `priority`, `delay`, `data` (rendered as `data-*` attributes), `footer` (append
193
+ to `<body>` instead of `<head>`), and the callbacks `onBeforeStart`,
194
+ `onComplete`, `onError`.
195
+
196
+ Writing your own is a function returning a promise:
197
+
198
+ ```js
199
+ core.registerType('json', (process) =>
200
+ fetch(process.url)
201
+ .then((r) => r.json())
202
+ .then(process.onComplete)
203
+ );
204
+ ```
205
+
206
+ ## Triggers
207
+
208
+ Set `trigger` on a process or a flow.
209
+
210
+ | Trigger | Fires when |
211
+ | --- | --- |
212
+ | `'load'` | The window `load` event |
213
+ | `'domready'` | `DOMContentLoaded`, or immediately if it already happened |
214
+ | `'idle'` | `requestIdleCallback`, falling back to a 200 ms timeout |
215
+ | `'interaction'` or `true` | First click, keydown, wheel, mousedown, mousemove or touchstart |
216
+ | `'delay:2000'` | After the given number of milliseconds |
217
+ | `'hover:<selector>'` | Pointer enters the element |
218
+ | `'visible:<selector>'` | Element intersects the viewport (`IntersectionObserver`) |
219
+ | `'appears:<selector>'` | Element is inserted into the DOM (`MutationObserver`) |
220
+ | `'media:<query>'` | Media query matches, now or later |
221
+ | a function | You call the callback it receives |
222
+
223
+ Selectors and queries may contain colons; they are parsed by prefix length, not
224
+ by splitting.
225
+
226
+ Combine them with an array (all must fire) or an operator object:
227
+
228
+ ```js
229
+ { trigger: ['domready', 'delay:1000'] }
230
+ { trigger: { operator: 'or', triggers: ['idle', 'interaction'] } }
231
+ ```
232
+
233
+ ## Conditions
234
+
235
+ Set `condition` on a process or a flow. A failing condition skips it.
236
+
237
+ | Condition | Example |
238
+ | --- | --- |
239
+ | `media:<query>` | `media:(min-width: 768px)` |
240
+ | `lang:<op>:<value>` | `lang:startsWith:ru`, `lang:in:en-US,en-GB` |
241
+ | `tz:<op>:<value>` | `tz:contains:Europe`, `tz:offset:3` |
242
+ | `url:<op>:<value>` | `url:pathStartsWith:/blog`, `url:query:utm_source=ads` |
243
+ | `ua:<op>:<value>` | `ua:device:mobile`, `ua:browser:safari`, `ua:os:ios` |
244
+ | a function | Return `true` to run, `false` to skip |
245
+ | a boolean | `false` skips |
246
+
247
+ Operators: `equals`/`is`, `contains`, `startsWith`, `in` for language;
248
+ `equals`/`is`, `contains`, `offset` for timezone; `contains`, `path`,
249
+ `pathStartsWith`, `pathEndsWith`, `query`, `hostname`, `matches`, `pathMatches`
250
+ for URL; `contains`, `equals`/`is`, `matches`, `browser`, `device`,
251
+ `os`/`platform` for user agent.
252
+
253
+ Combine with an array (all must pass) or an operator object:
254
+
255
+ ```js
256
+ { condition: ['ua:device:desktop', 'url:pathStartsWith:/app'] }
257
+ { condition: { operator: 'or', conditions: ['lang:is:ru-RU', 'tz:contains:Europe'] } }
258
+ ```
259
+
260
+ An unparseable regular expression fails the condition rather than throwing.
261
+
262
+ ## Plugins
263
+
264
+ A plugin is a function that receives the instance and registers handlers on it.
265
+
266
+ | Plugin | What it adds |
267
+ | --- | --- |
268
+ | `conditions` | The five condition handlers above |
269
+ | `triggers` | The eight trigger handlers above |
270
+ | `logger` | A console logger with readable message names |
271
+ | `events` | Re-dispatches `DOMContentLoaded` and `load` per process, so late-loaded third-party scripts that listen for them still initialise |
272
+ | `circ` | Detects circular dependencies and breaks them |
273
+ | `dynamic` | Defers processes added after `load()` into their own flows |
274
+ | `simple-events` | Re-dispatches `DOMContentLoaded` and `load` globally once everything completes |
275
+
276
+ ```js
277
+ import core, { triggers, conditions, logger } from '@quietsapa/qsl';
278
+
279
+ core.use(triggers).use(conditions).use(logger);
280
+ ```
281
+
282
+ ## Events
283
+
284
+ Call `useEvents()` to enable them. Each carries the process config as `detail`.
285
+
286
+ `QSL:started`, `QSL:completed`, `QSL:error`, `QSL:skipped`,
287
+ `QSL:all:completed`.
288
+
289
+ ## Security
290
+
291
+ **QSL treats its configuration as trusted, privileged input.** The
292
+ `inline-script` type executes code, the `html` type writes to `innerHTML`, and
293
+ several conditions compile regular expressions from strings. Never build a
294
+ configuration from untrusted input.
295
+
296
+ Read [SECURITY.md](SECURITY.md) before deploying. It covers the trust model,
297
+ Content Security Policy, and how to report a vulnerability.
298
+
299
+ ## Development
300
+
301
+ ```sh
302
+ npm install
303
+ npm test # vitest + happy-dom
304
+ npm run build # three bundles into dist/
305
+ ```
306
+
307
+ `npm run check:version` guards against `VERSION` in `src/core.js` drifting away
308
+ from `package.json`.
309
+
310
+ ## Contributing
311
+
312
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Bug reports with a reproduction are the
313
+ most useful thing you can send.
314
+
315
+ ## License
316
+
317
+ Apache License 2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE).
@@ -0,0 +1,2 @@
1
+ !function(){"use strict";const e={VERSION:"0.1.0",PREFIX:"qsl-",FLOW_TYPE:{DEFAULT:"default",ORDERED:"ordered"},FLOW_STATE:{READY:"READY",RUNNING:"RUNNING",COMPLETED:"COMPLETED"},FLOW_OPTIONS:{delay:0,priority:0,between:null,trigger:null,condition:null,beforeStart:null,onComplete:null,group:null,paused:!1,preload:!1,fireEvents:!0,depends:[]},EVENTS:{STARTED:"QSL:started",COMPLETED:"QSL:completed",ERROR:"QSL:error",FLOW_STARTED:"QSL:flow:started",FLOW_COMPLETED:"QSL:flow:completed",ALL_COMPLETED:"QSL:all:completed",SKIPPED:"QSL:skipped",DOMREADY:"QSL:domready",LOADED:"QSL:loaded"},LIFECYCLE:{DOMREADY:"interactive"===document.readyState||"complete"===document.readyState,LOADED:!1},CALLBACK:"QSLReady",types:new Map,flows:new Map,flowOptions:new Map,flowGroups:new Map,currentProcessPerFlow:new Map,pendingFlows:new Set,pendingProcesses:new Set,completedProcesses:new Set,loadActions:new Set,initActions:new Set,addProcessFilters:new Set,completedFlowsActions:new Set,flowIdFilters:new Set,conditionHandlers:new Set,triggerHandlers:new Set,processCompleteActions:new Set,allCompleteActions:new Set,resetActions:new Set,handlerCallbacksFilters:new Set,onAllComplete:null,globalResolve:null,logger:null,hasStarted:!1,eventsEnabled:!1,initialized:!1,completing:!1,autoReset:!0,globalBetween:0,async init(){if(this.initialized)return this;if(window.__QSL__=window.__QSL__||this,"interactive"===document.readyState||"complete"===document.readyState?this.LIFECYCLE.DOMREADY=!0:document.addEventListener("DOMContentLoaded",()=>this.LIFECYCLE.DOMREADY=!0,{once:!0}),"complete"===document.readyState?this.LIFECYCLE.LOADED=!0:window.addEventListener("load",()=>this.LIFECYCLE.LOADED=!0,{once:!0}),this.registerType("console",e=>new Promise(t=>{var s;null==(s=e.onBeforeStart)||s.call(e),setTimeout(()=>{var s;e.message&&this.log(e.message,{timestamp:Date.now()}),null==(s=e.onComplete)||s.call(e),t()},e.delay||0)})),window.addEventListener("QSL:log",e=>this.log(e.detail.type,e.detail.config.id)),window.addEventListener("QSL:error",e=>this.error(e.detail.type,e.detail.error,e.detail.config.id)),this.initActions.size)for(const s of this.initActions)"function"==typeof s&&await s.call(this);this.initialized=!0;const e=document.currentScript?new URL(document.currentScript.src,document.baseURI):null;if(!e||"true"!==e.searchParams.get("async"))return this;const t=e.searchParams.get("callback")||this.CALLBACK;return"function"==typeof window[t]&&window[t](),this},use(e,...t){return"function"==typeof e&&e(this,...t),this},add(e,t=null){if(!e||"object"!=typeof e)return this;if(e.id=e.id?this.PREFIX+e.id:this.PREFIX+Math.random().toString(36).slice(2),e.skipped=!1,e.type||(e.type="console"),Array.isArray(e.depends)&&(e.depends=[...new Set(e.depends)]),this.addProcessFilters.size)for(const n of this.addProcessFilters)"function"==typeof n&&([t,e]=n.call(this,t,e));const s=this.normalizeFlowId(t);return e.flowId=s,this.getOrCreateFlow(s).push(e),this},async load({between:e=!1}={}){if(!this.hasStarted){this.hasStarted=!0,this.globalBetween=e;for(const e of this.loadActions)"function"==typeof e&&e.call(this);return new Promise(e=>{if(!this.flows.size)return this.reset(),e();this.globalResolve=e,this.processFlows()})}},reset(){if(this.resetActions.size)for(const e of this.resetActions)"function"==typeof e&&e.call(this);return this.flows.clear(),this.flowOptions.clear(),this.flowGroups.clear(),this.pendingFlows.clear(),this.pendingProcesses.clear(),this.completedProcesses.clear(),this.currentProcessPerFlow.clear(),this.onAllComplete=null,this.globalResolve=null,this.hasStarted=!1,this.globalBetween=0,this.log("RESET"),this},destroy(){return this.reset(),this.types.clear(),this.initActions.clear(),this.loadActions.clear(),this.addProcessFilters.clear(),this.completedFlowsActions.clear(),this.flowIdFilters.clear(),this.conditionHandlers.clear(),this.triggerHandlers.clear(),this.processCompleteActions.clear(),this.allCompleteActions.clear(),this.resetActions.clear(),this.handlerCallbacksFilters.clear(),this.logger=null,this.eventsEnabled=!1,this.initialized=!1,this},setLogger(e){return!e||"function"!=typeof e&&"object"!=typeof e||(this.logger=e),this},setOnAllComplete(e){return this.onAllComplete="function"==typeof e?e:null,this},useEvents(){return this.eventsEnabled=!0,this},log(e,...t){this.logger&&"function"==typeof this.logger.log&&this.logger.log(e,...t)},error(e,...t){this.logger&&"function"==typeof this.logger.error&&this.logger.error(e,...t)},fire(e,t){this.eventsEnabled&&this.EVENTS[e]&&window.dispatchEvent(new CustomEvent(this.EVENTS[e],{detail:t}))},normalizeFlowId(e){return!0===e?e=this.FLOW_TYPE.ORDERED:"string"!=typeof e&&(e=this.FLOW_TYPE.DEFAULT),e},registerType(e,t){return"string"!=typeof e||"function"!=typeof t||this.types.set(e,t),this},registerTypes(e){if(!e||"object"!=typeof e)return this;const t=Array.isArray(e)?e:Object.entries(e).map(([e,t])=>"function"==typeof t?{type:e,handler:t}:t);for(const s of t)s&&this.registerType(s.type,s.handler);return this},pauseGroup(e){const t=this.flowGroups.get(e);if(!t)return this;for(const s of t)this.setFlowOptions({paused:!0},s);return this},runGroup(e){const t=this.flowGroups.get(e);if(!t)return this;for(const s of t)this.setFlowOptions({paused:!1},s),this.runFlow(s);return this},runFlow(e,t=!1){const s=this.flows.get(e),n=this.flowOptions.get(e);return s&&n&&n.status===this.FLOW_STATE.READY?(this.processFlows(e,t),this):this},getOrCreateFlow(e){return this.flows.has(e)||(this.flows.set(e,[]),this.flowOptions.set(e,{...this.FLOW_OPTIONS,ordered:e===this.FLOW_TYPE.ORDERED,status:this.FLOW_STATE.READY})),this.flows.get(e)},setFlowOptions(e={},t=null){if(t=this.normalizeFlowId(t),this.getOrCreateFlow(t),!e||"object"!=typeof e)return this;const s=this.flowOptions.get(t);return Array.isArray(e.depends)&&e.depends.length&&(e.depends=[...new Set(e.depends)],e.paused=!0,this.pendingFlows.add(t)),e.group&&(this.flowGroups.has(e.group)||this.flowGroups.set(e.group,new Set),this.flowGroups.get(e.group).add(t)),this.flowOptions.set(t,{...s,...e}),this},checkPendingFlows(){if(this.pendingFlows.size)for(const e of this.pendingFlows){const t=this.flowOptions.get(e);t&&t.depends&&t.depends.every(e=>{const t=this.flowOptions.get(this.normalizeFlowId(e));return t&&t.status===this.FLOW_STATE.COMPLETED})&&(this.setFlowOptions({paused:!1},e),this.pendingFlows.delete(e),this.runFlow(e))}},checkPendingProcesses(){if(this.pendingProcesses.size)for(const e of this.pendingProcesses){const t=e.depends.filter(e=>{const t=Array.from(this.flows.values()).some(t=>t.some(t=>t.id===this.PREFIX+e));return!this.completedProcesses.has(this.PREFIX+e)&&!t});if(t.length){this.pendingProcesses.delete(e),this.log("DEP_NOT_FOUND",e.id,`${t.join(", ")}`),e._depsResolved=!0,e._triggered=!0,e._waitResolve&&e._waitResolve();continue}e.depends.every(e=>this.completedProcesses.has(this.PREFIX+e))&&(this.pendingProcesses.delete(e),e._depsResolved=!0,e._triggered&&e._waitResolve&&e._waitResolve())}},async maybeComplete(){var e;if(!this.hasStarted)return;if(this.pendingFlows.size)for(const n of this.pendingFlows){const e=this.flowOptions.get(n);e&&e.depends&&e.depends.some(e=>!this.flowOptions.has(this.normalizeFlowId(e)))&&(this.setFlowOptions({status:this.FLOW_STATE.COMPLETED},n),this.pendingFlows.delete(n),this.log("FLOW_DEP_SKIPPED",{flow:n,depends:`${e.depends.filter(e=>!this.flowOptions.has(this.normalizeFlowId(e))).join(", ")}`}))}let t=!!this.flowOptions.size&&Array.from(this.flowOptions.values()).every(e=>e.status===this.FLOW_STATE.COMPLETED);if(!t)return;let s=!0;if(this.flows.size&&this.completedFlowsActions.size)for(const n of this.completedFlowsActions)s=n.call(this,t,this.flows,this.flowOptions);if(s&&!this.completing){if(this.completing=!0,this.log("ALL_COMPLETED"),this.fire("ALL_COMPLETED"),null==(e=this.onAllComplete)||e.call(this),null!==this.globalResolve&&this.globalResolve(),this.allCompleteActions.size)for(const e of this.allCompleteActions)"function"==typeof e&&e.call(this);this.autoReset&&this.reset(),this.completing=!1}},processFlows(e=null,t=!1){if(!this.flows.size)return;let s=e?[e]:Array.from(this.flows.keys());if(s.length&&this.flowIdFilters.size)for(const n of this.flowIdFilters)"function"==typeof n&&(s=n.call(this,s));s.sort((e,t)=>{const s=this.flowOptions.get(e),n=this.flowOptions.get(t),i=null!=(null==s?void 0:s.trigger),o=null!=(null==n?void 0:n.trigger);if(i&&!o)return 1;if(!i&&o)return-1;const r=(null==s?void 0:s.priority)||0;return((null==n?void 0:n.priority)||0)-r});for(const n of s){const s=this.flowOptions.get(n);if(s&&s.status!==this.FLOW_STATE.COMPLETED)if(this.getConditionStatus(s.condition))this.setFlowOptions({status:this.FLOW_STATE.COMPLETED},n);else if(!s.paused){if((null===e||e&&!t)&&null!=s.trigger){const e=this.getTriggerFunction(s.trigger,s);if(e){this.setFlowOptions({paused:!0},n);let t=!1;e(()=>{t||(t=!0,this.setFlowOptions({paused:!1},n),this.runFlow(n,!0))});continue}}this.setFlowOptions({status:this.FLOW_STATE.RUNNING},n),(async()=>{var e,t;null==(e=s.beforeStart)||e.call(s),s.delay&&s.delay>0&&await new Promise(e=>setTimeout(e,s.delay));const i=(this.flows.get(n)||[]).slice();if(i.sort((e,t)=>(t.priority||0)-(e.priority||0)),(s.preload||s.prefetch)&&!s.trigger)for(const n of i)if(!n.trigger&&("script"===n.type&&n.src||"style"===n.type&&n.href))try{const e=document.createElement("link");e.rel=s.preload?"preload":"prefetch",e.href=n.src||n.href,e.as="script"===n.type?"script":"style",n.crossOrigin&&(e.crossOrigin=n.crossOrigin),document.head.appendChild(e)}catch(r){this.error("PRELOAD_ERROR",r,n.id)}const o=void 0!==s.between&&null!==s.between?s.between:this.globalBetween;if(s.ordered){let e=Promise.resolve();i.forEach((t,s)=>{e=e.then(async()=>{s>0&&o&&await new Promise(e=>setTimeout(e,o)),await this.run(t)})}),await e}else{const e=i.map(async(e,t)=>(t>0&&o&&await new Promise(e=>setTimeout(e,o)),this.run(e)));await Promise.all(e)}this.setFlowOptions({status:this.FLOW_STATE.COMPLETED},n),null==(t=s.onComplete)||t.call(s),this.checkPendingFlows(),this.maybeComplete()})()}}this.checkPendingFlows(),this.maybeComplete()},async run(e){if(!e._running&&!this.getConditionStatus(e.condition)){if(e._running=!0,e._waitPromise||(e._triggered=!1,e._depsResolved=!1,e._waitPromise=new Promise(t=>e._waitResolve=t)),null!=e.trigger){const t=this.getTriggerFunction(e.trigger,e);t?t(()=>{e._triggered=!0,e._depsResolved&&e._waitResolve()}):e._triggered=!0}else e._triggered=!0;if(Array.isArray(e.depends)&&e.depends.length){const t=e.depends.filter(e=>{const t=Array.from(this.flows.values()).some(t=>t.some(t=>t.id===this.PREFIX+e));return!this.completedProcesses.has(this.PREFIX+e)&&!t});if(t.length)this.log("DEP_NOT_FOUND",e.id,`${t.join(", ")}`),e._depsResolved=!0,e._triggered=!0,e._waitResolve&&e._waitResolve();else{e.depends.every(e=>this.completedProcesses.has(this.PREFIX+e))?(e._depsResolved=!0,e._triggered&&e._waitResolve()):this.pendingProcesses.add(e)}}else e._depsResolved=!0,e._triggered&&e._waitResolve();return await e._waitPromise,this.execute(e)}},getConditionStatus(e){if(null==e)return!1;if(Array.isArray(e))return e.some(e=>this.getConditionStatus(e));if("object"==typeof e&&e.operator){const{operator:t,conditions:s}=e;return!!Array.isArray(s)&&("or"===t?s.every(e=>this.getConditionStatus(e)):"and"===t&&s.some(e=>this.getConditionStatus(e)))}if("function"==typeof e&&!e())return!0;if("boolean"==typeof e&&!e)return!0;if(this.conditionHandlers.size)for(const t of this.conditionHandlers)if("function"==typeof t){const s=t.call(this,e);if(!0===s||!1===s)return s}return!1},getTriggerFunction(e,t){if(null==e)return null;if("function"==typeof e)return t=>e(t);if(Array.isArray(e))return s=>{const n=e.map(e=>this.getTriggerFunction(e,t)).filter(e=>e);if(0===n.length)return void s();let i=0;const o=()=>{++i===n.length&&s()};n.forEach(e=>e(o))};if("object"==typeof e&&e.operator){const{operator:s,triggers:n}=e;return Array.isArray(n)?"or"===s?e=>{const s=n.map(e=>this.getTriggerFunction(e,t)).filter(e=>e);if(0===s.length)return void e();let i=!1;const o=()=>{i||(i=!0,e())};s.forEach(e=>e(o))}:"and"===s?e=>{const s=n.map(e=>this.getTriggerFunction(e,t)).filter(e=>e);if(0===s.length)return void e();let i=0;const o=()=>{++i===s.length&&e()};s.forEach(e=>e(o))}:null:null}if(this.triggerHandlers.size)for(const s of this.triggerHandlers)if("function"==typeof s){const n=s.call(this,e,t);if(n&&"function"==typeof n)return n}return!0===e||"interaction"===e?e=>this.waitForInteraction(e):null},waitForInteraction(e){["click","keydown","wheel","mousedown","mousemove","touchstart"].forEach(t=>window.addEventListener(t,()=>e(),{once:!0,passive:!0}))},async execute(e){const t=async()=>{if(this.processCompleteActions.size)for(const t of this.processCompleteActions)"function"==typeof t&&t.call(this,e);this.completedProcesses.add(e.id),e._running=!1,this.checkPendingProcesses()};if(e.skipped)return this.fire("SKIPPED",{...e,id:e.id}),void t();const s=this.types.get(e.type);if(!s)return this.log("UNKNOWN_TYPE",e.type,e.id),void t();this.fire("STARTED",{...e,id:e.id});const n={};if(this.handlerCallbacksFilters.size)for(const i of this.handlerCallbacksFilters)if("function"==typeof i){const t=i.call(this,e);t&&"object"==typeof t&&Object.assign(n,t)}return s(e,n).then(()=>{this.fire("COMPLETED",{...e,id:e.id}),t()}).catch(()=>{this.fire("ERROR",{...e,id:e.id}),t()})}},t=(e,t)=>t?(e.includes("?")?"&":"?")+Date.now():"",s=e=>{window.dispatchEvent(new CustomEvent("QSL:log",{detail:e}))},n=e=>{window.dispatchEvent(new CustomEvent("QSL:error",{detail:e}))},i=(e,t={})=>new Promise(async i=>{var o;const{flowId:r,tag:l,id:a,delay:c,data:d,onBeforeStart:u,onComplete:h,onError:f,footer:p,dom:g,onElement:E,onCustomResolve:w}=e;if(r&&l&&a)try{u&&await u(e),s({tag:l,type:"PROCESS_STARTED",config:e}),c&&await new Promise(e=>setTimeout(e,c));let r=document.createElement(l);if(null==E||E(r,e),null==(o=t.registerProcessElement)||o.call(t,r,e),d&&"object"==typeof d)for(const[e,t]of Object.entries(d))r.setAttribute(`data-${e.replace(/([A-Z])/g,"-$1").toLowerCase().replace(/[^a-z0-9_-]/g,"").replace(/^-/,"")}`,String(t));w||(r.onload=()=>{s({tag:l,type:"PROCESS_COMPLETED",config:e}),null==h||h(),i()}),r.onerror=t=>{n({tag:l,type:"PROCESS_FAILED",config:e}),null==f||f(t),i(t)},g&&(p?document.body:document.head).appendChild(r),null==w||w({el:r,config:e,resolve:i})}catch(m){n({tag:l,type:"PROCESS_FAILED",config:e,error:m}),null==f||f(m),i(m)}else i()}),o={type:"inline-script",handler:(e,t)=>{var n;let o=null,r=!1;const l=`QSL:inline-script:completed:${e.id}`,a=()=>{var t;s({tag:"inline-script",type:"INLINE_SCRIPT_SUCCESS",config:e}),s({tag:"inline-script",type:"PROCESS_COMPLETED",config:e}),null==(t=null==e?void 0:e.onComplete)||t.call(e),null==o||o()},c={...e,code:null==(n=e.code)?void 0:n.replace(/<script.*?>|<\/script>/gi,"")};return i({...c,tag:"script",dom:!0,onElement:(e,t)=>{const{code:s,module:n,id:i,flowId:c}=t;if(s&&(e.textContent=s),n){e.type="module";const t=e.textContent,s=JSON.stringify(String(c)),n=`(function(){window.__QSL__.currentProcessPerFlow.set(${s},${JSON.stringify(String(i))});try{${t}}finally{window.__QSL__.currentProcessPerFlow.delete(${s});window.dispatchEvent(new Event(${JSON.stringify(String(l))}));}})();`;e.textContent=n;const d=()=>{window.removeEventListener(l,d),o?a():r=!0};window.addEventListener(l,d)}},onCustomResolve:({el:e,config:t,resolve:s})=>{const{module:n}=t;o=s,n&&r?a():n||queueMicrotask(()=>{o===s&&a()})}},t)}},r={type:"script",handler:(e,s)=>i({...e,tag:"script",dom:!0,onElement:(e,{src:s,module:n,async:i,defer:o,crossOrigin:r,integrity:l,bypassCache:a})=>{n&&(e.type="module"),i&&(e.async=i),o&&(e.defer=o),r&&(e.crossOrigin=r),l&&(e.integrity=l),s&&(e.src=s+t(s,a))}},s)},l={type:"style",handler:(e,t)=>{var n;const o={...e,code:null==(n=e.code)?void 0:n.replace(/<style.*?>|<\/style>/gi,"")};return i({...o,tag:"style",dom:!0,onElement:(e,{code:t})=>{t&&(e.textContent=t)},onCustomResolve:({el:e,config:t,resolve:n})=>{const{tag:i,onComplete:o}=t;s({tag:i,type:"INLINE_STYLE_SUCCESS",config:t}),s({tag:i,type:"PROCESS_COMPLETED",config:t}),null==o||o(),n()}},t)}},a={type:"stylesheet",handler:(e,s)=>i({...e,tag:"link",dom:!0,onElement:(e,{href:s,crossOrigin:n,bypassCache:i})=>{e.rel="stylesheet",n&&(e.crossOrigin=n),e.href=s+t(s,i)}},s)},c={type:"pixel",handler:(e,n)=>i({...e,tag:"img",dom:!0,onElement:(e,{style:s={display:"none"},dom:n,src:i,bypassCache:o})=>{if(n||(e=new window.Image),e.src=i+t(i,o),e.width=1,e.height=1,s&&"object"==typeof s)for(const[t,r]of Object.entries(s))e.style.setProperty(t,r)},onCustomResolve:({el:e,config:t,resolve:n})=>{const{tag:i,dom:o,onComplete:r}=t,l=()=>{s({tag:i,type:"IMAGE_LOADED",config:t}),s({tag:i,type:"PROCESS_COMPLETED",config:t}),null==r||r(),n()};o?e.onload=()=>l():l()}},n)},d={type:"shadow",handler:(e,t)=>i({...e,onBeforeStart:async({tag:e})=>await window.customElements.whenDefined(e),onElement:(e,{shadowData:t})=>{e.data=t||{},(null==t?void 0:t.hidden)&&e.setAttribute("hidden","")},onCustomResolve:({el:e,config:t,resolve:i})=>{const{tag:o,shadowData:r,onComplete:l,onError:a}=t,c=()=>{const c=null==r?void 0:r.container;let d=null;if("body"===c)d=document.body;else if("string"==typeof c&&c)try{d=document.querySelector(c)}catch(u){d=null}if(!d){const e=new Error(`Container not found: ${c}`);return n({tag:o,type:"SHADOW_FAILED",config:t,error:e}),null==a||a(e),void i(e)}"top"===(null==r?void 0:r.position)?d.insertBefore(e,d.firstChild):d.appendChild(e),s({tag:o,type:"SHADOW_SUCCESS",config:t}),s({tag:o,type:"PROCESS_COMPLETED",config:t}),null==l||l(),i()};"interactive"===document.readyState||"complete"===document.readyState?c():document.addEventListener("DOMContentLoaded",c,{once:!0})}},t)},u={type:"html",handler:(e,t)=>i({...e,dom:!0,onElement:(e,{html:t="",id:s="",className:n="",style:i={}})=>{if(t&&(e.innerHTML=t),s&&(e.id=s),n&&(e.className=Array.isArray(n)?n.join(" "):n),i&&"object"==typeof i)for(const[o,r]of Object.entries(i))e.style.setProperty(o,r)},onCustomResolve:({el:e,config:t,resolve:n})=>{const{tag:i,onComplete:o}=t;s({tag:i,type:"HTML_SUCCESS",config:t}),s({tag:i,type:"PROCESS_COMPLETED",config:t}),null==o||o(),n()}},t)};const h=(e,t)=>e.slice(t.length),f=(e,t)=>"string"==typeof e&&e.startsWith(t),p=(e,t)=>{let s=null;try{s=document.querySelector(e)}catch(o){return void t(null)}if(s)return void t(s);if("function"!=typeof MutationObserver)return void t(null);const n=document.body||document.documentElement;if(!n)return void t(null);const i=new MutationObserver(()=>{const s=document.querySelector(e);s&&(i.disconnect(),t(s))});i.observe(n,{childList:!0,subtree:!0})};e.registerTypes([r,o,l,a,c,d,u]).use(function(e){e.initActions.add(function(){e.logger={VERSION:"qsl-logger",LOG:{LOGGER_LOADED:"[QSL] Logger loaded",LOGGER_LOAD_ERROR:"[QSL] Logger load error:",UNKNOWN_TYPE:"[QSL] Unknown type:",PROCESS_STARTED:"[QSL] Process started:",PROCESS_COMPLETED:"[QSL] Process completed:",PROCESS_FAILED:"[QSL] Process failed:",STYLESHEET_STARTED:"[QSL] Stylesheet loading:",STYLESHEET_LOADED:"[QSL] Stylesheet loaded:",STYLESHEET_FAILED:"[QSL] Stylesheet failed to load:",INLINE_SCRIPT_STARTED:"[QSL] Inline script loading:",INLINE_SCRIPT_SUCCESS:"[QSL] Inline script loaded:",INLINE_SCRIPT_ERROR:"[QSL] Inline script load error:",INLINE_STYLE_STARTED:"[QSL] Inline style loading:",INLINE_STYLE_SUCCESS:"[QSL] Inline style loaded:",INLINE_STYLE_ERROR:"[QSL] Inline style load error:",IMAGE_STARTED:"[QSL] Image pixel loading:",IMAGE_LOADED:"[QSL] Image pixel loaded:",IMAGE_FAILED:"[QSL] Image pixel failed:",SHADOW_STARTED:"[QSL] Shadow element loading:",SHADOW_SUCCESS:"[QSL] Shadow element loaded:",SHADOW_FAILED:"[QSL] Shadow element failed:",HTML_STARTED:"[QSL] HTML element loading:",HTML_SUCCESS:"[QSL] HTML element loaded:",HTML_FAILED:"[QSL] HTML element failed:",PRELOAD_ERROR:"[QSL] Preload error:",FLOW_DEP_SKIPPED:"[QSL] Flow dependency missed:",DEP_NOT_FOUND:"[QSL] Dependency not found:",RESET:"[QSL] Global reset",ALL_COMPLETED:"[QSL] Loading is completed"},log(e,...t){this.LOG[e]?console.log(this.LOG[e],...t,{timestamp:Date.now()}):console.log("[QSL] "+e,...t)},error(e,...t){this.LOG[e]?console.error(this.LOG[e],...t):console.error("[QSL] "+e,...t)}}})}).use(function(e){const t=new Map,s=new WeakMap;let n=null,i=null,o=!1;const r=e=>{if(!e)return"";try{return e.includes("://")?new URL(e).pathname:e.split("?")[0].split("#")[0]}catch(t){return e.split("?")[0].split("#")[0]}};e.loadActions.add(function(){if(o)return;n=document.addEventListener,i=window.addEventListener,o=!0;const e=(e,n,i)=>{var o;let l=null,a=null;if(document.currentScript){const e=s.get(document.currentScript);e&&(l=e.processId,a=e.flowId)}if(!l&&this.currentProcessPerFlow.size)for(const[t,s]of this.currentProcessPerFlow)if(s){l=s,a=t;break}if(!l){const e=(new Error).stack;if(e){const t=e.split("\n");for(let e=2;e<t.length;e++){const s=t[e].match(/([^()\s]+\.js(?:\?[^:)]*)?):\d+(?::\d+)?/);if(s){const e=s[1],t=e.indexOf("?"),n=-1===t?e.trim():e.slice(0,t).trim();if(n){const e=n.indexOf("?"),t=n.indexOf("#"),s=-1===e?-1===t?n.length:t:-1===t?e:Math.min(e,t);let i=n.slice(0,s);try{i.includes("://")&&(i=new URL(i).pathname)}catch(c){}const o=i.lastIndexOf("/"),d=-1===o?i:i.slice(o+1);for(const[n,c]of this.flows){for(const e of c)if(e.src&&"script"===e.type){const t=r(e.src),s=t.lastIndexOf("/"),o=-1===s?t:t.slice(s+1);if(i===t||d&&d===o){l=e.id,a=n;break}}if(l)break}if(l)break}}}}}if(l&&a){const s=this.flows.get(a),r=null==s?void 0:s.find(e=>e.id===l||e.id===this.PREFIX+l);if(r){if(!1!==r.fireEvents&&!1!==(null==(o=this.flowOptions.get(a))?void 0:o.fireEvents)){const s=`${e}:${r.id}`;t.has(r.id)||t.set(r.id,[]);const o=t.get(r.id);o.some(e=>e.name===s)||o.push({type:n,name:s}),i&&(e=s)}}}return e};document.addEventListener=(t,s,i)=>{if("DOMContentLoaded"===t&&this.LIFECYCLE.DOMREADY){const s=e.call(this,t,this.EVENTS.DOMREADY,!0);s!==t&&(t=s,queueMicrotask(()=>document.dispatchEvent(new Event(s))))}return n.call(document,t,s,i)},window.addEventListener=(t,s,n)=>{if("load"===t&&this.LIFECYCLE.LOADED){const s=e.call(this,t,this.EVENTS.LOADED,!0);s!==t&&(t=s,queueMicrotask(()=>window.dispatchEvent(new Event(s))))}return i.call(window,t,s,n)}}),e.handlerCallbacksFilters.add(function(e){return{registerProcessElement:(e,t)=>{s.set(e,{flowId:t.flowId,processId:t.id})}}}),e.processCompleteActions.add(function(e){const s=t.get(e.id);if(s&&Array.isArray(s))for(const t of s)t.type===this.EVENTS.DOMREADY?this.LIFECYCLE.DOMREADY?document.dispatchEvent(new Event(t.name)):document.addEventListener("DOMContentLoaded",()=>{document.dispatchEvent(new Event(t.name))},{once:!0}):t.type===this.EVENTS.LOADED&&(this.LIFECYCLE.LOADED?window.dispatchEvent(new Event(t.name)):window.addEventListener("load",()=>{window.dispatchEvent(new Event(t.name))},{once:!0}))}),e.resetActions.add(function(){o&&n&&i&&(document.addEventListener=n,window.addEventListener=i,o=!1)}),e.customEvents=t}).use(function(e){e.loadActions.add(function(){const e=(t,s,n=new Set)=>{if(n.has(t))return!0;n.add(t);const i=s(t)||[];for(const o of i)if(e(o,s,new Set(n)))return!0;return!1},t=e=>{var t;return(null==(t=this.flowOptions.get(this.normalizeFlowId(e)))?void 0:t.depends)||[]};for(const[n,i]of this.flowOptions.entries())Array.isArray(i.depends)&&i.depends.length&&e(n,t)&&(this.log("CIRC_FLOW_DEP_SKIPPED",n,i.depends),this.setFlowOptions({status:this.FLOW_STATE.COMPLETED},n));const s=e=>{for(const t of this.flows.values()){const s=t.find(t=>t.id===e);if(s&&Array.isArray(s.depends))return s.depends.map(e=>this.PREFIX+e)}return[]};for(const n of this.flows.values())for(const t of n)Array.isArray(t.depends)&&t.depends.length&&e(t.id,s)&&(this.log("CIRC_PROCESS_DEP_SKIPPED",t.id,t.depends),t.condition=!1)}),e.logger&&"qsl-logger"===e.logger.VERSION&&(e.logger.LOG.CIRC_FLOW_DEP_SKIPPED="[QSL] Circular flow dependency skipped:",e.logger.LOG.CIRC_PROCESS_DEP_SKIPPED="[QSL] Circular process dependency skipped:")}).use(function(e){e.conditionHandlers.add(function(e){return"string"==typeof e&&e.startsWith("media:")?!window.matchMedia(e.slice(6)).matches:null})}).use(function(e){e.conditionHandlers.add(function(e){var t;if("string"!=typeof e||!e.startsWith("lang:"))return null;const s=e.split(":"),n=s[1]||"equals",i=s.slice(2).join(":"),o=navigator.language||(null==(t=navigator.languages)?void 0:t[0])||"";switch(n){case"equals":case"is":return o!==i;case"contains":return!o.includes(i);case"startsWith":return!o.startsWith(i);case"in":return!i.split(",").map(e=>e.trim()).includes(o);default:return!0}})}).use(function(e){e.conditionHandlers.add(function(e){if("string"!=typeof e||!e.startsWith("tz:")&&!e.startsWith("timezone:"))return null;const t=e.split(":"),s=t[1]||"equals",n=t.slice(2).join(":");try{const e=Intl.DateTimeFormat().resolvedOptions().timeZone,t=-(new Date).getTimezoneOffset()/60;switch(s){case"equals":case"is":return e!==n;case"contains":return!e.includes(n);case"offset":return t!==parseInt(n);default:return!0}}catch(i){return!0}})}).use(function(e){e.conditionHandlers.add(function(e){if("string"!=typeof e||!e.startsWith("url:"))return null;const t=window.location,s=t.href,n=t.pathname,i=t.search,o=t.hostname,r=e.split(":"),l=r[1],a=r.slice(2).join(":");if(!l)return!0;switch(l){case"contains":return!s.includes(a);case"path":return!n.includes(a);case"pathStartsWith":return!n.startsWith(a);case"pathEndsWith":return!n.endsWith(a);case"query":if(i){const e=new URLSearchParams(i);if(a.includes("=")){const[t,s]=a.split("=");return e.get(t)!==s}return!e.has(a)}return!0;case"hostname":return!o.includes(a);case"matches":try{return!new RegExp(a).test(s)}catch(c){return!0}case"pathMatches":try{return!new RegExp(a).test(n)}catch(c){return!0}default:return!0}})}).use(function(e){e.conditionHandlers.add(function(e){if("string"!=typeof e||!e.startsWith("ua:")&&!e.startsWith("userAgent:"))return null;const t=e.split(":"),s=t[1]||"contains",n=t.slice(2).join(":"),i=navigator.userAgent||"",o=i.toLowerCase(),r=n.toLowerCase();switch(s){case"contains":if(!o.includes(r))return!0;break;case"equals":case"is":if(i!==n)return!0;break;case"matches":try{if(!new RegExp(n,"i").test(i))return!0}catch(l){return!0}break;case"browser":if(!{chrome:/chrome/i.test(i)&&!/edg|opr/i.test(i),firefox:/firefox/i.test(i),safari:/safari/i.test(i)&&!/chrome|chromium|edg|opr/i.test(i),edge:/edg/i.test(i),opera:/opr/i.test(i),ie:/msie|trident/i.test(i),chromium:/chromium/i.test(i)}[r])return!0;break;case"device":const e=/mobile|android|iphone|ipod|blackberry|iemobile|opera mini/i.test(i),t=/tablet|ipad|playbook|silk/i.test(i)||e&&/android/i.test(i)&&!/mobile/i.test(i),s=!e&&!t;switch(r){case"mobile":if(!e)return!0;break;case"tablet":if(!t)return!0;break;case"desktop":if(!s)return!0;break;default:return!0}break;case"os":case"platform":if(!{windows:/win/i.test(i),mac:/mac/i.test(i),ios:/iphone|ipad|ipod/i.test(i),android:/android/i.test(i),linux:/linux/i.test(i)&&!/android/i.test(i),unix:/unix/i.test(i),chromeos:/cros/i.test(i)}[r])return!0;break;default:return!0}return!1})}).use(function(e){e.triggerHandlers.add(function(e){return"load"!==e?null:e=>{"complete"===document.readyState?e():window.addEventListener("load",()=>e(),{once:!0})}})}).use(function(e){e.triggerHandlers.add(function(e){return"idle"!==e?null:e=>{"function"==typeof window.requestIdleCallback?window.requestIdleCallback(()=>e()):setTimeout(()=>e(),200)}})}).use(function(e){e.triggerHandlers.add(function(e){return"domready"!==e?null:e=>{"interactive"===document.readyState||"complete"===document.readyState?e():document.addEventListener("DOMContentLoaded",()=>e(),{once:!0})}})}).use(function(e){e.triggerHandlers.add(function(e){if(!f(e,"delay:"))return null;const t=parseInt(h(e,"delay:"),10);return e=>setTimeout(e,Number.isFinite(t)&&t>0?t:0)})}).use(function(e){e.triggerHandlers.add(function(e){if(!f(e,"hover:"))return null;const t=h(e,"hover:");return e=>{p(t,t=>{t?t.addEventListener("mouseenter",()=>e(),{once:!0,passive:!0}):e()})}})}).use(function(e){e.triggerHandlers.add(function(e){if(!f(e,"visible:"))return null;const t=h(e,"visible:");return e=>{p(t,t=>{if(!t||"function"!=typeof IntersectionObserver)return void e();const s=new IntersectionObserver(t=>{for(const n of t)if(n.isIntersecting)return s.disconnect(),void e()});s.observe(t)})}})}).use(function(e){e.triggerHandlers.add(function(e){if(!f(e,"appears:"))return null;const t=h(e,"appears:");return e=>p(t,()=>e())})}).use(function(e){e.triggerHandlers.add(function(e,t){if(!f(e,"media:"))return null;const s=h(e,"media:");return e=>{if(!s.length||"function"!=typeof window.matchMedia)return t&&(t.skipped=!0),void e();const n=window.matchMedia(s);if(n.matches)return void e();const i=t=>{t.matches&&(n.removeEventListener("change",i),e())};n.addEventListener("change",i)}})}).init()}();
2
+ //# sourceMappingURL=qsl.min.js.map