@cocreate/plugins 1.4.2 → 1.6.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.
@@ -9,33 +9,36 @@ jobs:
9
9
  about:
10
10
  runs-on: ubuntu-latest
11
11
  steps:
12
- - name: Checkout
13
- uses: actions/checkout@v3
14
- - name: Setup Node.js
15
- uses: actions/setup-node@v3
16
- with:
17
- node-version: 16
18
- - name: Jaid/action-sync-node-meta
19
- uses: jaid/action-sync-node-meta@v1.4.0
20
- with:
21
- direction: overwrite-github
22
- githubToken: "${{ secrets.GITHUB }}"
12
+ - name: Checkout repository
13
+ uses: actions/checkout@v7
14
+
15
+ - name: Sync package.json description to GitHub About
16
+ env:
17
+ GITHUB_TOKEN: ${{ secrets.GITHUB }}
18
+ run: |
19
+ DESC=$(jq -r '.description // empty' package.json)
20
+ if [ -n "$DESC" ]; then
21
+ echo "Updating repository description to: $DESC"
22
+ gh api -X PATCH repos/${{ github.repository }} -f description="$DESC"
23
+ else
24
+ echo "No description found in package.json."
25
+ fi
23
26
 
24
27
  release:
25
28
  runs-on: ubuntu-latest
26
29
  steps:
27
- - name: Checkout
28
- uses: actions/checkout@v4
30
+ - name: Checkout repository
31
+ uses: actions/checkout@v7
29
32
  with:
30
- fetch-depth: 0 # Required so semantic-release can trace git tags/history
33
+ fetch-depth: 0
34
+ token: ${{ secrets.GITHUB }} # Authenticates git CLI for tag/commit pushes
31
35
 
32
36
  - name: Setup Node.js
33
- uses: actions/setup-node@v4
37
+ uses: actions/setup-node@v7
34
38
  with:
35
- node-version: 22
39
+ node-version: '24'
36
40
 
37
41
  - name: Install Semantic Release & Plugins
38
- # Installs semantic-release and its plugins on the runner
39
42
  run: |
40
43
  npm install -g semantic-release \
41
44
  @semantic-release/changelog \
@@ -43,12 +46,11 @@ jobs:
43
46
  @semantic-release/github \
44
47
  @semantic-release/git
45
48
 
46
- - name: Run Semantic Release (Native)
49
+ - name: Run Semantic Release
47
50
  id: semantic
48
- # This will automatically pick up your export default config file in the repository root
49
51
  run: npx semantic-release
50
52
  env:
51
- GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
53
+ GITHUB_TOKEN: "${{ secrets.GITHUB }}"
52
54
  NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
53
55
 
54
56
  outputs:
@@ -1,4 +1,4 @@
1
- module.exports = {
1
+ export default {
2
2
  "organization_id": "",
3
3
  "key": "",
4
4
  "host": "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/plugins",
3
- "version": "1.4.2",
3
+ "version": "1.6.0",
4
4
  "description": "CoCreate plugins",
5
5
  "author": "CoCreate LLC",
6
6
  "license": "AGPL-3.0",
@@ -24,12 +24,13 @@
24
24
  "type": "GitHub Sponsors ❤",
25
25
  "url": "https://github.com/sponsors/CoCreate-app"
26
26
  },
27
+ "type": "module",
27
28
  "main": "./src/index.js",
28
29
  "dependencies": {
29
- "@cocreate/observer": "^1.20.0"
30
+ "@cocreate/observer": "^1.21.0"
30
31
  },
31
32
  "devDependencies": {
32
- "@cocreate/webpack": "^1.6.0"
33
+ "@cocreate/webpack": "^1.7.0"
33
34
  },
34
35
  "allowScripts": {
35
36
  "@cocreate/observer@1.19.0": true,
package/src/index.js CHANGED
@@ -1,236 +1,277 @@
1
- /********************************************************************************
2
- * Copyright (C) 2026 CoCreate and Contributors.
3
- *
4
- * This program is free software: you can redistribute it and/or modify
5
- * it under the terms of the GNU Affero General Public License as published
6
- * by the Free Software Foundation, either version 3 of the License, or
7
- * (at your option) any later version.
8
- *
9
- * This program is distributed in the hope that it will be useful,
10
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
- * GNU Affero General Public License for more details.
13
- *
14
- * You should have received a copy of the GNU Affero General Public License
15
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
- *
17
- ********************************************************************************/
18
-
19
- import Observer from "@cocreate/observer";
20
- import { processOperators } from "./operators.js";
21
-
22
- const plugins = {};
1
+ import Observer from '@cocreate/observer';
2
+ import { astAsync } from '@cocreate/utils';
3
+
23
4
  const scriptCache = new Map();
24
- const cssMarker = typeof document !== 'undefined' ? document.querySelector('link[plugins]') : null;
5
+ const cssMarker = typeof document !== 'undefined'
6
+ ? document.head.querySelector('meta[name="viewport"]') || document.head.lastChild
7
+ : null;
25
8
 
26
9
  /**
27
- * Initializes plugins on a collection of HTML elements.
28
- * @param {HTMLElement|HTMLElement[]|NodeList} elements
10
+ * Retrieves the global plugin configuration object from window.CoCreateConfig.
11
+ * @returns {Object}
29
12
  */
30
- export function init(elements) {
31
- if (!elements) return;
32
- const collection = (elements instanceof HTMLElement || elements instanceof Element)
33
- ? [elements]
34
- : Array.from(elements);
35
-
36
- collection.forEach(el => {
37
- if (el) {
38
- processPlugin(el);
13
+ function getPluginsConfig() {
14
+ if (typeof window !== 'undefined' && window.CoCreateConfig && window.CoCreateConfig.plugins) {
15
+ return window.CoCreateConfig.plugins;
16
+ }
17
+ return {};
18
+ }
19
+
20
+ /**
21
+ * Dynamically registers or updates a plugin definition in global config.
22
+ * @param {string} name - Plugin identifier
23
+ * @param {Object} config - Plugin configuration object (js, css, etc.)
24
+ */
25
+ export function registerPlugin(name, config) {
26
+ if (typeof window !== 'undefined') {
27
+ window.CoCreateConfig = window.CoCreateConfig || {};
28
+ window.CoCreateConfig.plugins = window.CoCreateConfig.plugins || {};
29
+ window.CoCreateConfig.plugins[name] = config;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Dynamic script loader to ensure plugin scripts are loaded once.
35
+ * Sets crossOrigin="anonymous" by default for simple string URLs.
36
+ * @param {string|Object} item - Script URL or object with configuration
37
+ * @returns {Promise<void>}
38
+ */
39
+ function loadScript(item) {
40
+ const src = typeof item === 'string' ? item : item?.src;
41
+ if (!src) return Promise.resolve();
42
+
43
+ if (scriptCache.has(src)) return scriptCache.get(src);
44
+
45
+ const promise = new Promise((resolve, reject) => {
46
+ const script = document.createElement('script');
47
+ script.src = src;
48
+ script.async = true;
49
+
50
+ // Default crossOrigin to "anonymous" unless explicitly set otherwise in an object
51
+ const crossOrigin = typeof item === 'object' && 'crossOrigin' in item ? item.crossOrigin : 'anonymous';
52
+ if (crossOrigin) {
53
+ script.crossOrigin = crossOrigin;
54
+ }
55
+
56
+ if (typeof item === 'object' && item.integrity) {
57
+ script.integrity = item.integrity;
58
+ }
59
+
60
+ script.onload = () => resolve();
61
+ script.onerror = () => {
62
+ scriptCache.delete(src);
63
+ reject(new Error(`Failed to load script: ${src}`));
64
+ };
65
+ document.head.appendChild(script);
66
+ });
67
+
68
+ scriptCache.set(src, promise);
69
+ return promise;
70
+ }
71
+
72
+ /**
73
+ * Dynamic CSS stylesheet loader for plugins.
74
+ * Sets crossOrigin="anonymous" by default for simple string URLs.
75
+ * @param {string|Object} item - Stylesheet URL or object with configuration
76
+ * @returns {Promise<void>}
77
+ */
78
+ function loadCSS(item) {
79
+ const href = typeof item === 'string' ? item : item?.href;
80
+ if (!href) return Promise.resolve();
81
+
82
+ return new Promise((resolve, reject) => {
83
+ if (document.querySelector(`link[href="${href}"]`)) {
84
+ return resolve();
85
+ }
86
+ const link = document.createElement('link');
87
+ link.rel = 'stylesheet';
88
+ link.href = href;
89
+
90
+ // Default crossOrigin to "anonymous" unless explicitly set otherwise in an object
91
+ const crossOrigin = typeof item === 'object' && 'crossOrigin' in item ? item.crossOrigin : 'anonymous';
92
+ if (crossOrigin) {
93
+ link.crossOrigin = crossOrigin;
94
+ }
95
+
96
+ if (typeof item === 'object' && item.integrity) {
97
+ link.integrity = item.integrity;
98
+ }
99
+
100
+ link.onload = () => resolve();
101
+ link.onerror = () => reject(new Error(`Failed to load stylesheet: ${href}`));
102
+
103
+ if (cssMarker && cssMarker.parentNode) {
104
+ cssMarker.parentNode.insertBefore(link, cssMarker);
105
+ } else {
106
+ document.head.appendChild(link);
39
107
  }
40
108
  });
41
109
  }
42
110
 
111
+ /**
112
+ * Preloads JS scripts and CSS stylesheets declared in global plugins config.
113
+ * Runs in parallel using Promise.allSettled.
114
+ * @param {Array<string>} pluginNames - Names of target plugins to preload
115
+ */
43
116
  async function preloadResources(pluginNames) {
117
+ const plugins = getPluginsConfig();
118
+ const promises = [];
119
+
44
120
  for (const name of pluginNames) {
45
- const pluginDef = plugins[name];
46
- if (pluginDef && !window[name]) {
47
- if (pluginDef.css) {
48
- pluginDef.css.forEach(href => {
49
- if (!document.querySelector(`link[href="${href}"]`)) {
50
- const link = document.createElement("link");
51
- link.rel = "stylesheet";
52
- link.href = href;
53
- if (cssMarker) cssMarker.parentNode.insertBefore(link, cssMarker);
54
- else document.head.appendChild(link);
55
- }
56
- });
121
+ const config = plugins[name];
122
+ if (config) {
123
+ if (config.css) {
124
+ const cssFiles = Array.isArray(config.css) ? config.css : [config.css];
125
+ cssFiles.forEach(href => promises.push(loadCSS(href)));
57
126
  }
58
-
59
- if (pluginDef.js) {
60
- for (const item of pluginDef.js) {
61
- const src = typeof item === 'string' ? item : item.src;
62
- if (!src) continue;
63
- if (!scriptCache.has(src)) {
64
- const existingScript = document.querySelector(`script[src*="${src}"]`);
65
- if (existingScript && (existingScript.dataset.loaded === "true" || window[name])) {
66
- scriptCache.set(src, Promise.resolve());
67
- } else {
68
- const s = document.createElement("script");
69
- s.src = src;
70
- scriptCache.set(src, new Promise((resolve) => {
71
- s.onload = () => { s.dataset.loaded = "true"; resolve(); };
72
- s.onerror = () => resolve();
73
- document.head.appendChild(s);
74
- }));
75
- }
76
- }
77
- await scriptCache.get(src);
78
- }
127
+ if (config.js) {
128
+ const jsFiles = Array.isArray(config.js) ? config.js : [config.js];
129
+ jsFiles.forEach(src => promises.push(loadScript(src)));
79
130
  }
80
131
  }
81
132
  }
133
+
134
+ const results = await Promise.allSettled(promises);
135
+ results.forEach(res => {
136
+ if (res.status === 'rejected') {
137
+ console.warn('[Plugin System]', res.reason.message || res.reason);
138
+ }
139
+ });
140
+ }
141
+
142
+ /**
143
+ * Helper to poll window for plugin library constructor if not immediately present.
144
+ * @param {string} pluginName - Target plugin identifier
145
+ * @param {number} retries - Number of retries
146
+ * @param {number} delay - Retry interval in ms
147
+ * @returns {Promise<Function|Object|null>}
148
+ */
149
+ async function waitForPluginOnWindow(pluginName, retries = 5, delay = 50) {
150
+ for (let i = 0; i < retries; i++) {
151
+ const lib = getPluginLibrary(pluginName);
152
+ if (lib) return lib;
153
+ await new Promise(res => setTimeout(res, delay));
154
+ }
155
+ return getPluginLibrary(pluginName);
82
156
  }
83
157
 
158
+ /**
159
+ * Helper to resolve and normalize plugin library constructor from window object.
160
+ * @param {string} pluginName - The target plugin identifier
161
+ * @returns {Function|Object|null}
162
+ */
163
+ function getPluginLibrary(pluginName) {
164
+ if (typeof window === 'undefined') return null;
165
+ if (window[pluginName]) return window[pluginName];
166
+
167
+ // Try capitalized variant (e.g. 'simpleBar' -> 'SimpleBar')
168
+ const capitalized = pluginName.charAt(0).toUpperCase() + pluginName.slice(1);
169
+ if (window[capitalized]) return window[capitalized];
170
+
171
+ return null;
172
+ }
173
+
174
+ /**
175
+ * Parses element 'plugin' attribute, executes AST configuration via astAsync,
176
+ * sets core library on window, and attaches instance onto element property.
177
+ * @param {Element} el - DOM element with plugin attribute
178
+ */
84
179
  async function processPlugin(el) {
85
- if (!el.attributes) return;
180
+ if (!el || !el.attributes) return;
86
181
 
87
- // Direct attributes collection map lookup to avoid binding/context prototype errors
88
182
  const pluginAttrNode = el.attributes['plugin'] || el.attributes.getNamedItem?.('plugin');
89
183
  const rawPluginAttr = pluginAttrNode?.value?.trim();
90
184
  if (!rawPluginAttr) return;
91
185
 
92
- const context = {
93
- element: el,
94
- this: el,
95
- plugin: null, // Tracks active sibling plugin instance
96
- data: { ...el.dataset },
97
- event: {},
98
- crud: window.CoCreate?.crud || null
99
- };
100
-
101
186
  try {
102
187
  let configs = [];
103
-
104
- // Unified Direct Parser: Pure JSON parsing with simple zero-argument fallback
188
+
105
189
  if (rawPluginAttr.startsWith('{') || rawPluginAttr.startsWith('[')) {
106
190
  const parsedConfig = JSON.parse(rawPluginAttr);
107
191
  configs = Array.isArray(parsedConfig) ? parsedConfig : [parsedConfig];
108
192
  } else {
109
- // Symmetrical Fallback: Plain strings mapped straight to constructor blocks
110
193
  configs = [{ [rawPluginAttr]: [] }];
111
194
  }
112
-
195
+
113
196
  const targetPlugins = new Set();
114
197
  configs.forEach(conf => {
115
198
  Object.keys(conf).forEach(k => {
116
- if (!k.startsWith('$')) {
199
+ if (!k.startsWith('\u0024')) {
117
200
  targetPlugins.add(k.split('.')[0]);
118
201
  }
119
202
  });
120
203
  });
121
-
204
+
122
205
  await preloadResources(Array.from(targetPlugins));
123
206
 
124
- // Execute each configuration block sequentially
125
207
  for (const configBlock of configs) {
126
- await executeConfigBlock(context, configBlock);
127
- }
128
- } catch (err) {
129
- console.error("[Plugin System] Failed to parse and execute plugin configuration:", err, rawPluginAttr);
130
- }
131
- }
208
+ for (const [pluginName, astValue] of Object.entries(configBlock)) {
209
+ // Poll window to ensure script execution has completed
210
+ const pluginLib = await waitForPluginOnWindow(pluginName);
132
211
 
133
- function instantiatePlugin(ConstructorFunc, argsArray) {
134
- if (typeof ConstructorFunc !== 'function') return ConstructorFunc;
135
-
136
- // Check standard ES6 class structure
137
- const isEs6Class = /^\s*class\s+/.test(ConstructorFunc.toString());
138
-
139
- if (isEs6Class) {
140
- return new ConstructorFunc(...argsArray);
141
- }
142
-
143
- try {
144
- // Fallback Step A: Attempt standard function invocation (e.g. particlesJS)
145
- return ConstructorFunc(...argsArray);
146
- } catch (err) {
147
- // Fallback Step B: If invocation throws constructor error (e.g. Babel transpiled ES5 classes)
148
- if (err instanceof TypeError) {
149
- return new ConstructorFunc(...argsArray);
150
- }
151
- throw err;
152
- }
153
- }
212
+ // Ensure plugin library definition is attached to window once loaded
213
+ if (pluginLib && typeof window !== 'undefined' && !window[pluginName]) {
214
+ window[pluginName] = pluginLib;
215
+ }
154
216
 
155
- async function executeConfigBlock(context, configBlock) {
156
- for (const [key, value] of Object.entries(configBlock)) {
157
- // 1. If key is an operator, process it natively via AST engine
158
- if (key.startsWith("$")) {
159
- await processOperators(context, { [key]: value });
160
- continue;
161
- }
217
+ // Format operator key with $ for AST engine recognition (e.g. SimpleBar -> $SimpleBar)
218
+ const operatorName = pluginName.startsWith('\u0024') ? pluginName : `$${pluginName}`;
162
219
 
163
- // 2. Intercept standard lifecycle methods to bind them directly as functions on the Element
164
- if (["getValue", "setValue", "read", "write"].includes(key)) {
165
- context.element[key] = async function(...args) {
166
- const localContext = {
167
- ...context,
168
- event: { ...context.event, value: args[0], arguments: args }
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;
169
231
  };
170
- return await processOperators(localContext, value);
171
- };
172
- continue;
173
- }
174
-
175
- const parts = key.split('.');
176
- const rootPluginName = parts[0];
177
232
 
178
- // 3. Resolve target from window object using dot notation
179
- let ConstructorFunc = window;
180
- for (const part of parts) {
181
- if (ConstructorFunc) {
182
- ConstructorFunc = ConstructorFunc[part];
183
- }
184
- }
185
-
186
- // 4. If it's a function, treat it as a constructor to initialize the plugin
187
- if (typeof ConstructorFunc === 'function' && !context.element[rootPluginName]) {
188
- let constructorArgs = [];
189
- if (value !== undefined) {
190
- const evaluatedArgs = await processOperators(context, value);
191
- constructorArgs = Array.isArray(evaluatedArgs) ? evaluatedArgs : [evaluatedArgs];
192
- }
233
+ // Provide operator wrapper in context for AST registry lookup
234
+ const context = {
235
+ [operatorName]: opWrapper
236
+ };
193
237
 
194
- // Create the plugin instance and bind it directly to the element under its root name
195
- const instance = instantiatePlugin(ConstructorFunc, constructorArgs);
196
- context.element[rootPluginName] = instance;
197
- context.plugin = instance; // Update context.plugin reference for sibling method chaining
198
- continue;
199
- }
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 }
243
+ });
200
244
 
201
- // 5. Explicit Instance Method Chaining (e.g. "FullCalendar.render")
202
- let methodTarget = context.element[rootPluginName];
203
- if (methodTarget && parts.length > 1) {
204
- let method = methodTarget;
205
- let parent = methodTarget;
206
- for (let i = 1; i < parts.length; i++) {
207
- parent = method;
208
- if (method) method = method[parts[i]];
209
- }
210
- if (typeof method === 'function') {
211
- let methodArgs = [];
212
- if (value !== undefined) {
213
- const evaluatedArgs = await processOperators(context, value);
214
- methodArgs = Array.isArray(evaluatedArgs) ? evaluatedArgs : [evaluatedArgs];
245
+ if (result !== undefined && !pluginName.startsWith('\u0024')) {
246
+ // Attach returned plugin instance directly onto element property
247
+ el[pluginName] = result;
215
248
  }
216
- await method.apply(parent, methodArgs);
217
- continue;
218
249
  }
219
250
  }
251
+ } catch (err) {
252
+ console.error('[Plugin System] Failed to parse and execute plugin configuration:', err, rawPluginAttr);
253
+ }
254
+ }
220
255
 
221
- // 6. Sibling Method Chaining (e.g. "render" directly on context.plugin)
222
- if (context.plugin && typeof context.plugin[key] === 'function') {
223
- let methodArgs = [];
224
- if (value !== undefined) {
225
- const evaluatedArgs = await processOperators(context, value);
226
- methodArgs = Array.isArray(evaluatedArgs) ? evaluatedArgs : [evaluatedArgs];
227
- }
228
- await context.plugin[key].apply(context.plugin, methodArgs);
229
- continue;
230
- }
256
+ /**
257
+ * Initializes plugins on single elements, collections, or selector query matches.
258
+ * @param {Element|NodeList|Array|string} elements - Target element(s) or DOM query selector
259
+ */
260
+ export function init(elements) {
261
+ if (!elements) return;
231
262
 
232
- // 7. Fallback: Process operators or generic actions on properties
233
- await processOperators(context, { [key]: value });
263
+ if (typeof elements === 'string') {
264
+ elements = document.querySelectorAll(elements);
265
+ }
266
+
267
+ if (elements instanceof Element) {
268
+ processPlugin(elements);
269
+ } else if (elements.length !== undefined || Array.isArray(elements)) {
270
+ Array.from(elements).forEach(el => {
271
+ if (el instanceof Element) {
272
+ processPlugin(el);
273
+ }
274
+ });
234
275
  }
235
276
  }
236
277
 
@@ -244,11 +285,7 @@ if (typeof document !== 'undefined') {
244
285
  callback: (mutation) => init(mutation.target)
245
286
  });
246
287
 
247
- import("./CoCreate.config.js")
248
- .then((Config) => {
249
- const data = Config.plugins || Config.default?.plugins || Config.default || {};
250
- Object.assign(plugins, data);
251
- })
252
- .catch(() => {})
253
- .finally(() => init(document.querySelectorAll(selector)));
254
- }
288
+ init(document.querySelectorAll(selector));
289
+ }
290
+
291
+ export default { init, registerPlugin };
package/src/operators.js DELETED
@@ -1,293 +0,0 @@
1
- /********************************************************************************
2
- * Copyright (C) 2026 CoCreate and Contributors.
3
- *
4
- * This program is free software: you can redistribute it and/or modify
5
- * it under the terms of the GNU Affero General Public License as published
6
- * by the Free Software Foundation, either version 3 of the License, or
7
- * (at your option) any later version.
8
- *
9
- * This program is distributed in the hope that it will be useful,
10
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
- * GNU Affero General Public License for more details.
13
- *
14
- * You should have received a copy of the GNU Affero General Public License
15
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
- *
17
- ********************************************************************************/
18
-
19
- import { getValueFromObject, queryElements } from "@cocreate/utils";
20
-
21
- const IsomorphicGlobals = {
22
- Math,
23
- Number,
24
- String,
25
- Boolean,
26
- Date,
27
- JSON,
28
- Array,
29
- Object,
30
- parseInt,
31
- parseFloat,
32
- encodeURIComponent,
33
- decodeURIComponent,
34
- btoa: (str) => {
35
- if (typeof btoa === 'function') return btoa(String(str));
36
- return Buffer.from(String(str)).toString("base64");
37
- },
38
- atob: (b64) => {
39
- if (typeof atob === 'function') return atob(b64);
40
- return Buffer.from(String(b64), "base64").toString("utf-8");
41
- },
42
- concat: (...args) => args.flat().map(arg => typeof arg === 'object' ? JSON.stringify(arg) : arg).join(''),
43
-
44
- // Core Asynchronous Flow Control Operators
45
- await: {
46
- all: async (promises) => Promise.all(promises),
47
- any: async (promises) => Promise.any(promises),
48
- race: async (promises) => Promise.race(promises),
49
- allSettled: async (promises) => Promise.allSettled(promises),
50
- delay: (ms) => new Promise(resolve => setTimeout(resolve, Number(ms) || 0))
51
- }
52
- };
53
-
54
- const BrowserGlobals = {
55
- get $organization_id() { return typeof localStorage !== 'undefined' ? localStorage.getItem("organization_id") : ""; },
56
- get $user_id() { return typeof localStorage !== 'undefined' ? localStorage.getItem("user_id") : ""; },
57
- get $client_id() { return typeof localStorage !== 'undefined' ? localStorage.getItem("clientId") : ""; },
58
- get $session_id() { return typeof localStorage !== 'undefined' ? localStorage.getItem("session_id") : ""; },
59
- get $innerWidth() { return typeof window !== 'undefined' ? window.innerWidth : 0; },
60
- get $innerHeight() { return typeof window !== 'undefined' ? window.innerHeight : 0; },
61
- get $href() { return typeof window !== 'undefined' ? window.location.href.replace(/\/$/, "") : ""; },
62
- get $origin() { return typeof window !== 'undefined' ? window.location.origin : ""; },
63
- get $protocol() { return typeof window !== 'undefined' ? window.location.protocol : ""; },
64
- get $hostname() { return typeof window !== 'undefined' ? window.location.hostname : ""; },
65
- get $host() { return typeof window !== 'undefined' ? window.location.host : ""; },
66
- get $port() { return typeof window !== 'undefined' ? window.location.port : ""; },
67
- get $pathname() { return typeof window !== 'undefined' ? window.location.pathname.replace(/\/$/, "") : ""; },
68
- get $hash() { return typeof window !== 'undefined' ? window.location.hash : ""; },
69
- get $subdomain() {
70
- if (typeof window === 'undefined') return "";
71
- const parts = window.location.hostname.split('.');
72
- return parts.length > 2 ? parts[0] : "";
73
- },
74
- get $relativePath() {
75
- if (typeof window === 'undefined') return "./";
76
- const currentPath = window.location.pathname.replace(/\/[^\/]*$/, "");
77
- const depth = currentPath.split("/").filter(Boolean).length;
78
- return depth > 0 ? "../".repeat(depth) : "./";
79
- },
80
- get $path() {
81
- if (typeof window === 'undefined') return "";
82
- let path = window.location.pathname;
83
- if (path.split("/").pop().includes(".")) path = path.replace(/\/[^\/]+$/, "/");
84
- return path === "/" ? "" : path;
85
- },
86
- $true: () => true,
87
- $false: () => false,
88
- $parse: (value = "") => {
89
- try { return JSON.parse(value); } catch (e) { return value; }
90
- },
91
- $query: function(selector) {
92
- if (typeof document === 'undefined') return undefined;
93
- let element = this?.element || null;
94
- if (typeof selector === 'string') {
95
- selector = selector.trim().replace(/^['"]|['"]$/g, '');
96
- }
97
- try {
98
- const results = queryElements({ element, selector });
99
- return results && results.length > 0 ? results[0] : undefined;
100
- } catch (error) {
101
- console.warn(`[AST Engine] Invalid $query selector => "${selector}"`, error);
102
- return undefined;
103
- }
104
- },
105
- $queryAll: function(selector) {
106
- if (typeof document === 'undefined') return [];
107
- let element = this?.element || null;
108
- if (typeof selector === 'string') {
109
- selector = selector.trim().replace(/^['"]|['"]$/g, '');
110
- if (selector.includes('$document')) return [];
111
- }
112
- try {
113
- const results = queryElements({ element, selector });
114
- return results ? Array.from(results) : [];
115
- } catch (error) {
116
- console.warn(`[AST Engine] Invalid $queryAll selector => "${selector}"`, error);
117
- return [];
118
- }
119
- }
120
- };
121
-
122
- const getSandboxScope = (context) => {
123
- const isBrowser = typeof window !== 'undefined';
124
- return {
125
- ...IsomorphicGlobals,
126
- ...(isBrowser ? BrowserGlobals : {}),
127
- ...context
128
- };
129
- };
130
-
131
- /**
132
- * Main recursive traversal loop of the AST Compiler.
133
- * Analyzes object structures from the inside-out to resolve nested expression boundaries.
134
- */
135
- export async function processOperators(context, execute) {
136
- if (execute instanceof Promise) {
137
- execute = await execute;
138
- }
139
-
140
- if (execute === null || execute === undefined) {
141
- return execute;
142
- }
143
-
144
- if (Array.isArray(execute)) {
145
- const evaluatedArray = [];
146
- for (let i = 0; i < execute.length; i++) {
147
- evaluatedArray[i] = await processOperators(context, execute[i]);
148
- }
149
- return evaluatedArray;
150
- } else if (typeof execute === "object") {
151
- const keys = Object.keys(execute);
152
-
153
- // Match operators starting with "$" except raw database storage parameters
154
- const operatorKey = keys.find(k => k.startsWith("$") && !["$array", "$filter", "$storage", "$database"].includes(k));
155
-
156
- if (operatorKey && keys.length === 1) {
157
- return await executeOperator(context, operatorKey, execute[operatorKey]);
158
- }
159
-
160
- const evaluatedObject = {};
161
- for (const key of keys) {
162
- let finalKey = key;
163
-
164
- if (key.startsWith("$") && !["$array", "$filter", "$storage", "$database"].includes(key)) {
165
- const resolvedKey = await executeOperator(context, key);
166
- if (typeof resolvedKey === "string" || typeof resolvedKey === "number") {
167
- finalKey = String(resolvedKey);
168
- }
169
- }
170
-
171
- let finalValue = execute[key];
172
- if (typeof finalValue === "string" && finalValue.startsWith("$") && !["$array", "$filter", "$storage", "$database"].includes(finalValue)) {
173
- finalValue = await executeOperator(context, finalValue);
174
- } else if (Array.isArray(finalValue) || (typeof finalValue === "object" && finalValue !== null)) {
175
- finalValue = await processOperators(context, finalValue);
176
- }
177
-
178
- evaluatedObject[finalKey] = finalValue;
179
- }
180
-
181
- return evaluatedObject;
182
- } else if (typeof execute === "string" && execute.startsWith("$") && !["$array", "$filter", "$storage", "$database"].includes(execute)) {
183
- return await executeOperator(context, execute);
184
- }
185
-
186
- return execute;
187
- }
188
-
189
- async function executeOperator(context, operator, args) {
190
- const localContext = {
191
- state: {},
192
- ...context
193
- };
194
-
195
- // Evaluate core context property mappings
196
- if (args === undefined) {
197
- if (operator.startsWith("$state.")) {
198
- return getValueFromObject(localContext.state, operator.substring(7));
199
- } else if (operator === "$state") {
200
- return localContext.state;
201
- } else if (operator.startsWith("$data.")) {
202
- return getValueFromObject(localContext.data, operator.substring(6));
203
- } else if (operator === "$data") {
204
- return localContext.data;
205
- } else if (operator.startsWith("$event.")) {
206
- return getValueFromObject(localContext.event, operator.substring(7));
207
- } else if (operator === "$event") {
208
- return localContext.event;
209
- } else if (operator.startsWith("$request.")) {
210
- return getValueFromObject(localContext.request, operator.substring(9));
211
- } else if (operator === "$request") {
212
- return localContext.request;
213
- } else if (operator.startsWith("$header.")) {
214
- const headerKey = operator.substring(8).toLowerCase();
215
- return localContext.request?.headers?.[headerKey];
216
- } else if (operator === "$rawBody") {
217
- return localContext.rawBody;
218
- } else if (operator === "$this" || operator === "$element") {
219
- return localContext.element;
220
- } else if (operator === "$plugin" || operator === "$instance") {
221
- return localContext.plugin;
222
- } else if (operator === "$value") {
223
- const el = localContext.element;
224
- return el && typeof el.getValue === 'function' ? el.getValue() : "";
225
- }
226
- }
227
-
228
- let isExplicitNew = false;
229
- let cleanPath = operator.startsWith("$") ? operator.substring(1) : operator;
230
-
231
- if (cleanPath.startsWith("new.")) {
232
- isExplicitNew = true;
233
- cleanPath = cleanPath.substring(4);
234
- }
235
-
236
- const parts = cleanPath.split('.');
237
- const SandboxScope = getSandboxScope(localContext);
238
-
239
- let target = SandboxScope[parts[0]];
240
- let parentContext = SandboxScope;
241
-
242
- if (target === undefined && localContext.element) {
243
- target = localContext.element[parts[0]];
244
- parentContext = localContext.element;
245
- }
246
-
247
- if (target !== undefined) {
248
- for (let i = 1; i < parts.length; i++) {
249
- parentContext = target;
250
- target = target[parts[i]];
251
- if (target === undefined) break;
252
- }
253
-
254
- if (typeof target === 'function') {
255
- let argsArray = [];
256
- if (args !== undefined) {
257
- if (typeof args === 'object' && args !== null && (args.$args !== undefined || args.$arguments !== undefined)) {
258
- const explicitArgs = args.$args !== undefined ? args.$args : args.$arguments;
259
-
260
- if (Array.isArray(explicitArgs)) {
261
- argsArray = [];
262
- for (let i = 0; i < explicitArgs.length; i++) {
263
- argsArray.push(await processOperators(localContext, explicitArgs[i]));
264
- }
265
- } else {
266
- const evaluatedSingle = await processOperators(localContext, explicitArgs);
267
- argsArray = [evaluatedSingle];
268
- }
269
- } else {
270
- const evaluatedSingle = await processOperators(localContext, args);
271
- argsArray = [evaluatedSingle];
272
- }
273
- }
274
-
275
- const isEs6Class = /^\s*class\s+/.test(target.toString());
276
-
277
- if (isExplicitNew || isEs6Class) {
278
- return new target(...argsArray);
279
- }
280
-
281
- return await target.apply(parentContext, argsArray);
282
- } else if (target !== undefined) {
283
- return target;
284
- }
285
- }
286
-
287
- return operator;
288
- }
289
-
290
- export { processOperators as processOperatorsAsync };
291
- export const customOperators = {};
292
-
293
- export default processOperators;