@cocreate/plugins 1.7.0 → 1.9.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/CONTRIBUTING.md CHANGED
@@ -13,7 +13,7 @@ Fork the [project on Github](https://github.com/CoCreate-app/CoCreate-plugins) a
13
13
  ```
14
14
  git clone https://github.com/contributor/CoCreate-plugins.git
15
15
  cd CoCreate-plugins
16
- git remote add upstream https://github.com/CoCreate-app/CoCreate-plugins.git
16
+ git remote add upstream https://gitlab.com/cocreatejs/CoCreate-plugins.git
17
17
  ```
18
18
 
19
19
  ## Create a Topic Branch
package/README.md CHANGED
@@ -1,92 +1,432 @@
1
- # CoCreate-plugins
2
-
3
- CoCreate plugins Take it for a spin in our [playground!](https://cocreate.app/docs/plugins)
4
-
5
- ![minified](https://img.badgesize.io/https://cdn.cocreate.app/plugins/latest/CoCreate-plugins.min.js?style=flat-square&label=minified&color=orange)
6
- ![gzip](https://img.badgesize.io/https://cdn.cocreate.app/plugins/latest/CoCreate-plugins.min.js?compression=gzip&style=flat-square&label=gzip&color=yellow)
7
- ![brotli](https://img.badgesize.io/https://cdn.cocreate.app/plugins/latest/CoCreate-plugins.min.js?compression=brotli&style=flat-square&label=brotli)
8
- ![GitHub latest release](https://img.shields.io/github/v/release/CoCreate-app/CoCreate-plugins?style=flat-square)
9
- ![GitHub contributors](https://img.shields.io/github/contributors/CoCreate-app/CoCreate-plugins?style=flat-square)
10
- ![License](https://img.shields.io/static/v1?style=flat-square&label=license&message=AGPL-3.0&color=green)
11
- ![Hiring](https://img.shields.io/static/v1?style=flat-square&label=&message=Hiring&color=blueviolet)
12
-
13
- ![CoCreate-plugins](https://cdn.cocreate.app/docs/CoCreate-plugins.gif)
14
-
15
- ## [Docs & Demo](https://cocreate.app/docs/plugins)
16
-
17
- For a complete guide and working demo refer to the [doumentation](https://cocreate.app/docs/plugins)
18
-
19
- ## CDN
20
-
21
- ```html
22
- <script src="https://cdn.cocreate.app/plugins/latest/CoCreate-plugins.min.js"></script>
23
- ```
24
-
25
- ```html
26
- <script src="https://cdn.cocreate.app/plugins/latest/CoCreate-plugins.min.css"></script>
27
- ```
28
-
29
- ## NPM
30
-
31
- ```shell
32
- $ npm i @cocreate/plugins
33
- ```
34
-
35
- ## yarn
36
-
37
- ```shell
38
- $ yarn install @cocreate/plugins
39
- ```
40
-
41
- # Table of Contents
42
-
43
- - [Table of Contents](#table-of-contents)
44
- - [Announcements](#announcements)
45
- - [Roadmap](#roadmap)
46
- - [How to Contribute](#how-to-contribute)
47
- - [About](#about)
48
- - [License](#license)
49
-
50
- <a name="announcements"></a>
51
-
52
- # Announcements
53
-
54
- All updates to this library are documented in our [CHANGELOG](https://github.com/CoCreate-app/CoCreate-plugins/blob/master/CHANGELOG.md) and [releases](https://github.com/CoCreate-app/CoCreate-plugins/releases). You may also subscribe to email for releases and breaking changes.
55
-
56
- <a name="roadmap"></a>
57
-
58
- # Roadmap
59
-
60
- If you are interested in the future direction of this project, please take a look at our open [issues](https://github.com/CoCreate-app/CoCreate-plugins/issues) and [pull requests](https://github.com/CoCreate-app/CoCreate-plugins/pulls). We would love to hear your feedback.
61
-
62
- <a name="about"></a>
63
-
64
- # About
65
-
66
- CoCreate-plugins is guided and supported by the CoCreate Developer Experience Team.
67
-
68
- Please Email the Developer Experience Team [here](mailto:develop@cocreate.app) in case of any queries.
69
-
70
- CoCreate-plugins is maintained and funded by CoCreate. The names and logos for CoCreate are trademarks of CoCreate, LLC.
71
-
72
- <a name="contribute"></a>
73
-
74
- # How to Contribute
75
-
76
- We encourage contribution to our libraries (you might even score some nifty swag), please see our [CONTRIBUTING](https://github.com/CoCreate-app/CoCreate-plugins/blob/master/CONTRIBUTING.md) guide for details.
77
-
78
- We want this library to be community-driven, and CoCreate led. We need your help to realize this goal. To help make sure we are building the right things in the right order, we ask that you create [issues](https://github.com/CoCreate-app/CoCreate-plugins/issues) and [pull requests](https://github.com/CoCreate-app/CoCreate-plugins/pulls) or merely upvote or comment on existing issues or pull requests.
79
-
80
- We appreciate your continued support, thank you!
81
-
82
- <a name="license"></a>
83
-
84
- # License
85
-
86
- This software is dual-licensed under the GNU Affero General Public License version 3 (AGPLv3) and a commercial license.
87
-
88
- - **Open Source Use**: For open-source projects and non-commercial use, this software is available under the AGPLv3. The AGPLv3 allows you to freely use, modify, and distribute this software, provided that all modifications and derivative works are also licensed under the AGPLv3. For the full license text, see the [LICENSE file](https://github.com/CoCreate-app/CoCreate-plugins/blob/master/LICENSE).
89
-
90
- - **Commercial Use**: For-profit companies and individuals intending to use this software for commercial purposes must obtain a commercial license. The commercial license is available when you sign up for an API key on our [website](https://cocreate.app). This license permits proprietary use and modification of the software without the copyleft requirements of the AGPLv3. It is ideal for integrating this software into proprietary commercial products and applications.
91
-
92
- If you have not purchased a commercial license and intend to use this software for commercial purposes, you are required to sign up for an API key on our website.
1
+ # CoCreate Plugins
2
+
3
+ A declarative browser plugin loader and runtime for CoCreate. It loads registered JavaScript and CSS resources, exposes their browser globals to the CoCreate AST, initializes plugin instances on DOM elements, and connects those instances to standard element value APIs.
4
+
5
+ ---
6
+
7
+ ## Documentation
8
+
9
+ For interactive examples, visit the [CoCreate Plugins documentation](https://cocreate.app/docs/plugins).
10
+
11
+ ## Table of Contents
12
+
13
+ - [Architectural Overview](#architectural-overview)
14
+ - [Installation](#installation)
15
+ - [Registering Plugin Resources](#registering-plugin-resources)
16
+ - [Using the `plugin` Attribute](#using-the-plugin-attribute)
17
+ - [AST Execution Model](#ast-execution-model)
18
+ - [Element Value Integration](#element-value-integration)
19
+ - [Programmatic API](#programmatic-api)
20
+ - [Resource Loading](#resource-loading)
21
+ - [Error Handling](#error-handling)
22
+ - [Security](#security)
23
+ - [Announcements](#announcements)
24
+ - [Roadmap](#roadmap)
25
+ - [How to Contribute](#how-to-contribute)
26
+ - [License](#license)
27
+
28
+ ---
29
+
30
+ ## Architectural Overview
31
+
32
+ `@cocreate/plugins` connects three parts of the browser runtime:
33
+
34
+ 1. A plugin definition in `window.CoCreateConfig.plugins` identifies the JavaScript and CSS resources.
35
+ 2. A DOM element's `plugin` attribute contains an AST configuration.
36
+ 3. The resolved plugin instance and other top-level AST values are attached to that element.
37
+
38
+ ```text
39
+ CoCreateConfig.plugins
40
+ │
41
+ ├── load JavaScript and CSS
42
+ │
43
+ ▼
44
+ window.PluginName
45
+ │
46
+ ├── AST executes plugin configuration
47
+ │
48
+ ▼
49
+ element.PluginName
50
+ ```
51
+
52
+ Plugin configurations execute sequentially. A constructor result created by one top-level key is therefore available to every key that follows it.
53
+
54
+ The module observes `[plugin]` elements automatically, including elements added or updated after the initial page load.
55
+
56
+ ---
57
+
58
+ ## Installation
59
+
60
+ ### NPM
61
+
62
+ ```shell
63
+ npm install @cocreate/plugins
64
+ ```
65
+
66
+ ### Yarn
67
+
68
+ ```shell
69
+ yarn add @cocreate/plugins
70
+ ```
71
+
72
+ ### CDN
73
+
74
+ ```html
75
+ <script src="https://cdn.cocreate.app/plugins/latest/CoCreate-plugins.min.js"></script>
76
+ ```
77
+
78
+ When using the package as a module:
79
+
80
+ ```javascript
81
+ import plugins from "@cocreate/plugins";
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Registering Plugin Resources
87
+
88
+ Plugin resource definitions live under `window.CoCreateConfig.plugins`. The property name should match the global name created by the external library.
89
+
90
+ ```javascript
91
+ window.CoCreateConfig = {
92
+ plugins: {
93
+ SimpleBar: {
94
+ js: "https://cdn.jsdelivr.net/npm/simplebar@latest/dist/simplebar.min.js",
95
+ css: "https://cdn.jsdelivr.net/npm/simplebar@latest/dist/simplebar.css"
96
+ },
97
+ FullCalendar: {
98
+ js: "https://cdn.jsdelivr.net/npm/fullcalendar@6.1.15/index.global.min.js"
99
+ }
100
+ }
101
+ };
102
+ ```
103
+
104
+ Both `js` and `css` accept one resource or an array of resources. A resource can be a URL string or an object:
105
+
106
+ ```javascript
107
+ window.CoCreateConfig.plugins.Example = {
108
+ js: [
109
+ {
110
+ src: "https://cdn.example.com/example.min.js",
111
+ crossOrigin: "anonymous",
112
+ integrity: "sha384-..."
113
+ }
114
+ ],
115
+ css: [
116
+ {
117
+ href: "https://cdn.example.com/example.min.css",
118
+ crossOrigin: "anonymous",
119
+ integrity: "sha384-..."
120
+ }
121
+ ]
122
+ };
123
+ ```
124
+
125
+ Plugins can also be registered at runtime:
126
+
127
+ ```javascript
128
+ import { registerPlugin } from "@cocreate/plugins";
129
+
130
+ registerPlugin("SimpleBar", {
131
+ js: "https://cdn.jsdelivr.net/npm/simplebar@latest/dist/simplebar.min.js",
132
+ css: "https://cdn.jsdelivr.net/npm/simplebar@latest/dist/simplebar.css"
133
+ });
134
+ ```
135
+
136
+ Registration describes where a library is loaded from. Initialization behavior belongs in the element's `plugin` attribute.
137
+
138
+ ---
139
+
140
+ ## Using the `plugin` Attribute
141
+
142
+ ### Constructor or Function
143
+
144
+ The simplest object form calls the registered global and assigns its result to the element under the same name.
145
+
146
+ ```html
147
+ <div
148
+ plugin='{
149
+ "SimpleBar": ["$this", {}]
150
+ }'
151
+ style="max-height: 300px">
152
+ </div>
153
+ ```
154
+
155
+ This is conceptually equivalent to:
156
+
157
+ ```javascript
158
+ element.SimpleBar = new SimpleBar(element, {});
159
+ ```
160
+
161
+ `$this` resolves to the element containing the `plugin` attribute.
162
+
163
+ For a global function that receives a single options object:
164
+
165
+ ```html
166
+ <button
167
+ plugin='{
168
+ "Toastify": {
169
+ "text": "Saved successfully",
170
+ "duration": 3000
171
+ },
172
+ "Toastify.showToast": []
173
+ }'>
174
+ Show notification
175
+ </button>
176
+ ```
177
+
178
+ ### Nested Constructor
179
+
180
+ Some libraries expose constructors as properties of a global namespace. A single nested member identifies the constructor to invoke:
181
+
182
+ ```html
183
+ <div
184
+ id="calendar"
185
+ plugin='{
186
+ "FullCalendar": {
187
+ "Calendar": [
188
+ "$this",
189
+ {
190
+ "initialView": "dayGridMonth",
191
+ "editable": true
192
+ }
193
+ ]
194
+ },
195
+ "FullCalendar.render": []
196
+ }'>
197
+ </div>
198
+ ```
199
+
200
+ The first key creates the instance:
201
+
202
+ ```javascript
203
+ element.FullCalendar = new window.FullCalendar.Calendar(element, options);
204
+ ```
205
+
206
+ The next key operates on that resolved instance:
207
+
208
+ ```javascript
209
+ element.FullCalendar.render();
210
+ ```
211
+
212
+ ### Bare Plugin Name
213
+
214
+ A plain attribute value invokes the registered global without arguments:
215
+
216
+ ```html
217
+ <div plugin="ExamplePlugin"></div>
218
+ ```
219
+
220
+ Use the JSON form whenever the plugin requires the element, options, or additional operations.
221
+
222
+ ### Multiple Configuration Blocks
223
+
224
+ The attribute can contain an array. Each object is processed in order:
225
+
226
+ ```html
227
+ <div
228
+ plugin='[
229
+ { "FirstPlugin": ["$this"] },
230
+ { "SecondPlugin": ["$this", {}] }
231
+ ]'>
232
+ </div>
233
+ ```
234
+
235
+ ---
236
+
237
+ ## AST Execution Model
238
+
239
+ Each configuration object is converted into one AST program and evaluated sequentially.
240
+
241
+ ```json
242
+ {
243
+ "FullCalendar": {
244
+ "Calendar": ["$this", {}]
245
+ },
246
+ "FullCalendar.render": []
247
+ }
248
+ ```
249
+
250
+ Conceptually becomes:
251
+
252
+ ```json
253
+ {
254
+ "FullCalendar": {
255
+ "$FullCalendar.Calendar": ["$this", {}]
256
+ },
257
+ "FullCalendar.render": {
258
+ "$FullCalendar.render": []
259
+ }
260
+ }
261
+ ```
262
+
263
+ Execution follows these rules:
264
+
265
+ - Plugin globals are placed in the AST sandbox.
266
+ - `$this` is the element being initialized.
267
+ - Arrays are passed as positional arguments.
268
+ - An object is passed as a single argument.
269
+ - Native classes are instantiated with `new`.
270
+ - Functions are called normally.
271
+ - A resolved top-level value becomes available to subsequent keys.
272
+ - Resolved top-level keys without dot notation are attached to the element.
273
+ - Dotted keys represent operations and are not attached as element properties.
274
+
275
+ Ordinary non-plugin keys remain part of the AST program, so they can store intermediate results or participate in later operations.
276
+
277
+ ```html
278
+ <div
279
+ plugin='{
280
+ "ExamplePlugin": ["$this"],
281
+ "items": { "$ExamplePlugin.getItems": [] },
282
+ "count": "$items.length"
283
+ }'>
284
+ </div>
285
+ ```
286
+
287
+ After evaluation, `element.ExamplePlugin`, `element.items`, and `element.count` contain the corresponding resolved values.
288
+
289
+ ---
290
+
291
+ ## Element Value Integration
292
+
293
+ The reserved `getValue` and `setValue` keys connect a plugin instance to CoCreate's standard element value interface. Unlike initialization operations, these AST expressions are stored and evaluated only when the corresponding element method is called.
294
+
295
+ ```html
296
+ <div
297
+ id="calendar"
298
+ component
299
+ plugin='{
300
+ "FullCalendar": {
301
+ "Calendar": ["$this", { "events": [] }]
302
+ },
303
+ "FullCalendar.render": [],
304
+ "getValue": "$FullCalendar.getEvents",
305
+ "setValue": {
306
+ "$FullCalendar.addEvent": "$event.value"
307
+ }
308
+ }'>
309
+ </div>
310
+ ```
311
+
312
+ ### Reading a Value
313
+
314
+ ```javascript
315
+ const events = document.querySelector("#calendar").getValue();
316
+ ```
317
+
318
+ The string method reference is invoked with no arguments. Asynchronous consumers can use:
319
+
320
+ ```javascript
321
+ const events = await document.querySelector("#calendar").getValueAsync();
322
+ ```
323
+
324
+ ### Setting a Value
325
+
326
+ ```javascript
327
+ document.querySelector("#calendar").setValue({
328
+ title: "Planning meeting",
329
+ start: "2026-09-01"
330
+ });
331
+ ```
332
+
333
+ The runtime scope exposes:
334
+
335
+ | AST value | Description |
336
+ | --- | --- |
337
+ | `$event.value` | Value passed to `element.setValue(value)` |
338
+ | `$event.dispatch` | Optional dispatch argument passed to `setValue` |
339
+ | `$this` | Element containing the plugin instance |
340
+ | `$PluginName` | Resolved plugin instance |
341
+
342
+ ---
343
+
344
+ ## Programmatic API
345
+
346
+ ### `registerPlugin(name, config)`
347
+
348
+ Registers or replaces a resource definition in `window.CoCreateConfig.plugins`.
349
+
350
+ ```javascript
351
+ registerPlugin("ExamplePlugin", {
352
+ js: "https://cdn.example.com/plugin.js",
353
+ css: "https://cdn.example.com/plugin.css"
354
+ });
355
+ ```
356
+
357
+ ### `init(elements)`
358
+
359
+ Finds and initializes plugin configurations. It accepts an element, collection, array, or selector and returns a promise.
360
+
361
+ ```javascript
362
+ import { init } from "@cocreate/plugins";
363
+
364
+ await init(document.querySelector("#calendar"));
365
+ await init(document.querySelectorAll("[plugin]"));
366
+ await init("[plugin]");
367
+ ```
368
+
369
+ Calling `init()` directly is generally unnecessary because the module observes matching elements automatically. It is useful when application code needs to wait for plugin initialization to complete.
370
+
371
+ ---
372
+
373
+ ## Resource Loading
374
+
375
+ - JavaScript resources are cached by URL and loaded once.
376
+ - CSS resources already present in the document are not inserted again.
377
+ - Multiple resources are loaded concurrently with `Promise.allSettled()`.
378
+ - String resource definitions default to `crossOrigin="anonymous"`.
379
+ - Object definitions can override `crossOrigin` or provide `integrity`.
380
+ - After loading, the runtime waits briefly for the expected browser global.
381
+
382
+ The configured name should match the library's browser global. A capitalized fallback is supported, so a configured `simpleBar` can resolve `window.SimpleBar`, but using the exact global name is recommended.
383
+
384
+ ---
385
+
386
+ ## Error Handling
387
+
388
+ Resource failures are logged as warnings. Initialization fails when the expected global is still unavailable after loading:
389
+
390
+ ```text
391
+ [Plugin System] Plugin 'ExamplePlugin' is not available on window.
392
+ ```
393
+
394
+ Invalid JSON, AST failures, constructor errors, and method errors are reported with the original `plugin` attribute for debugging.
395
+
396
+ Because initialization is asynchronous, application code that requires a completed instance should await `init()`.
397
+
398
+ ---
399
+
400
+ ## Security
401
+
402
+ Plugin scripts execute with the same browser privileges as the application. Only register resources from trusted locations.
403
+
404
+ For third-party CDN resources:
405
+
406
+ - Pin versions instead of relying on floating tags such as `latest`.
407
+ - Provide Subresource Integrity metadata when available.
408
+ - Use an appropriate Content Security Policy.
409
+ - Review the library's data access and network behavior.
410
+
411
+ AST configuration controls how an already trusted plugin is initialized; it does not isolate untrusted JavaScript.
412
+
413
+ ---
414
+
415
+ ## Announcements
416
+
417
+ Updates are documented in the project [CHANGELOG](https://github.com/CoCreate-app/CoCreate-plugins/blob/master/CHANGELOG.md) and [releases](https://github.com/CoCreate-app/CoCreate-plugins/releases).
418
+
419
+ ## Roadmap
420
+
421
+ See the open [issues](https://github.com/CoCreate-app/CoCreate-plugins/issues) and [pull requests](https://github.com/CoCreate-app/CoCreate-plugins/pulls) for current development priorities.
422
+
423
+ ## How to Contribute
424
+
425
+ Contributions are welcome. See [CONTRIBUTING.md](https://github.com/CoCreate-app/CoCreate-plugins/blob/master/CONTRIBUTING.md) before opening an issue or pull request.
426
+
427
+ ## License
428
+
429
+ This software is dual-licensed under the GNU Affero General Public License version 3 (AGPLv3) and a commercial license.
430
+
431
+ - **Open-source use:** Modifications and derivative works must comply with the AGPLv3. See the [LICENSE](https://github.com/CoCreate-app/CoCreate-plugins/blob/master/LICENSE).
432
+ - **Commercial use:** A commercial license is available from [CoCreate](https://cocreate.app).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/plugins",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "CoCreate plugins",
5
5
  "author": "CoCreate LLC",
6
6
  "license": "AGPL-3.0",
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "repository": {
16
16
  "type": "git",
17
- "url": "git+https://github.com/CoCreate-app/CoCreate-plugins.git"
17
+ "url": "git+https://gitlab.com/cocreatejs/CoCreate-plugins.git"
18
18
  },
19
19
  "bugs": {
20
20
  "url": "https://github.com/CoCreate-app/CoCreate-plugins/issues"
@@ -27,14 +27,15 @@
27
27
  "type": "module",
28
28
  "main": "./src/index.js",
29
29
  "dependencies": {
30
- "@cocreate/observer": "^1.21.0"
30
+ "@cocreate/observer": "^1.23.0",
31
+ "@cocreate/utils": "^1.54.0"
31
32
  },
32
33
  "devDependencies": {
33
- "@cocreate/webpack": "^1.7.0"
34
+ "@cocreate/webpack": "^1.9.0"
34
35
  },
35
36
  "allowScripts": {
36
37
  "@cocreate/observer@1.19.0": true,
37
- "@cocreate/utils@1.42.2": true,
38
+ "@cocreate/utils@1.52.0": true,
38
39
  "@cocreate/webpack@1.4.3": true
39
40
  }
40
41
  }
package/src/index.js CHANGED
@@ -2,7 +2,9 @@ import Observer from '@cocreate/observer';
2
2
  import { astAsync } from '@cocreate/utils';
3
3
 
4
4
  const scriptCache = new Map();
5
- const cssMarker = typeof document !== 'undefined'
5
+ const pendingElements = new WeakSet();
6
+ const initializedPlugins = new WeakMap();
7
+ const cssMarker = typeof document !== 'undefined'
6
8
  ? document.head.querySelector('meta[name="viewport"]') || document.head.lastChild
7
9
  : null;
8
10
 
@@ -110,7 +112,8 @@ function loadCSS(item) {
110
112
 
111
113
  /**
112
114
  * Preloads JS scripts and CSS stylesheets declared in global plugins config.
113
- * Runs in parallel using Promise.allSettled.
115
+ * Loads each plugin's JavaScript resources in declared order while allowing
116
+ * independent plugins and stylesheets to load in parallel.
114
117
  * @param {Array<string>} pluginNames - Names of target plugins to preload
115
118
  */
116
119
  async function preloadResources(pluginNames) {
@@ -126,7 +129,11 @@ async function preloadResources(pluginNames) {
126
129
  }
127
130
  if (config.js) {
128
131
  const jsFiles = Array.isArray(config.js) ? config.js : [config.js];
129
- jsFiles.forEach(src => promises.push(loadScript(src)));
132
+ promises.push((async () => {
133
+ for (const src of jsFiles) {
134
+ await loadScript(src);
135
+ }
136
+ })());
130
137
  }
131
138
  }
132
139
  }
@@ -171,6 +178,91 @@ function getPluginLibrary(pluginName) {
171
178
  return null;
172
179
  }
173
180
 
181
+ function getPluginNames(configBlock) {
182
+ const configuredPlugins = getPluginsConfig();
183
+ const pluginNames = new Set();
184
+
185
+ function collect(value) {
186
+ if (!value || typeof value !== 'object') return;
187
+
188
+ for (const [key, childValue] of Object.entries(value)) {
189
+ const name = key.replace(/^\$/, '').split('.')[0];
190
+ if (configuredPlugins[name] || getPluginLibrary(name)) {
191
+ pluginNames.add(name);
192
+ }
193
+ collect(childValue);
194
+ }
195
+ }
196
+
197
+ collect(configBlock);
198
+
199
+ return pluginNames;
200
+ }
201
+
202
+ function isClassLike(value) {
203
+ if (typeof value !== 'function') return false;
204
+
205
+ const source = Function.prototype.toString.call(value);
206
+ if (/^class\s|^class{/.test(source)) return true;
207
+
208
+ return !!value.prototype && Object.getOwnPropertyNames(value.prototype)
209
+ .some(member => member !== 'constructor');
210
+ }
211
+
212
+ function getPluginMember(library, path) {
213
+ let value = library;
214
+
215
+ for (const property of path) {
216
+ if (value === null || value === undefined || !(property in Object(value))) {
217
+ return undefined;
218
+ }
219
+ value = value[property];
220
+ }
221
+
222
+ return value;
223
+ }
224
+
225
+ function createPluginOperations(configBlock, pluginNames, libraries) {
226
+ const operations = [];
227
+
228
+ for (const [key, value] of Object.entries(configBlock)) {
229
+ const normalizedKey = key.replace(/^\$/, '');
230
+ const pluginName = normalizedKey.split('.')[0];
231
+
232
+ if (!pluginNames.has(pluginName)) {
233
+ operations.push({ ast: { [key]: value } });
234
+ continue;
235
+ }
236
+
237
+ let operator = normalizedKey;
238
+ let parameters = value;
239
+ const library = libraries['$' + pluginName];
240
+ const memberPath = normalizedKey.split('.').slice(1);
241
+ const pluginMember = getPluginMember(library, memberPath);
242
+ const members = value && typeof value === 'object' && !Array.isArray(value)
243
+ ? Object.entries(value)
244
+ : [];
245
+
246
+ if (
247
+ !normalizedKey.includes('.') &&
248
+ typeof library !== 'function' &&
249
+ members.length === 1
250
+ ) {
251
+ const [member, memberParameters] = members[0];
252
+ operator = `${pluginName}.${member.replace(/^\$/, '')}`;
253
+ parameters = memberParameters;
254
+ }
255
+
256
+ operations.push({
257
+ ast: { ['$' + operator]: parameters },
258
+ pluginName,
259
+ attach: !normalizedKey.includes('.') || isClassLike(pluginMember)
260
+ });
261
+ }
262
+
263
+ return operations;
264
+ }
265
+
174
266
  /**
175
267
  * Parses element 'plugin' attribute, executes AST configuration via astAsync,
176
268
  * sets core library on window, and attaches instance onto element property.
@@ -183,6 +275,11 @@ async function processPlugin(el) {
183
275
  const rawPluginAttr = pluginAttrNode?.value?.trim();
184
276
  if (!rawPluginAttr) return;
185
277
 
278
+ // Added-node and attribute observers can discover the same element during
279
+ // separate mutation cycles. Do not initialize an unchanged plugin twice.
280
+ if (initializedPlugins.get(el) === rawPluginAttr) return;
281
+ initializedPlugins.set(el, rawPluginAttr);
282
+
186
283
  try {
187
284
  let configs = [];
188
285
 
@@ -193,62 +290,64 @@ async function processPlugin(el) {
193
290
  configs = [{ [rawPluginAttr]: [] }];
194
291
  }
195
292
 
196
- const targetPlugins = new Set();
197
- configs.forEach(conf => {
198
- Object.keys(conf).forEach(k => {
199
- if (!k.startsWith('\u0024')) {
200
- targetPlugins.add(k.split('.')[0]);
201
- }
202
- });
203
- });
293
+ const configPlans = configs.map(configBlock => ({
294
+ configBlock,
295
+ pluginNames: getPluginNames(configBlock)
296
+ }));
297
+ const targetPlugins = new Set(
298
+ configPlans.flatMap(({ pluginNames }) => Array.from(pluginNames))
299
+ );
204
300
 
205
301
  await preloadResources(Array.from(targetPlugins));
206
302
 
207
- for (const configBlock of configs) {
208
- for (const [pluginName, astValue] of Object.entries(configBlock)) {
209
- // Poll window to ensure script execution has completed
210
- const pluginLib = await waitForPluginOnWindow(pluginName);
303
+ for (const { configBlock, pluginNames } of configPlans) {
304
+ const libraries = {};
211
305
 
212
- // Ensure plugin library definition is attached to window once loaded
213
- if (pluginLib && typeof window !== 'undefined' && !window[pluginName]) {
214
- window[pluginName] = pluginLib;
306
+ for (const pluginName of pluginNames) {
307
+ const library = await waitForPluginOnWindow(pluginName);
308
+ if (!library) {
309
+ throw new Error(`Plugin '${pluginName}' is not available on window.`);
215
310
  }
311
+ libraries['$' + pluginName] = library;
312
+ }
216
313
 
217
- // Format operator key with $ for AST engine recognition (e.g. SimpleBar -> $SimpleBar)
218
- const operatorName = pluginName.startsWith('\u0024') ? pluginName : `$${pluginName}`;
219
-
220
- // Create constructor wrapper to safely instantiate class/function
221
- const opWrapper = function(params) {
222
- const args = Array.isArray(params) ? params : [params];
223
- if (typeof pluginLib === 'function') {
224
- try {
225
- return new pluginLib(...args);
226
- } catch (e) {
227
- return pluginLib(...args);
228
- }
229
- }
230
- return pluginLib;
231
- };
232
-
233
- // Provide operator wrapper in context for AST registry lookup
234
- const context = {
235
- [operatorName]: opWrapper
236
- };
237
-
238
- // Execute AST with the $ operator key so astAsync invokes operatorKey logic
239
- const result = await astAsync({
240
- node: el,
241
- sandbox: context,
242
- executeAST: { [operatorName]: astValue }
314
+ const scope = { ...libraries, $this: el };
315
+
316
+ if (Object.prototype.hasOwnProperty.call(configBlock, '$return')) {
317
+ const value = await astAsync({
318
+ executeAST: configBlock,
319
+ sandbox: scope,
320
+ node: el
243
321
  });
244
322
 
245
- if (result !== undefined && !pluginName.startsWith('\u0024')) {
246
- // Attach returned plugin instance directly onto element property
247
- el[pluginName] = result;
323
+ if (value !== undefined && pluginNames.size === 1) {
324
+ const [pluginName] = pluginNames;
325
+ el[pluginName] = value;
326
+ }
327
+ continue;
328
+ }
329
+
330
+ const operations = createPluginOperations(
331
+ configBlock,
332
+ pluginNames,
333
+ libraries
334
+ );
335
+
336
+ for (const operation of operations) {
337
+ const value = await astAsync({
338
+ executeAST: operation.ast,
339
+ sandbox: scope,
340
+ node: el
341
+ });
342
+
343
+ if (operation.attach && value !== undefined) {
344
+ scope['$' + operation.pluginName] = value;
345
+ el[operation.pluginName] = value;
248
346
  }
249
347
  }
250
348
  }
251
349
  } catch (err) {
350
+ initializedPlugins.delete(el);
252
351
  console.error('[Plugin System] Failed to parse and execute plugin configuration:', err, rawPluginAttr);
253
352
  }
254
353
  }
@@ -258,21 +357,33 @@ async function processPlugin(el) {
258
357
  * @param {Element|NodeList|Array|string} elements - Target element(s) or DOM query selector
259
358
  */
260
359
  export function init(elements) {
261
- if (!elements) return;
360
+ if (!elements) return Promise.resolve([]);
262
361
 
263
362
  if (typeof elements === 'string') {
264
363
  elements = document.querySelectorAll(elements);
265
364
  }
266
365
 
267
366
  if (elements instanceof Element) {
268
- processPlugin(elements);
367
+ return processPlugin(elements);
269
368
  } else if (elements.length !== undefined || Array.isArray(elements)) {
270
- Array.from(elements).forEach(el => {
271
- if (el instanceof Element) {
272
- processPlugin(el);
273
- }
274
- });
369
+ return Promise.all(
370
+ Array.from(elements)
371
+ .filter(el => el instanceof Element)
372
+ .map(el => processPlugin(el))
373
+ );
275
374
  }
375
+
376
+ return Promise.resolve([]);
377
+ }
378
+
379
+ function scheduleInit(element) {
380
+ if (!element || pendingElements.has(element)) return;
381
+
382
+ pendingElements.add(element);
383
+ requestAnimationFrame(() => {
384
+ pendingElements.delete(element);
385
+ if (element.isConnected) init(element);
386
+ });
276
387
  }
277
388
 
278
389
  if (typeof document !== 'undefined') {
@@ -282,10 +393,12 @@ if (typeof document !== 'undefined') {
282
393
  types: ["addedNodes", "attributes"],
283
394
  selector: selector,
284
395
  attributeFilter: ["plugin"],
285
- callback: (mutation) => init(mutation.target)
396
+ callback: (mutation) => {
397
+ scheduleInit(mutation.target);
398
+ }
286
399
  });
287
400
 
288
401
  init(document.querySelectorAll(selector));
289
402
  }
290
403
 
291
- export default { init, registerPlugin };
404
+ export default { init, registerPlugin };
package/webpack.config.js CHANGED
@@ -1,9 +1,13 @@
1
- const path = require("path");
2
- const MiniCssExtractPlugin = require("mini-css-extract-plugin");
3
- const { EsbuildPlugin } = require("esbuild-loader");
4
- const { FileUploader } = require("@cocreate/webpack");
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import MiniCssExtractPlugin from "mini-css-extract-plugin";
4
+ import { EsbuildPlugin } from "esbuild-loader";
5
+ import webpackPackage from "@cocreate/webpack";
5
6
 
6
- module.exports = async (env, argv) => {
7
+ const { FileUploader } = webpackPackage;
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+
10
+ export default async (env, argv) => {
7
11
  const isProduction = argv && argv.mode === "production";
8
12
  const config = {
9
13
  entry: {
@@ -52,7 +56,7 @@ module.exports = async (env, argv) => {
52
56
  })
53
57
  ],
54
58
  splitChunks: {
55
- pluginsGroups: {
59
+ cacheGroups: {
56
60
  defaultVendors: false
57
61
  }
58
62
  }