@cocreate/plugins 1.8.0 → 2.0.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/.github/FUNDING.yml +1 -1
- package/.github/workflows/automated.yml +18 -7
- package/CHANGELOG.md +73 -38
- package/CONTRIBUTING.md +6 -6
- package/README.md +444 -92
- package/docs/index.html +4 -4
- package/package.json +9 -8
- package/src/index.js +144 -65
- package/test/result-storage.test.js +212 -0
- package/webpack.config.js +10 -6
- /package/{release.config.js → release.config.cjs} +0 -0
package/src/index.js
CHANGED
|
@@ -2,13 +2,18 @@ import Observer from '@cocreate/observer';
|
|
|
2
2
|
import { astAsync } from '@cocreate/utils';
|
|
3
3
|
|
|
4
4
|
const scriptCache = new Map();
|
|
5
|
-
const
|
|
5
|
+
const pendingElements = new WeakSet();
|
|
6
|
+
const initializedPlugins = new WeakMap();
|
|
7
|
+
const initializingPlugins = new WeakMap();
|
|
8
|
+
const cssMarker = typeof document !== 'undefined'
|
|
6
9
|
? document.head.querySelector('meta[name="viewport"]') || document.head.lastChild
|
|
7
10
|
: null;
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* Retrieves the global plugin configuration object from window.CoCreateConfig.
|
|
11
14
|
* @returns {Object}
|
|
15
|
+
* @private
|
|
16
|
+
* @environment client
|
|
12
17
|
*/
|
|
13
18
|
function getPluginsConfig() {
|
|
14
19
|
if (typeof window !== 'undefined' && window.CoCreateConfig && window.CoCreateConfig.plugins) {
|
|
@@ -21,6 +26,8 @@ function getPluginsConfig() {
|
|
|
21
26
|
* Dynamically registers or updates a plugin definition in global config.
|
|
22
27
|
* @param {string} name - Plugin identifier
|
|
23
28
|
* @param {Object} config - Plugin configuration object (js, css, etc.)
|
|
29
|
+
* @public
|
|
30
|
+
* @environment client
|
|
24
31
|
*/
|
|
25
32
|
export function registerPlugin(name, config) {
|
|
26
33
|
if (typeof window !== 'undefined') {
|
|
@@ -35,6 +42,8 @@ export function registerPlugin(name, config) {
|
|
|
35
42
|
* Sets crossOrigin="anonymous" by default for simple string URLs.
|
|
36
43
|
* @param {string|Object} item - Script URL or object with configuration
|
|
37
44
|
* @returns {Promise<void>}
|
|
45
|
+
* @private
|
|
46
|
+
* @environment client
|
|
38
47
|
*/
|
|
39
48
|
function loadScript(item) {
|
|
40
49
|
const src = typeof item === 'string' ? item : item?.src;
|
|
@@ -74,6 +83,8 @@ function loadScript(item) {
|
|
|
74
83
|
* Sets crossOrigin="anonymous" by default for simple string URLs.
|
|
75
84
|
* @param {string|Object} item - Stylesheet URL or object with configuration
|
|
76
85
|
* @returns {Promise<void>}
|
|
86
|
+
* @private
|
|
87
|
+
* @environment client
|
|
77
88
|
*/
|
|
78
89
|
function loadCSS(item) {
|
|
79
90
|
const href = typeof item === 'string' ? item : item?.href;
|
|
@@ -110,8 +121,11 @@ function loadCSS(item) {
|
|
|
110
121
|
|
|
111
122
|
/**
|
|
112
123
|
* Preloads JS scripts and CSS stylesheets declared in global plugins config.
|
|
113
|
-
*
|
|
124
|
+
* Loads each plugin's JavaScript resources in declared order while allowing
|
|
125
|
+
* independent plugins and stylesheets to load in parallel.
|
|
114
126
|
* @param {Array<string>} pluginNames - Names of target plugins to preload
|
|
127
|
+
* @private
|
|
128
|
+
* @environment client
|
|
115
129
|
*/
|
|
116
130
|
async function preloadResources(pluginNames) {
|
|
117
131
|
const plugins = getPluginsConfig();
|
|
@@ -126,7 +140,11 @@ async function preloadResources(pluginNames) {
|
|
|
126
140
|
}
|
|
127
141
|
if (config.js) {
|
|
128
142
|
const jsFiles = Array.isArray(config.js) ? config.js : [config.js];
|
|
129
|
-
|
|
143
|
+
promises.push((async () => {
|
|
144
|
+
for (const src of jsFiles) {
|
|
145
|
+
await loadScript(src);
|
|
146
|
+
}
|
|
147
|
+
})());
|
|
130
148
|
}
|
|
131
149
|
}
|
|
132
150
|
}
|
|
@@ -145,6 +163,8 @@ async function preloadResources(pluginNames) {
|
|
|
145
163
|
* @param {number} retries - Number of retries
|
|
146
164
|
* @param {number} delay - Retry interval in ms
|
|
147
165
|
* @returns {Promise<Function|Object|null>}
|
|
166
|
+
* @private
|
|
167
|
+
* @environment client
|
|
148
168
|
*/
|
|
149
169
|
async function waitForPluginOnWindow(pluginName, retries = 5, delay = 50) {
|
|
150
170
|
for (let i = 0; i < retries; i++) {
|
|
@@ -159,6 +179,8 @@ async function waitForPluginOnWindow(pluginName, retries = 5, delay = 50) {
|
|
|
159
179
|
* Helper to resolve and normalize plugin library constructor from window object.
|
|
160
180
|
* @param {string} pluginName - The target plugin identifier
|
|
161
181
|
* @returns {Function|Object|null}
|
|
182
|
+
* @private
|
|
183
|
+
* @environment client
|
|
162
184
|
*/
|
|
163
185
|
function getPluginLibrary(pluginName) {
|
|
164
186
|
if (typeof window === 'undefined') return null;
|
|
@@ -171,18 +193,53 @@ function getPluginLibrary(pluginName) {
|
|
|
171
193
|
return null;
|
|
172
194
|
}
|
|
173
195
|
|
|
196
|
+
function getPluginNames(configBlock) {
|
|
197
|
+
const configuredPlugins = getPluginsConfig();
|
|
198
|
+
const pluginNames = new Set();
|
|
199
|
+
|
|
200
|
+
function collect(value) {
|
|
201
|
+
if (!value || typeof value !== 'object') return;
|
|
202
|
+
|
|
203
|
+
for (const [key, childValue] of Object.entries(value)) {
|
|
204
|
+
const name = key.replace(/^\$/, '').split('.')[0];
|
|
205
|
+
if (configuredPlugins[name] || getPluginLibrary(name)) {
|
|
206
|
+
pluginNames.add(name);
|
|
207
|
+
}
|
|
208
|
+
collect(childValue);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
collect(configBlock);
|
|
213
|
+
|
|
214
|
+
return pluginNames;
|
|
215
|
+
}
|
|
216
|
+
|
|
174
217
|
/**
|
|
175
218
|
* Parses element 'plugin' attribute, executes AST configuration via astAsync,
|
|
176
|
-
*
|
|
219
|
+
* with result storage controlled explicitly by the AST configuration.
|
|
177
220
|
* @param {Element} el - DOM element with plugin attribute
|
|
221
|
+
* @private
|
|
222
|
+
* @environment client
|
|
178
223
|
*/
|
|
179
|
-
|
|
180
|
-
if (!el || !el.attributes) return;
|
|
224
|
+
function processPlugin(el) {
|
|
225
|
+
if (!el || !el.attributes) return Promise.resolve();
|
|
181
226
|
|
|
182
227
|
const pluginAttrNode = el.attributes['plugin'] || el.attributes.getNamedItem?.('plugin');
|
|
183
228
|
const rawPluginAttr = pluginAttrNode?.value?.trim();
|
|
184
|
-
if (!rawPluginAttr) return;
|
|
229
|
+
if (!rawPluginAttr) return Promise.resolve();
|
|
230
|
+
|
|
231
|
+
const pending = initializingPlugins.get(el);
|
|
232
|
+
if (pending?.value === rawPluginAttr) return pending.promise;
|
|
233
|
+
if (initializedPlugins.get(el) === rawPluginAttr) return Promise.resolve();
|
|
234
|
+
|
|
235
|
+
const promise = initializePlugin(el, rawPluginAttr).finally(() => {
|
|
236
|
+
if (initializingPlugins.get(el)?.promise === promise) initializingPlugins.delete(el);
|
|
237
|
+
});
|
|
238
|
+
initializingPlugins.set(el, { value: rawPluginAttr, promise });
|
|
239
|
+
return promise;
|
|
240
|
+
}
|
|
185
241
|
|
|
242
|
+
async function initializePlugin(el, rawPluginAttr) {
|
|
186
243
|
try {
|
|
187
244
|
let configs = [];
|
|
188
245
|
|
|
@@ -190,65 +247,41 @@ async function processPlugin(el) {
|
|
|
190
247
|
const parsedConfig = JSON.parse(rawPluginAttr);
|
|
191
248
|
configs = Array.isArray(parsedConfig) ? parsedConfig : [parsedConfig];
|
|
192
249
|
} else {
|
|
193
|
-
configs = [{ [rawPluginAttr]: [] }];
|
|
250
|
+
configs = [{ ['$' + rawPluginAttr.replace(/^\$/, '')]: [] }];
|
|
194
251
|
}
|
|
195
252
|
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
});
|
|
253
|
+
const configPlans = configs.map(configBlock => ({
|
|
254
|
+
configBlock,
|
|
255
|
+
pluginNames: getPluginNames(configBlock)
|
|
256
|
+
}));
|
|
257
|
+
const targetPlugins = new Set(
|
|
258
|
+
configPlans.flatMap(({ pluginNames }) => Array.from(pluginNames))
|
|
259
|
+
);
|
|
204
260
|
|
|
205
261
|
await preloadResources(Array.from(targetPlugins));
|
|
206
262
|
|
|
207
|
-
for (const configBlock of
|
|
208
|
-
|
|
209
|
-
// Poll window to ensure script execution has completed
|
|
210
|
-
const pluginLib = await waitForPluginOnWindow(pluginName);
|
|
211
|
-
|
|
212
|
-
// Ensure plugin library definition is attached to window once loaded
|
|
213
|
-
if (pluginLib && typeof window !== 'undefined' && !window[pluginName]) {
|
|
214
|
-
window[pluginName] = pluginLib;
|
|
215
|
-
}
|
|
263
|
+
for (const { configBlock, pluginNames } of configPlans) {
|
|
264
|
+
const libraries = {};
|
|
216
265
|
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
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 }
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
if (result !== undefined && !pluginName.startsWith('\u0024')) {
|
|
246
|
-
// Attach returned plugin instance directly onto element property
|
|
247
|
-
el[pluginName] = result;
|
|
266
|
+
for (const pluginName of pluginNames) {
|
|
267
|
+
const library = await waitForPluginOnWindow(pluginName);
|
|
268
|
+
if (!library) {
|
|
269
|
+
throw new Error(`Plugin '${pluginName}' is not available on window.`);
|
|
248
270
|
}
|
|
271
|
+
libraries['$' + pluginName] = library;
|
|
249
272
|
}
|
|
273
|
+
|
|
274
|
+
const scope = { ...libraries, $this: el };
|
|
275
|
+
|
|
276
|
+
await astAsync({
|
|
277
|
+
executeAST: configBlock,
|
|
278
|
+
sandbox: scope,
|
|
279
|
+
node: el
|
|
280
|
+
});
|
|
250
281
|
}
|
|
282
|
+
initializedPlugins.set(el, rawPluginAttr);
|
|
251
283
|
} catch (err) {
|
|
284
|
+
initializedPlugins.delete(el);
|
|
252
285
|
console.error('[Plugin System] Failed to parse and execute plugin configuration:', err, rawPluginAttr);
|
|
253
286
|
}
|
|
254
287
|
}
|
|
@@ -256,23 +289,37 @@ async function processPlugin(el) {
|
|
|
256
289
|
/**
|
|
257
290
|
* Initializes plugins on single elements, collections, or selector query matches.
|
|
258
291
|
* @param {Element|NodeList|Array|string} elements - Target element(s) or DOM query selector
|
|
292
|
+
* @public
|
|
293
|
+
* @environment client
|
|
259
294
|
*/
|
|
260
295
|
export function init(elements) {
|
|
261
|
-
if (!elements) return;
|
|
296
|
+
if (!elements) return Promise.resolve([]);
|
|
262
297
|
|
|
263
298
|
if (typeof elements === 'string') {
|
|
264
299
|
elements = document.querySelectorAll(elements);
|
|
265
300
|
}
|
|
266
301
|
|
|
267
302
|
if (elements instanceof Element) {
|
|
268
|
-
processPlugin(elements);
|
|
303
|
+
return processPlugin(elements);
|
|
269
304
|
} else if (elements.length !== undefined || Array.isArray(elements)) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
305
|
+
return Promise.all(
|
|
306
|
+
Array.from(elements)
|
|
307
|
+
.filter(el => el instanceof Element)
|
|
308
|
+
.map(el => processPlugin(el))
|
|
309
|
+
);
|
|
275
310
|
}
|
|
311
|
+
|
|
312
|
+
return Promise.resolve([]);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function scheduleInit(element) {
|
|
316
|
+
if (!element || pendingElements.has(element)) return;
|
|
317
|
+
|
|
318
|
+
pendingElements.add(element);
|
|
319
|
+
requestAnimationFrame(() => {
|
|
320
|
+
pendingElements.delete(element);
|
|
321
|
+
if (element.isConnected) init(element);
|
|
322
|
+
});
|
|
276
323
|
}
|
|
277
324
|
|
|
278
325
|
if (typeof document !== 'undefined') {
|
|
@@ -282,10 +329,42 @@ if (typeof document !== 'undefined') {
|
|
|
282
329
|
types: ["addedNodes", "attributes"],
|
|
283
330
|
selector: selector,
|
|
284
331
|
attributeFilter: ["plugin"],
|
|
285
|
-
callback: (mutation) =>
|
|
332
|
+
callback: (mutation) => {
|
|
333
|
+
scheduleInit(mutation.target);
|
|
334
|
+
}
|
|
286
335
|
});
|
|
287
336
|
|
|
288
337
|
init(document.querySelectorAll(selector));
|
|
289
338
|
}
|
|
290
339
|
|
|
291
|
-
export default { init, registerPlugin };
|
|
340
|
+
export default { init, registerPlugin };
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
*
|
|
344
|
+
* @name plugin
|
|
345
|
+
* @kind member
|
|
346
|
+
* @attribute
|
|
347
|
+
* @environment client
|
|
348
|
+
*/
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* CoCreate plugins
|
|
352
|
+
*
|
|
353
|
+
* @module plugins
|
|
354
|
+
* @environment client
|
|
355
|
+
*/
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Declarative selectors recognized by this module.
|
|
359
|
+
*
|
|
360
|
+
* @name declarative-selectors
|
|
361
|
+
* @kind member
|
|
362
|
+
* @selectors
|
|
363
|
+
* @environment client
|
|
364
|
+
* @example
|
|
365
|
+
* ```json
|
|
366
|
+
* [
|
|
367
|
+
* "[plugin]"
|
|
368
|
+
* ]
|
|
369
|
+
* ```
|
|
370
|
+
*/
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import vm from 'node:vm';
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import { astAsync } from '@cocreate/utils';
|
|
6
|
+
|
|
7
|
+
const code = fs.readFileSync(new URL('../src/index.js', import.meta.url), 'utf8')
|
|
8
|
+
.replace(/^import .*;\r?\n/gm, '')
|
|
9
|
+
.replace(/^export /gm, '')
|
|
10
|
+
.replace('default { init, registerPlugin };', 'globalThis.pluginInit = init;');
|
|
11
|
+
|
|
12
|
+
class Element {
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.attributes = { plugin: { value: typeof config === 'string' ? config : JSON.stringify(config) } };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function runtime(libraries, evaluate = astAsync) {
|
|
19
|
+
const errors = [];
|
|
20
|
+
const context = {
|
|
21
|
+
astAsync: evaluate, Element,
|
|
22
|
+
window: { ...libraries, Object },
|
|
23
|
+
console: { error: (...args) => errors.push(args), warn: (...args) => errors.push(args) }
|
|
24
|
+
};
|
|
25
|
+
vm.runInNewContext(code, context);
|
|
26
|
+
return {
|
|
27
|
+
async init(element) {
|
|
28
|
+
await context.pluginInit(element);
|
|
29
|
+
assert.deepEqual(errors, []);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test('bare plugin initialization does not attach its returned instance', async () => {
|
|
35
|
+
let calls = 0;
|
|
36
|
+
class ExamplePlugin {
|
|
37
|
+
constructor() { calls++; }
|
|
38
|
+
}
|
|
39
|
+
const app = runtime({ ExamplePlugin });
|
|
40
|
+
const el = new Element('ExamplePlugin');
|
|
41
|
+
await app.init(el);
|
|
42
|
+
await app.init(el);
|
|
43
|
+
assert.equal(calls, 1);
|
|
44
|
+
assert.equal(Object.hasOwn(el, 'ExamplePlugin'), false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('nested constructors and $return results do not attach automatically', async () => {
|
|
48
|
+
class Widget {}
|
|
49
|
+
const app = runtime({ Library: { Widget } });
|
|
50
|
+
for (const config of [
|
|
51
|
+
{ '$Library.Widget': [] },
|
|
52
|
+
{ $return: { '$Library.Widget': [] } }
|
|
53
|
+
]) {
|
|
54
|
+
const el = new Element(config);
|
|
55
|
+
await app.init(el);
|
|
56
|
+
assert.equal(Object.hasOwn(el, 'Library'), false);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('AST can explicitly store a returned instance on $this under a chosen key', async () => {
|
|
61
|
+
class ExamplePlugin {}
|
|
62
|
+
const app = runtime({ ExamplePlugin });
|
|
63
|
+
const el = new Element({
|
|
64
|
+
'$this.customInstance': { $ExamplePlugin: [] }
|
|
65
|
+
});
|
|
66
|
+
await app.init(el);
|
|
67
|
+
assert.ok(el.customInstance instanceof ExamplePlugin);
|
|
68
|
+
assert.equal(Object.hasOwn(el, 'ExamplePlugin'), false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('AST can store results elsewhere and call methods on explicit references', async () => {
|
|
72
|
+
class ExamplePlugin {
|
|
73
|
+
render() { this.rendered = true; }
|
|
74
|
+
}
|
|
75
|
+
const destination = {};
|
|
76
|
+
const app = runtime({ ExamplePlugin, destination });
|
|
77
|
+
const el = new Element({
|
|
78
|
+
instance: { $ExamplePlugin: [] },
|
|
79
|
+
'$instance.render': [],
|
|
80
|
+
$return: { '$destination': { '$Object.assign': ['$destination', { instance: '$instance' }] } }
|
|
81
|
+
});
|
|
82
|
+
await app.init(el);
|
|
83
|
+
assert.ok(destination.instance instanceof ExamplePlugin);
|
|
84
|
+
assert.equal(destination.instance.rendered, true);
|
|
85
|
+
assert.equal(Object.hasOwn(el, 'ExamplePlugin'), false);
|
|
86
|
+
assert.equal(Object.hasOwn(el, 'instance'), false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('subsequent operations keep referencing a global library after a call returns a value', async () => {
|
|
90
|
+
let calls = 0;
|
|
91
|
+
function GlobalPlugin() { return {}; }
|
|
92
|
+
GlobalPlugin.highlightAll = () => { calls++; };
|
|
93
|
+
const app = runtime({ GlobalPlugin });
|
|
94
|
+
const el = new Element({ '$GlobalPlugin': [], '$GlobalPlugin.highlightAll': [] });
|
|
95
|
+
await app.init(el);
|
|
96
|
+
assert.equal(calls, 1);
|
|
97
|
+
assert.equal(Object.hasOwn(el, 'GlobalPlugin'), false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('passes the entire unchanged configuration to AST once with the host and library scope', async () => {
|
|
101
|
+
const config = {
|
|
102
|
+
created: { $ExamplePlugin: ['$this'] },
|
|
103
|
+
'$this.instance': '$created',
|
|
104
|
+
'$this.instance.render': []
|
|
105
|
+
};
|
|
106
|
+
class ExamplePlugin {
|
|
107
|
+
constructor(host) { this.host = host; }
|
|
108
|
+
render() { this.rendered = true; }
|
|
109
|
+
}
|
|
110
|
+
const calls = [];
|
|
111
|
+
const app = runtime({ ExamplePlugin }, async options => {
|
|
112
|
+
calls.push(options);
|
|
113
|
+
return astAsync(options);
|
|
114
|
+
});
|
|
115
|
+
const el = new Element(config);
|
|
116
|
+
await app.init(el);
|
|
117
|
+
assert.equal(calls.length, 1);
|
|
118
|
+
assert.deepEqual(JSON.parse(JSON.stringify(calls[0].executeAST)), config);
|
|
119
|
+
assert.equal(calls[0].sandbox.$this, el);
|
|
120
|
+
assert.equal(calls[0].sandbox.$ExamplePlugin, ExamplePlugin);
|
|
121
|
+
assert.equal(calls[0].node, el);
|
|
122
|
+
assert.ok(el.instance instanceof ExamplePlugin);
|
|
123
|
+
assert.equal(el.instance.host, el);
|
|
124
|
+
assert.equal(el.instance.rendered, true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('plain keys store data rather than being rewritten into plugin calls', async () => {
|
|
128
|
+
let calls = 0;
|
|
129
|
+
function ExamplePlugin() { calls++; }
|
|
130
|
+
const app = runtime({ ExamplePlugin });
|
|
131
|
+
const el = new Element({
|
|
132
|
+
ExamplePlugin: ['configuration data'],
|
|
133
|
+
'$this.saved': '$ExamplePlugin'
|
|
134
|
+
});
|
|
135
|
+
await app.init(el);
|
|
136
|
+
assert.equal(calls, 0);
|
|
137
|
+
assert.equal(el.saved, ExamplePlugin);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('configuration blocks execute in order without special treatment for $return', async () => {
|
|
141
|
+
const order = [];
|
|
142
|
+
function ExamplePlugin(value) { order.push(value); return value; }
|
|
143
|
+
const app = runtime({ ExamplePlugin });
|
|
144
|
+
const el = new Element([
|
|
145
|
+
{ '$this.first': { $ExamplePlugin: ['first'] } },
|
|
146
|
+
{ $return: { '$this.second': { $ExamplePlugin: ['$this.first'] } } }
|
|
147
|
+
]);
|
|
148
|
+
await app.init(el);
|
|
149
|
+
assert.deepEqual(order, ['first', 'first']);
|
|
150
|
+
assert.equal(el.first, 'first');
|
|
151
|
+
assert.equal(el.second, 'first');
|
|
152
|
+
assert.equal(Object.hasOwn(el, 'ExamplePlugin'), false);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('concurrent initialization calls await the same pending setup', async () => {
|
|
156
|
+
let release;
|
|
157
|
+
let entered;
|
|
158
|
+
let calls = 0;
|
|
159
|
+
const gate = new Promise(resolve => { release = resolve; });
|
|
160
|
+
const started = new Promise(resolve => { entered = resolve; });
|
|
161
|
+
const app = runtime({ ExamplePlugin() {} }, async args => {
|
|
162
|
+
calls++;
|
|
163
|
+
entered();
|
|
164
|
+
await gate;
|
|
165
|
+
return astAsync(args);
|
|
166
|
+
});
|
|
167
|
+
const element = new Element('ExamplePlugin');
|
|
168
|
+
const first = app.init(element);
|
|
169
|
+
await started;
|
|
170
|
+
let secondFinished = false;
|
|
171
|
+
const second = app.init(element).then(() => { secondFinished = true; });
|
|
172
|
+
await Promise.resolve();
|
|
173
|
+
await Promise.resolve();
|
|
174
|
+
assert.equal(secondFinished, false);
|
|
175
|
+
assert.equal(calls, 1);
|
|
176
|
+
release();
|
|
177
|
+
await Promise.all([first, second]);
|
|
178
|
+
assert.equal(secondFinished, true);
|
|
179
|
+
await app.init(element);
|
|
180
|
+
assert.equal(calls, 1);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('a plugin can await another initializer before connecting its result', async () => {
|
|
184
|
+
let release;
|
|
185
|
+
let entered;
|
|
186
|
+
let connected = false;
|
|
187
|
+
const gate = new Promise(resolve => { release = resolve; });
|
|
188
|
+
const started = new Promise(resolve => { entered = resolve; });
|
|
189
|
+
const markdown = new Element({ '$this.parser': { $Parser: [] } });
|
|
190
|
+
const CoCreate = { plugins: { init: () => app.init(markdown) } };
|
|
191
|
+
const app = runtime({
|
|
192
|
+
CoCreate,
|
|
193
|
+
async Parser() {
|
|
194
|
+
entered();
|
|
195
|
+
await gate;
|
|
196
|
+
return { ready: true };
|
|
197
|
+
},
|
|
198
|
+
connect() {
|
|
199
|
+
assert.equal(markdown.parser.ready, true);
|
|
200
|
+
connected = true;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
const prism = new Element({ '$CoCreate.plugins.init': ['#markdown'], $connect: [] });
|
|
204
|
+
const first = app.init(markdown);
|
|
205
|
+
await started;
|
|
206
|
+
const second = app.init(prism);
|
|
207
|
+
await Promise.resolve();
|
|
208
|
+
assert.equal(connected, false);
|
|
209
|
+
release();
|
|
210
|
+
await Promise.all([first, second]);
|
|
211
|
+
assert.equal(connected, true);
|
|
212
|
+
});
|
package/webpack.config.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
+
cacheGroups: {
|
|
56
60
|
defaultVendors: false
|
|
57
61
|
}
|
|
58
62
|
}
|
|
File without changes
|