@emulsify/core 4.2.0 → 4.3.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/.storybook/main-vite.js +91 -36
- package/README.md +185 -56
- package/config/a11y.config.js +2 -1
- package/config/vite/entries.js +4 -4
- package/config/vite/plugins/{copy-src-assets.js → assets/copy-src-assets.js} +1 -1
- package/config/vite/plugins/{copy-twig-files.js → assets/copy-twig-files.js} +1 -1
- package/config/vite/plugins/{css-asset-relativizer.js → assets/css-asset-relativizer.js} +9 -0
- package/config/vite/plugins/{mirror-components.js → assets/mirror-components.js} +2 -28
- package/config/vite/plugins/{source-file-index.js → assets/source-file-index.js} +1 -1
- package/config/vite/plugins/{svg-sprite.js → assets/svg-sprite.js} +2 -2
- package/config/vite/plugins/index.js +23 -14
- package/config/vite/plugins/reporter/asset-resolver.js +541 -0
- package/config/vite/plugins/reporter/build-errors.js +284 -0
- package/config/vite/plugins/reporter/diagnostics.js +366 -0
- package/config/vite/plugins/reporter/format.js +199 -0
- package/config/vite/plugins/reporter/index.js +308 -0
- package/config/vite/plugins/reporter/render.js +758 -0
- package/config/vite/plugins/reporter/sass-logger.js +237 -0
- package/config/vite/plugins/reporter/vite-logger.js +188 -0
- package/config/vite/plugins/reporter/watch-mode.js +40 -0
- package/config/vite/plugins/{twig-extension-installers.js → twig/extension-installers.js} +1 -1
- package/config/vite/{twig-extensions.js → plugins/twig/extensions.js} +2 -2
- package/config/vite/plugins/{twig-module.js → twig/twig-module.js} +387 -115
- package/config/vite/plugins/{virtual-twig-asset-sources.js → twig/virtual-twig-asset-sources.js} +37 -136
- package/config/vite/plugins/{virtual-twig-globs.js → twig/virtual-twig-globs.js} +3 -32
- package/config/vite/plugins/{vituum-patch.js → twig/vituum-patch.js} +3 -3
- package/config/vite/plugins.js +1 -1
- package/config/vite/project-config.js +1 -1
- package/config/vite/project-structure.js +1 -1
- package/config/vite/utils/lru.js +77 -0
- package/config/vite/utils/package-version.js +42 -0
- package/config/vite/utils/paths.js +1 -9
- package/config/vite/utils/react-singleton.js +1 -1
- package/config/vite/vite.config.js +51 -5
- package/package.json +93 -58
- package/scripts/a11y.js +115 -23
- package/scripts/audit/checks/core-imports.js +78 -0
- package/scripts/audit/checks/css-asset-references.js +99 -0
- package/scripts/audit/checks/drupal-assumptions.js +48 -0
- package/scripts/audit/checks/files-outside-roots.js +53 -0
- package/scripts/audit/checks/generated-package-scripts.js +113 -0
- package/scripts/audit/checks/legacy-twig-stories.js +33 -0
- package/scripts/audit/checks/package-overrides.js +91 -0
- package/scripts/audit/checks/project-config.js +71 -0
- package/scripts/audit/checks/story-discovery.js +35 -0
- package/scripts/audit/checks/twig-references.js +69 -0
- package/scripts/audit/checks/twig-volume.js +54 -0
- package/scripts/audit/checks/webpack-patterns.js +86 -0
- package/scripts/audit/index.js +177 -0
- package/scripts/audit/lib/css.js +165 -0
- package/scripts/audit/lib/files.js +168 -0
- package/scripts/audit/lib/findings.js +31 -0
- package/scripts/audit/lib/package-json.js +65 -0
- package/scripts/audit/lib/twig.js +227 -0
- package/scripts/audit/report.js +273 -0
- package/scripts/audit-twig-stories.js +115 -78
- package/scripts/audit.js +150 -1632
- package/scripts/check-node-version.js +136 -10
- package/scripts/inspect-components.js +456 -0
- package/scripts/lib/cli.js +179 -0
- package/scripts/lib/fs.js +31 -0
- package/scripts/lib/proc.js +78 -0
- package/scripts/lib/text.js +14 -0
- package/scripts/loadYaml.js +2 -2
- package/src/extensions/shared/attributes.js +3 -3
- package/src/extensions/shared/lists.js +2 -6
- package/src/extensions/shared/root-relative.js +38 -0
- package/src/storybook/index.js +4 -0
- package/src/storybook/render-twig.js +1 -1
- package/src/storybook/render-web-component.js +459 -0
- package/src/storybook/twig/asset-source-runtime.js +193 -0
- package/src/storybook/twig/{source-extensions.js → constants.js} +3 -1
- package/src/storybook/twig/reference-paths.js +2 -13
- package/src/storybook/twig/resolver.js +20 -3
- package/src/storybook/twig/setup.js +12 -2
- package/src/storybook/twig/source-function.js +5 -2
- package/config/vite/utils/unique.js +0 -36
- package/src/storybook/twig/include.js +0 -28
- package/src/storybook/twig/source-events.js +0 -5
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file React Storybook renderer for vanilla custom elements.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React, { useLayoutEffect, useRef } from 'react';
|
|
6
|
+
|
|
7
|
+
const reservedReactProps = new Set(['children', 'key', 'ref']);
|
|
8
|
+
const supportedArgsModes = ['properties', 'attributes'];
|
|
9
|
+
const reservedCustomElementNames = new Set([
|
|
10
|
+
'annotation-xml',
|
|
11
|
+
'color-profile',
|
|
12
|
+
'font-face',
|
|
13
|
+
'font-face-src',
|
|
14
|
+
'font-face-uri',
|
|
15
|
+
'font-face-format',
|
|
16
|
+
'font-face-name',
|
|
17
|
+
'missing-glyph',
|
|
18
|
+
]);
|
|
19
|
+
const validCustomElementNameCharacterPattern =
|
|
20
|
+
/^[-.0-9_a-z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u{10000}-\u{EFFFF}]*$/u;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Validate a custom element tag name.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} tagName - Candidate custom element tag name.
|
|
26
|
+
* @returns {void}
|
|
27
|
+
*/
|
|
28
|
+
function assertValidTagName(tagName) {
|
|
29
|
+
if (
|
|
30
|
+
typeof tagName !== 'string' ||
|
|
31
|
+
!/^[a-z]/u.test(tagName) ||
|
|
32
|
+
!validCustomElementNameCharacterPattern.test(tagName.slice(1)) ||
|
|
33
|
+
!tagName.includes('-') ||
|
|
34
|
+
reservedCustomElementNames.has(tagName)
|
|
35
|
+
) {
|
|
36
|
+
throw new SyntaxError(
|
|
37
|
+
`Invalid custom element tag name "${String(
|
|
38
|
+
tagName,
|
|
39
|
+
)}". Names must start with an ASCII lowercase letter, contain a hyphen, use browser-supported custom-element name characters, and must not be a reserved name.`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Validate that a value can be registered as a custom element.
|
|
46
|
+
*
|
|
47
|
+
* Reflect.construct checks whether the value is constructable without invoking
|
|
48
|
+
* user code. The prototype check catches classes that cannot be used as
|
|
49
|
+
* HTMLElement implementations before the native registry is mutated.
|
|
50
|
+
*
|
|
51
|
+
* @param {*} ElementClass - Candidate custom element constructor.
|
|
52
|
+
* @returns {void}
|
|
53
|
+
*/
|
|
54
|
+
function assertCustomElementConstructor(ElementClass) {
|
|
55
|
+
let isConstructor = false;
|
|
56
|
+
|
|
57
|
+
if (typeof ElementClass === 'function') {
|
|
58
|
+
try {
|
|
59
|
+
Reflect.construct(Object, [], ElementClass);
|
|
60
|
+
isConstructor = true;
|
|
61
|
+
} catch {
|
|
62
|
+
// The native registry requires an actual constructor.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let inheritsFromHTMLElement = false;
|
|
67
|
+
if (isConstructor) {
|
|
68
|
+
try {
|
|
69
|
+
inheritsFromHTMLElement = HTMLElement.prototype.isPrototypeOf(
|
|
70
|
+
ElementClass.prototype,
|
|
71
|
+
);
|
|
72
|
+
} catch {
|
|
73
|
+
// Proxies and accessor-backed prototypes can fail this structural check.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!isConstructor || !inheritsFromHTMLElement) {
|
|
78
|
+
throw new TypeError(
|
|
79
|
+
'Invalid custom element constructor. Expected a constructable class whose prototype inherits from HTMLElement.',
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (
|
|
84
|
+
let SuperClass = Object.getPrototypeOf(ElementClass);
|
|
85
|
+
SuperClass && SuperClass !== HTMLElement;
|
|
86
|
+
SuperClass = Object.getPrototypeOf(SuperClass)
|
|
87
|
+
) {
|
|
88
|
+
const isNativeHtmlElementInterface =
|
|
89
|
+
/^HTML.*Element$/u.test(SuperClass.name || '') &&
|
|
90
|
+
globalThis[SuperClass.name] === SuperClass;
|
|
91
|
+
|
|
92
|
+
if (isNativeHtmlElementInterface) {
|
|
93
|
+
throw new TypeError(
|
|
94
|
+
`Invalid custom element constructor. Autonomous custom elements must extend HTMLElement rather than ${SuperClass.name}. Customized built-in elements are not supported.`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Reject customized built-in registration options the renderer cannot create.
|
|
102
|
+
*
|
|
103
|
+
* @param {ElementDefinitionOptions} [options] - Native registry options.
|
|
104
|
+
* @returns {void}
|
|
105
|
+
*/
|
|
106
|
+
function assertAutonomousElementOptions(options) {
|
|
107
|
+
if (options?.extends !== undefined) {
|
|
108
|
+
throw new TypeError(
|
|
109
|
+
'Invalid defineCustomElement option "extends". Customized built-in elements are not supported; register an autonomous custom element instead.',
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Return a useful name for a custom element constructor.
|
|
116
|
+
*
|
|
117
|
+
* @param {Function} ElementClass - Custom element constructor.
|
|
118
|
+
* @returns {string} Constructor name.
|
|
119
|
+
*/
|
|
120
|
+
function getConstructorName(ElementClass) {
|
|
121
|
+
return ElementClass.name || '(anonymous constructor)';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Define a custom element once, returning the registered constructor.
|
|
126
|
+
*
|
|
127
|
+
* The browser custom element registry is permanent. Returning an existing
|
|
128
|
+
* definition keeps Storybook HMR and repeated story evaluation from throwing.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} tagName - Custom element tag name.
|
|
131
|
+
* @param {CustomElementConstructor} ElementClass - Element class to register.
|
|
132
|
+
* @param {ElementDefinitionOptions} [options] - Native options; `extends` is unsupported.
|
|
133
|
+
* @returns {CustomElementConstructor} Registered element class.
|
|
134
|
+
*/
|
|
135
|
+
export function defineCustomElement(tagName, ElementClass, options) {
|
|
136
|
+
assertValidTagName(tagName);
|
|
137
|
+
assertCustomElementConstructor(ElementClass);
|
|
138
|
+
assertAutonomousElementOptions(options);
|
|
139
|
+
|
|
140
|
+
const ExistingElement = customElements.get(tagName);
|
|
141
|
+
if (ExistingElement) {
|
|
142
|
+
if (ExistingElement !== ElementClass) {
|
|
143
|
+
console.warn(
|
|
144
|
+
`Custom element "${tagName}" is already defined with ${getConstructorName(
|
|
145
|
+
ExistingElement,
|
|
146
|
+
)}; ${getConstructorName(
|
|
147
|
+
ElementClass,
|
|
148
|
+
)} was not registered. Reload Storybook to use the new constructor.`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return ExistingElement;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
customElements.define(tagName, ElementClass, options);
|
|
156
|
+
return ElementClass;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Validate and normalize render options.
|
|
161
|
+
*
|
|
162
|
+
* @param {object} options - Candidate renderer options.
|
|
163
|
+
* @returns {object} Normalized renderer options.
|
|
164
|
+
*/
|
|
165
|
+
function normalizeRenderOptions(options) {
|
|
166
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
167
|
+
throw new TypeError(
|
|
168
|
+
'Invalid renderWebComponent options. Expected an options object.',
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const argsAs = options.argsAs === undefined ? 'properties' : options.argsAs;
|
|
173
|
+
if (!supportedArgsModes.includes(argsAs)) {
|
|
174
|
+
throw new TypeError(
|
|
175
|
+
`Invalid renderWebComponent option "argsAs": "${String(
|
|
176
|
+
argsAs,
|
|
177
|
+
)}". Expected "properties" or "attributes".`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const events = normalizeEventMappings(options.events);
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
argsAs,
|
|
185
|
+
className: options.className,
|
|
186
|
+
events,
|
|
187
|
+
eventArgNames: new Set(events.map(([, argName]) => argName)),
|
|
188
|
+
id: options.id,
|
|
189
|
+
wrapper: options.wrapper,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Validate and normalize native event-to-arg mappings.
|
|
195
|
+
*
|
|
196
|
+
* @param {object} [events] - Native event names mapped to callback arg names.
|
|
197
|
+
* @returns {Array<[string, string]>} Normalized mappings.
|
|
198
|
+
*/
|
|
199
|
+
function normalizeEventMappings(events) {
|
|
200
|
+
if (events === undefined) {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!events || typeof events !== 'object' || Array.isArray(events)) {
|
|
205
|
+
throw new TypeError(
|
|
206
|
+
'Invalid renderWebComponent option "events". Expected an object mapping native event names to Storybook callback arg names.',
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return Object.entries(events).map(([eventName, argName]) => {
|
|
211
|
+
if (!eventName.trim()) {
|
|
212
|
+
throw new TypeError(
|
|
213
|
+
'Invalid renderWebComponent option "events": event names must be non-empty strings.',
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (typeof argName !== 'string' || !argName.trim()) {
|
|
218
|
+
throw new TypeError(
|
|
219
|
+
`Invalid renderWebComponent option "events.${eventName}". Expected a non-empty Storybook callback arg name.`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return [eventName, argName];
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Return Storybook args without React- or renderer-owned values.
|
|
229
|
+
*
|
|
230
|
+
* @param {object} [args={}] - Storybook args.
|
|
231
|
+
* @param {Set<string>} [eventArgNames] - Callback args owned by event mappings.
|
|
232
|
+
* @returns {Array<[string, *]>} Entries safe to apply to a custom element.
|
|
233
|
+
*/
|
|
234
|
+
function customElementArgEntries(args = {}, eventArgNames = new Set()) {
|
|
235
|
+
return Object.entries(args || {}).filter(
|
|
236
|
+
([key]) => !reservedReactProps.has(key) && !eventArgNames.has(key),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Apply Storybook args as DOM properties.
|
|
242
|
+
*
|
|
243
|
+
* @param {HTMLElement} element - Custom element instance.
|
|
244
|
+
* @param {Array<[string, *]>} entries - Storybook arg entries.
|
|
245
|
+
* @returns {void}
|
|
246
|
+
*/
|
|
247
|
+
function applyProperties(element, entries) {
|
|
248
|
+
for (const [key, value] of entries) {
|
|
249
|
+
element[key] = value;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Apply Storybook args as DOM attributes.
|
|
255
|
+
*
|
|
256
|
+
* @param {HTMLElement} element - Custom element instance.
|
|
257
|
+
* @param {Array<[string, *]>} entries - Storybook arg entries.
|
|
258
|
+
* @returns {void}
|
|
259
|
+
*/
|
|
260
|
+
function applyAttributes(element, entries) {
|
|
261
|
+
for (const [key, value] of entries) {
|
|
262
|
+
if (value === false || value == null) {
|
|
263
|
+
element.removeAttribute(key);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
element.setAttribute(key, value === true ? '' : String(value));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Clear an arg that was applied during the previous render.
|
|
273
|
+
*
|
|
274
|
+
* Assigning undefined in property mode lets custom setters clear their own
|
|
275
|
+
* state. Reflected DOM string properties need their generated attribute
|
|
276
|
+
* removed as well so values such as "undefined" do not remain visible.
|
|
277
|
+
*
|
|
278
|
+
* @param {HTMLElement} element - Custom element instance.
|
|
279
|
+
* @param {string} key - Previously applied arg name.
|
|
280
|
+
* @param {'properties'|'attributes'} argsAs - Previous application mode.
|
|
281
|
+
* @returns {void}
|
|
282
|
+
*/
|
|
283
|
+
function clearAppliedArg(element, key, argsAs) {
|
|
284
|
+
if (argsAs === 'attributes') {
|
|
285
|
+
element.removeAttribute(key);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
element[key] = undefined;
|
|
290
|
+
|
|
291
|
+
if (key === 'className') {
|
|
292
|
+
element.removeAttribute('class');
|
|
293
|
+
} else if (element.getAttribute(key) === 'undefined') {
|
|
294
|
+
element.removeAttribute(key);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Resolve active native event listeners from the current Storybook args.
|
|
300
|
+
*
|
|
301
|
+
* @param {object} args - Storybook args.
|
|
302
|
+
* @param {Array<[string, string]>} mappings - Event-to-callback mappings.
|
|
303
|
+
* @returns {Array<[string, Function]>} Native listeners to attach.
|
|
304
|
+
*/
|
|
305
|
+
function resolveEventListeners(args, mappings) {
|
|
306
|
+
return mappings.flatMap(([eventName, argName]) => {
|
|
307
|
+
const callback = args[argName];
|
|
308
|
+
|
|
309
|
+
if (callback == null) {
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (typeof callback !== 'function') {
|
|
314
|
+
throw new TypeError(
|
|
315
|
+
`Invalid Storybook arg "${argName}" for native event "${eventName}". Expected a function, null, or undefined.`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return [[eventName, callback]];
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Restore element-level presentation options after Storybook args are applied.
|
|
325
|
+
*
|
|
326
|
+
* Storybook args win while present. When an overriding arg is later omitted,
|
|
327
|
+
* the stable renderer option is restored because React may not write an
|
|
328
|
+
* unchanged virtual prop again.
|
|
329
|
+
*
|
|
330
|
+
* @param {HTMLElement} element - Custom element instance.
|
|
331
|
+
* @param {Set<string>} currentKeys - Args applied during this render.
|
|
332
|
+
* @param {object} options - Normalized render options.
|
|
333
|
+
* @returns {void}
|
|
334
|
+
*/
|
|
335
|
+
function restoreElementOptions(element, currentKeys, options) {
|
|
336
|
+
if (options.id != null && !currentKeys.has('id')) {
|
|
337
|
+
element.id = options.id;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const classArg = options.argsAs === 'attributes' ? 'class' : 'className';
|
|
341
|
+
if (options.className != null && !currentKeys.has(classArg)) {
|
|
342
|
+
element.className = options.className;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* React wrapper that applies Storybook args to a custom element via ref.
|
|
348
|
+
*
|
|
349
|
+
* @param {object} props - Component props.
|
|
350
|
+
* @param {string} props.tagName - Custom element tag name.
|
|
351
|
+
* @param {object} [props.args] - Storybook args.
|
|
352
|
+
* @param {object} [props.options] - Render options.
|
|
353
|
+
* @returns {React.ReactElement} React element.
|
|
354
|
+
*/
|
|
355
|
+
function CustomElementStory({ tagName, args = {}, options }) {
|
|
356
|
+
const elementRef = useRef(null);
|
|
357
|
+
const previousAppliedArgs = useRef({
|
|
358
|
+
argsAs: options.argsAs,
|
|
359
|
+
keys: new Set(),
|
|
360
|
+
});
|
|
361
|
+
const WrapperElement = options.wrapper;
|
|
362
|
+
const storyArgs = args && typeof args === 'object' ? args : {};
|
|
363
|
+
const entries = customElementArgEntries(storyArgs, options.eventArgNames);
|
|
364
|
+
const currentKeys = new Set(entries.map(([key]) => key));
|
|
365
|
+
const eventListeners = resolveEventListeners(storyArgs, options.events);
|
|
366
|
+
const elementProps = { ref: elementRef };
|
|
367
|
+
|
|
368
|
+
if (!WrapperElement) {
|
|
369
|
+
elementProps.id = options.id;
|
|
370
|
+
elementProps.className = options.className;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
useLayoutEffect(() => {
|
|
374
|
+
if (!elementRef.current) return;
|
|
375
|
+
|
|
376
|
+
const element = elementRef.current;
|
|
377
|
+
const previous = previousAppliedArgs.current;
|
|
378
|
+
|
|
379
|
+
for (const key of previous.keys) {
|
|
380
|
+
if (previous.argsAs !== options.argsAs || !currentKeys.has(key)) {
|
|
381
|
+
clearAppliedArg(element, key, previous.argsAs);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
for (const [eventName, callback] of eventListeners) {
|
|
386
|
+
element.addEventListener(eventName, callback);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (options.argsAs === 'attributes') {
|
|
390
|
+
applyAttributes(element, entries);
|
|
391
|
+
} else {
|
|
392
|
+
applyProperties(element, entries);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (!WrapperElement) {
|
|
396
|
+
restoreElementOptions(element, currentKeys, options);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
previousAppliedArgs.current = {
|
|
400
|
+
argsAs: options.argsAs,
|
|
401
|
+
keys: currentKeys,
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
return () => {
|
|
405
|
+
for (const [eventName, callback] of eventListeners) {
|
|
406
|
+
element.removeEventListener(eventName, callback);
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const element = React.createElement(
|
|
412
|
+
tagName,
|
|
413
|
+
elementProps,
|
|
414
|
+
storyArgs.children,
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
if (!WrapperElement) {
|
|
418
|
+
return element;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return React.createElement(
|
|
422
|
+
WrapperElement,
|
|
423
|
+
{
|
|
424
|
+
id: options.id,
|
|
425
|
+
className: options.className,
|
|
426
|
+
},
|
|
427
|
+
element,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
CustomElementStory.displayName = 'CustomElementStory';
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Create a React-compatible Storybook render function for a custom element.
|
|
435
|
+
*
|
|
436
|
+
* @param {string} tagName - Custom element tag name.
|
|
437
|
+
* @param {object} [options={}] - Render options.
|
|
438
|
+
* @param {'properties'|'attributes'} [options.argsAs='properties'] - How args are applied.
|
|
439
|
+
* @param {Record<string, string>} [options.events] - Native event names mapped to callback args.
|
|
440
|
+
* @param {string|Function} [options.wrapper] - Optional wrapper element.
|
|
441
|
+
* @param {string} [options.className] - Class for the wrapper or element.
|
|
442
|
+
* @param {string} [options.id] - ID for the wrapper or element.
|
|
443
|
+
* @returns {Function} Storybook render function.
|
|
444
|
+
*/
|
|
445
|
+
export function renderWebComponent(tagName, options = {}) {
|
|
446
|
+
assertValidTagName(tagName);
|
|
447
|
+
const normalizedOptions = normalizeRenderOptions(options);
|
|
448
|
+
|
|
449
|
+
const EmulsifyWebComponentRender = (args = {}) =>
|
|
450
|
+
React.createElement(CustomElementStory, {
|
|
451
|
+
tagName,
|
|
452
|
+
args,
|
|
453
|
+
options: normalizedOptions,
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
EmulsifyWebComponentRender.displayName = 'EmulsifyWebComponentRender';
|
|
457
|
+
|
|
458
|
+
return EmulsifyWebComponentRender;
|
|
459
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Shared runtime for virtual Twig source() text assets.
|
|
3
|
+
* This module is loaded by Core's Vite plugin and is not a package export.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const collectionHas = (values, value) =>
|
|
7
|
+
typeof values?.has === 'function'
|
|
8
|
+
? values.has(value)
|
|
9
|
+
: Array.isArray(values) && values.includes(value);
|
|
10
|
+
|
|
11
|
+
const collectionLength = (values) =>
|
|
12
|
+
typeof values?.size === 'number' ? values.size : values?.length || 0;
|
|
13
|
+
|
|
14
|
+
const unique = (values) => Array.from(new Set(values.filter(Boolean)));
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Normalize a Twig asset reference to a root-relative asset key segment.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} assetPath - Twig asset reference.
|
|
20
|
+
* @returns {string} Normalized asset path without @assets or leading slash.
|
|
21
|
+
*/
|
|
22
|
+
export const normalizeAssetPath = (assetPath) =>
|
|
23
|
+
String(assetPath || '')
|
|
24
|
+
.replace(/^@assets\//, '')
|
|
25
|
+
.replace(/^\/?assets\//, '')
|
|
26
|
+
.replace(/^\/+/, '');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build the virtual source-map keys that can satisfy one asset reference.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} assetPath - Twig asset reference.
|
|
32
|
+
* @param {{ assetRootPrefixes?: string[], generatedAssetRootPrefixes?: string[], generatedAssetAliases?: string[]|Set<string> }} [options={}] - Runtime key state.
|
|
33
|
+
* @returns {string[]} Candidate keys in lookup order.
|
|
34
|
+
*/
|
|
35
|
+
export function candidateKeysForAssetPath(
|
|
36
|
+
assetPath,
|
|
37
|
+
{
|
|
38
|
+
assetRootPrefixes = [],
|
|
39
|
+
generatedAssetRootPrefixes = [],
|
|
40
|
+
generatedAssetAliases = [],
|
|
41
|
+
} = {},
|
|
42
|
+
) {
|
|
43
|
+
const rawPath = String(assetPath || '');
|
|
44
|
+
const normalized = normalizeAssetPath(rawPath);
|
|
45
|
+
const directPath = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;
|
|
46
|
+
const generatedCandidates = collectionHas(generatedAssetAliases, normalized)
|
|
47
|
+
? generatedAssetRootPrefixes.map(
|
|
48
|
+
(root) => `${root.replace(/\/+$/, '')}/${normalized}`,
|
|
49
|
+
)
|
|
50
|
+
: [];
|
|
51
|
+
|
|
52
|
+
return unique([
|
|
53
|
+
rawPath,
|
|
54
|
+
directPath,
|
|
55
|
+
normalized ? `/${normalized}` : '',
|
|
56
|
+
...generatedCandidates,
|
|
57
|
+
...assetRootPrefixes.map(
|
|
58
|
+
(root) => `${root.replace(/\/+$/, '')}/${normalized}`,
|
|
59
|
+
),
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Find the source-map key that resolves one asset reference.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} assetPath - Twig asset reference.
|
|
67
|
+
* @param {{ assets?: Record<string, unknown>, assetRootPrefixes?: string[], generatedAssetRootPrefixes?: string[], generatedAssetAliases?: string[]|Set<string> }} [options={}] - Runtime key state.
|
|
68
|
+
* @returns {string|undefined} Matching source-map key.
|
|
69
|
+
*/
|
|
70
|
+
export function findAssetKey(assetPath, options = {}) {
|
|
71
|
+
const { assets = {} } = options;
|
|
72
|
+
|
|
73
|
+
return candidateKeysForAssetPath(assetPath, options).find((key) =>
|
|
74
|
+
Object.hasOwnProperty.call(assets, key),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const normalizeSourceText = (value) => {
|
|
79
|
+
const source = value?.default ?? value;
|
|
80
|
+
return typeof source === 'string' ? source : undefined;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const reportAssetLoadError = (key, error) => {
|
|
84
|
+
if (typeof console !== 'undefined' && typeof console.error === 'function') {
|
|
85
|
+
console.error(`source(): failed to load asset ${key}`, error);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Create the synchronous facade around lazy virtual asset source imports.
|
|
91
|
+
*
|
|
92
|
+
* @param {{
|
|
93
|
+
* assets?: Record<string, unknown>,
|
|
94
|
+
* assetRootPrefixes?: string[],
|
|
95
|
+
* generatedAssetRootPrefixes?: string[],
|
|
96
|
+
* generatedAssetAliases?: string[]|Set<string>,
|
|
97
|
+
* onLoadError?: Function
|
|
98
|
+
* }} [state={}] - Runtime source-map state.
|
|
99
|
+
* @returns {{ coversAssetPath: Function, hasAssetText: Function, isAssetTextLoading: Function, whenAssetTextLoaded: Function, getAssetText: Function, clearAssetTextCache: Function }} Runtime helpers.
|
|
100
|
+
*/
|
|
101
|
+
export function createAssetSourceRuntime(state = {}) {
|
|
102
|
+
const {
|
|
103
|
+
assets = {},
|
|
104
|
+
assetRootPrefixes = [],
|
|
105
|
+
generatedAssetRootPrefixes = [],
|
|
106
|
+
generatedAssetAliases = [],
|
|
107
|
+
onLoadError = reportAssetLoadError,
|
|
108
|
+
} = state;
|
|
109
|
+
const sourceTextCache = new Map();
|
|
110
|
+
const sourceLoadPromises = new Map();
|
|
111
|
+
const keyState = {
|
|
112
|
+
assets,
|
|
113
|
+
assetRootPrefixes,
|
|
114
|
+
generatedAssetRootPrefixes,
|
|
115
|
+
generatedAssetAliases,
|
|
116
|
+
};
|
|
117
|
+
const findKey = (assetPath) => findAssetKey(assetPath, keyState);
|
|
118
|
+
|
|
119
|
+
const coversAssetPath = (assetPath) =>
|
|
120
|
+
(collectionLength(assetRootPrefixes) > 0 ||
|
|
121
|
+
collectionLength(generatedAssetRootPrefixes) > 0) &&
|
|
122
|
+
normalizeAssetPath(assetPath).length > 0;
|
|
123
|
+
|
|
124
|
+
const hasAssetText = (assetPath) => Boolean(findKey(assetPath));
|
|
125
|
+
|
|
126
|
+
const isAssetTextLoading = (assetPath) => {
|
|
127
|
+
const key = findKey(assetPath);
|
|
128
|
+
return Boolean(key && sourceLoadPromises.has(key));
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const whenAssetTextLoaded = (assetPath) => {
|
|
132
|
+
const key = findKey(assetPath);
|
|
133
|
+
return key ? sourceLoadPromises.get(key) : undefined;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const getAssetText = (assetPath) => {
|
|
137
|
+
const key = findKey(assetPath);
|
|
138
|
+
if (!key) return undefined;
|
|
139
|
+
if (sourceTextCache.has(key)) {
|
|
140
|
+
return sourceTextCache.get(key);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const loader = assets[key];
|
|
144
|
+
const sourceText = normalizeSourceText(loader);
|
|
145
|
+
if (typeof sourceText === 'string') {
|
|
146
|
+
sourceTextCache.set(key, sourceText);
|
|
147
|
+
return sourceText;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (typeof loader === 'function' && !sourceLoadPromises.has(key)) {
|
|
151
|
+
let loadedSource;
|
|
152
|
+
try {
|
|
153
|
+
loadedSource = loader();
|
|
154
|
+
} catch (error) {
|
|
155
|
+
loadedSource = Promise.reject(error);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const sourceLoad = Promise.resolve(loadedSource)
|
|
159
|
+
.then((loaded) => {
|
|
160
|
+
const loadedText = normalizeSourceText(loaded);
|
|
161
|
+
if (typeof loadedText === 'string') {
|
|
162
|
+
sourceTextCache.set(key, loadedText);
|
|
163
|
+
}
|
|
164
|
+
return loadedText;
|
|
165
|
+
})
|
|
166
|
+
.catch((error) => {
|
|
167
|
+
onLoadError(key, error);
|
|
168
|
+
return undefined;
|
|
169
|
+
})
|
|
170
|
+
.finally(() => {
|
|
171
|
+
sourceLoadPromises.delete(key);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
sourceLoadPromises.set(key, sourceLoad);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return undefined;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const clearAssetTextCache = () => {
|
|
181
|
+
sourceTextCache.clear();
|
|
182
|
+
sourceLoadPromises.clear();
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
coversAssetPath,
|
|
187
|
+
hasAssetText,
|
|
188
|
+
isAssetTextLoading,
|
|
189
|
+
whenAssetTextLoaded,
|
|
190
|
+
getAssetText,
|
|
191
|
+
clearAssetTextCache,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file Shared
|
|
2
|
+
* @file Shared constants for Twig source() handling.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
export const TWIG_SOURCE_LOADED_EVENT = 'emulsify:twig-source-loaded';
|
|
6
|
+
|
|
5
7
|
// Text assets can be safely inlined; binary assets should remain URL-based.
|
|
6
8
|
export const INLINE_ASSET_EXTS = new Set([
|
|
7
9
|
'svg',
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { unique } from '../../extensions/shared/lists.js';
|
|
9
|
+
import { toRootRelativePath as toProjectRootRelativePath } from '../../extensions/shared/root-relative.js';
|
|
9
10
|
|
|
10
11
|
const ENV = (typeof __EMULSIFY_ENV__ !== 'undefined' && __EMULSIFY_ENV__) || {};
|
|
11
12
|
|
|
@@ -15,8 +16,6 @@ let rootRecordsCache = new WeakMap();
|
|
|
15
16
|
/** @type {object[]|undefined} */
|
|
16
17
|
let defaultRecords;
|
|
17
18
|
|
|
18
|
-
const normalizeGlobPath = (filePath) => filePath.replace(/\\/g, '/');
|
|
19
|
-
|
|
20
19
|
/**
|
|
21
20
|
* Convert an absolute project path to a Vite root-relative key.
|
|
22
21
|
*
|
|
@@ -25,17 +24,7 @@ const normalizeGlobPath = (filePath) => filePath.replace(/\\/g, '/');
|
|
|
25
24
|
* @returns {string} Root-relative path with a leading slash.
|
|
26
25
|
*/
|
|
27
26
|
export function toRootRelativePath(absolutePath, env = ENV) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const normalizedPath = normalizeGlobPath(absolutePath);
|
|
31
|
-
const projectDir = normalizeGlobPath(env?.projectDir || '');
|
|
32
|
-
|
|
33
|
-
if (projectDir && normalizedPath.startsWith(projectDir)) {
|
|
34
|
-
const relativePath = normalizedPath.slice(projectDir.length);
|
|
35
|
-
return relativePath.startsWith('/') ? relativePath : `/${relativePath}`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return normalizedPath.startsWith('/') ? normalizedPath : `/${normalizedPath}`;
|
|
27
|
+
return toProjectRootRelativePath(env?.projectDir, absolutePath);
|
|
39
28
|
}
|
|
40
29
|
|
|
41
30
|
/**
|