@excom/neutron 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/.rush/temp/chunked-rush-logs/neutron.apply-exports.chunks.jsonl +1 -0
  2. package/.rush/temp/chunked-rush-logs/neutron.build_package-metas.chunks.jsonl +1 -0
  3. package/.rush/temp/operation/apply-exports/all.log +1 -0
  4. package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
  5. package/.rush/temp/operation/apply-exports/state.json +3 -0
  6. package/.rush/temp/operation/build_package-metas/all.log +1 -0
  7. package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
  8. package/.rush/temp/operation/build_package-metas/state.json +3 -0
  9. package/.rush/temp/shrinkwrap-deps.json +3 -0
  10. package/config/rig.json +6 -0
  11. package/index.ts +9 -0
  12. package/package.json +45 -0
  13. package/rush-logs/neutron.apply-exports.cache.log +1 -0
  14. package/rush-logs/neutron.apply-exports.log +1 -0
  15. package/rush-logs/neutron.build_package-metas.cache.log +1 -0
  16. package/rush-logs/neutron.build_package-metas.log +1 -0
  17. package/src/command.ts +102 -0
  18. package/src/common-element.ts +377 -0
  19. package/src/constants.ts +101 -0
  20. package/src/devtools-hook.ts +93 -0
  21. package/src/lifecycle-configs.ts +304 -0
  22. package/src/neutron-element.ts +72 -0
  23. package/src/neutron-error.ts +6 -0
  24. package/src/neutron-internal.ts +550 -0
  25. package/src/neutron.ts +36 -0
  26. package/src/types/effect.types.ts +104 -0
  27. package/src/types/element.types.ts +263 -0
  28. package/src/types/index.ts +4 -0
  29. package/src/types/new.types.ts +159 -0
  30. package/src/types/shared.types.ts +25 -0
  31. package/src/utils/effect.ts +357 -0
  32. package/src/utils/element.ts +382 -0
  33. package/src/utils/index.ts +2 -0
  34. package/support/docs/COMMANDS.md +58 -0
  35. package/support/docs/COMPOSE.md +32 -0
  36. package/support/docs/DEBUG.md +11 -0
  37. package/support/docs/DEFINE.md +20 -0
  38. package/support/docs/EFFECTS.md +49 -0
  39. package/support/docs/EVENTS.md +57 -0
  40. package/support/docs/LIFECYCLES.md +69 -0
  41. package/support/docs/METHODS.md +49 -0
  42. package/support/docs/PROMISE_PROPS.md +29 -0
  43. package/support/docs/PROPS.md +64 -0
  44. package/support/docs/PROP_REACTIONS.md +35 -0
  45. package/support/docs/PROVISION.md +31 -0
  46. package/support/docs/README.md +118 -0
  47. package/support/docs/RECOMPOSE.md +70 -0
  48. package/support/docs/TYPESCRIPT.md +51 -0
  49. package/support/docs-sections.json +42 -0
  50. package/support/package-meta.json +129 -0
  51. package/support/tests/commands.test.ts +330 -0
  52. package/support/tests/common-element.test.ts +342 -0
  53. package/support/tests/devtools-hook.test.ts +209 -0
  54. package/support/tests/devtools-renderer.test.ts +125 -0
  55. package/support/tests/effects.test.ts +253 -0
  56. package/support/tests/element-config.test.ts +331 -0
  57. package/support/tests/entry.test.ts +68 -0
  58. package/support/tests/lifecycles.test.ts +489 -0
  59. package/support/tests/loop-guard.test.ts +162 -0
  60. package/support/tests/neutron.test.ts +1286 -0
  61. package/support/tests/recompose.test.ts +129 -0
  62. package/support/tests/utils.test.ts +75 -0
  63. package/tsconfig.json +5 -0
@@ -0,0 +1,550 @@
1
+ import { COMMAND_EVENT } from "./command";
2
+ import { CommonElement } from "./common-element";
3
+ import { PROTECTED_PROP_NAMES } from "./constants";
4
+ import { injectRendererIfNeeded, publicize } from "./devtools-hook";
5
+ import {
6
+ LifecycleConfigs,
7
+ registerLifecycle,
8
+ unregisterLifecycle,
9
+ } from "./lifecycle-configs";
10
+ import type { NeutronElement as TNeutronElement } from "./neutron-element";
11
+ import { NeutronError } from "./neutron-error";
12
+ import type {
13
+ AnyFunction,
14
+ BuiltConfig,
15
+ EventListenerArgs,
16
+ Obj,
17
+ OptsConfig,
18
+ RuntimeConfig,
19
+ } from "./types";
20
+ import {
21
+ createBuiltConfig,
22
+ createRuntimeConfig,
23
+ isBuiltInElement,
24
+ setDebugMethodSignature,
25
+ } from "./utils";
26
+ import { KitLogger } from "@excom/kit-logger";
27
+ import {
28
+ defineObservableProperty,
29
+ LoopGuard,
30
+ TokenList,
31
+ } from "@excom/kit-utils";
32
+ import {
33
+ BatchHandlers,
34
+ BatchManager,
35
+ Converter,
36
+ createElement,
37
+ QueueManager,
38
+ } from "@excom/kit-utils";
39
+
40
+ export class NeutronInternal {
41
+ static builtConfig: BuiltConfig;
42
+ static runtimeConfig: RuntimeConfig;
43
+ static batchLinkFns: AnyFunction[];
44
+ static batchHandlers: BatchHandlers;
45
+
46
+ static CustomElement: typeof TNeutronElement;
47
+
48
+ ctr: typeof NeutronInternal;
49
+
50
+ private element: TNeutronElement;
51
+ private elementRef: WeakRef<TNeutronElement>;
52
+
53
+ eventListeners: EventListenerArgs[] = [];
54
+ disconnectedEventListeners: EventListenerArgs[] = [];
55
+ broadcastListeners: EventListenerArgs[] = [];
56
+ disconnectedBroadcastListeners: EventListenerArgs[] = [];
57
+ propStore: Obj = {};
58
+ queueManager = new QueueManager();
59
+ batchManager: BatchManager;
60
+ execDisconnect?: null | (() => void);
61
+ debug?: {
62
+ effects: {
63
+ getTrace: () => string;
64
+ trigger: any;
65
+ effect: Obj;
66
+ }[];
67
+ lockLevels: string[];
68
+ };
69
+
70
+ /* --- PUBLIC: STATIC ELEMENT DEFINITION --- */
71
+ static define(
72
+ tag: string = this.builtConfig.tag,
73
+ options: ElementDefinitionOptions | undefined = this.builtConfig
74
+ .definitionOpts
75
+ ) {
76
+ if (!customElements.get(tag)) {
77
+ this.runtimeConfig = createRuntimeConfig(this.builtConfig);
78
+ this.CustomElement.observedAttributes = this.getObservedAttrs();
79
+ this.buildBatching();
80
+ customElements.define(tag, this.CustomElement, options);
81
+ /* First define() after a hook is installed registers the renderer.
82
+ * Late attachers should call `attachDevtools()` instead. */
83
+ injectRendererIfNeeded();
84
+ } else {
85
+ KitLogger.warn(
86
+ `Attempted to define Neutron element "${tag}", but it's already defined.`
87
+ );
88
+ }
89
+ }
90
+ /* Type-only builder step (see `ElementBuilder.withTypes`): declares
91
+ * method signatures on the element type before they are defined. */
92
+ static withTypes() {
93
+ return this;
94
+ }
95
+ static defineMethods(methods: Record<string, AnyFunction>) {
96
+ const entries = Object.entries(methods);
97
+ entries.forEach(([name, fn]) => {
98
+ setDebugMethodSignature(fn, name);
99
+ if (typeof fn !== "function") {
100
+ throw new Error(`Method ${name} is not a function`);
101
+ }
102
+ if (PROTECTED_PROP_NAMES.includes(name)) {
103
+ throw new NeutronError(`Cannot use protected name: "${name}"`);
104
+ }
105
+ });
106
+ this.builtConfig.methods.push(...entries);
107
+ return this;
108
+ }
109
+ static onConstructed(fn) {
110
+ return registerLifecycle(this, "constructed", [], fn);
111
+ }
112
+ static offConstructed(fn) {
113
+ return unregisterLifecycle(this, "constructed", [], fn);
114
+ }
115
+ static onConnected(fn) {
116
+ return registerLifecycle(this, "connected", [], fn);
117
+ }
118
+ static offConnected(fn) {
119
+ return unregisterLifecycle(this, "connected", [], fn);
120
+ }
121
+ static onAdopted(fn) {
122
+ return registerLifecycle(this, "adopted", [], fn);
123
+ }
124
+ static offAdopted(fn) {
125
+ return unregisterLifecycle(this, "adopted", [], fn);
126
+ }
127
+ static onError(fn) {
128
+ return registerLifecycle(this, "error", [], fn);
129
+ }
130
+ static offError(fn) {
131
+ return unregisterLifecycle(this, "error", [], fn);
132
+ }
133
+ static onEffect(names, fn) {
134
+ return registerLifecycle(this, "effect", names, fn);
135
+ }
136
+ static offEffect(names, fn) {
137
+ return unregisterLifecycle(this, "effect", names, fn);
138
+ }
139
+ static onPropUnset(names, fn) {
140
+ return registerLifecycle(this, "propUnset", names, fn);
141
+ }
142
+ static offPropUnset(names, fn) {
143
+ return unregisterLifecycle(this, "propUnset", names, fn);
144
+ }
145
+ static onPropSet(names, fn) {
146
+ return registerLifecycle(this, "propSet", names, fn);
147
+ }
148
+ static offPropSet(names, fn) {
149
+ return unregisterLifecycle(this, "propSet", names, fn);
150
+ }
151
+ static onPropChanged(names, fn) {
152
+ return registerLifecycle(this, "propChanged", names, fn);
153
+ }
154
+ static offPropChanged(names, fn) {
155
+ return unregisterLifecycle(this, "propChanged", names, fn);
156
+ }
157
+ static onPromiseResolved(names, fn) {
158
+ return registerLifecycle(this, "promiseResolved", names, fn);
159
+ }
160
+ static offPromiseResolved(names, fn) {
161
+ return unregisterLifecycle(this, "promiseResolved", names, fn);
162
+ }
163
+ static onPromiseRejected(names, fn) {
164
+ return registerLifecycle(this, "promiseRejected", names, fn);
165
+ }
166
+ static offPromiseRejected(names, fn) {
167
+ return unregisterLifecycle(this, "promiseRejected", names, fn);
168
+ }
169
+ static onBroadcast(names, fn) {
170
+ return registerLifecycle(this, "broadcast", names, fn);
171
+ }
172
+ static offBroadcast(names, fn) {
173
+ return unregisterLifecycle(this, "broadcast", names, fn);
174
+ }
175
+ static onEvent(names, fn) {
176
+ return registerLifecycle(this, "event", names, fn);
177
+ }
178
+ static offEvent(names, fn) {
179
+ return unregisterLifecycle(this, "event", names, fn);
180
+ }
181
+ static onEventDefault(names, fn) {
182
+ return registerLifecycle(this, "eventDefault", names, fn);
183
+ }
184
+ static offEventDefault(names, fn) {
185
+ return unregisterLifecycle(this, "eventDefault", names, fn);
186
+ }
187
+ static onCommand(names, fn) {
188
+ return registerLifecycle(this, "command", names, fn);
189
+ }
190
+ static offCommand(names, fn) {
191
+ return unregisterLifecycle(this, "command", names, fn);
192
+ }
193
+ static onDisconnected(fn) {
194
+ return registerLifecycle(this, "disconnected", [], fn);
195
+ }
196
+ static offDisconnected(fn) {
197
+ return unregisterLifecycle(this, "disconnected", [], fn);
198
+ }
199
+
200
+ /* --- PROTECTED: STATIC ELEMENT DEFINITION --- */
201
+ static setup(optsConfig: OptsConfig, CustomElement: typeof TNeutronElement) {
202
+ this.builtConfig = createBuiltConfig(optsConfig);
203
+ /* DevTools: one record per definition (no instance, so no
204
+ * weakElement). The extension audits attribute names from it. */
205
+ publicize(["neutron", "defined"], {
206
+ tag: this.builtConfig.tag,
207
+ props: Object.values(this.builtConfig.props ?? {}).map(
208
+ ({ prop, attr }) => ({ prop, attr })
209
+ ),
210
+ });
211
+ // wire the class to this Internal
212
+ CustomElement.NeutronInternal = this;
213
+ this.CustomElement = CustomElement;
214
+ }
215
+ private static getObservedAttrs() {
216
+ return Object.values(this.runtimeConfig.props ?? {})
217
+ .filter((propConfig) => propConfig.notify === "attr" && propConfig.attr)
218
+ .map((propConfig) => propConfig.attr as string);
219
+ }
220
+ private static buildBatching() {
221
+ const linkTracker = {};
222
+ this.batchLinkFns = [];
223
+ this.batchHandlers = [];
224
+ Object.values(LifecycleConfigs).forEach((c) => {
225
+ if (c.key && c.isBatched) {
226
+ const batchPrefix = c.batchPrefix || "";
227
+ const lifecycleEntries = this.runtimeConfig.lifecycles[c.key];
228
+ lifecycleEntries.forEach((lifecycleEntry) => {
229
+ const [batchNames, handler] = lifecycleEntry;
230
+ if (c.triggerFn) {
231
+ batchNames.forEach((batchName) => {
232
+ if (!linkTracker[batchPrefix + batchName]) {
233
+ linkTracker[batchPrefix + batchName] = true;
234
+ this.batchLinkFns.push((instance) => {
235
+ c.triggerFn?.(instance, batchPrefix, batchName);
236
+ });
237
+ }
238
+ });
239
+ }
240
+ this.batchHandlers.push([
241
+ batchNames.map((batchName) => batchPrefix + batchName),
242
+ handler,
243
+ c.batchExecFn,
244
+ ]);
245
+ });
246
+ }
247
+ });
248
+ }
249
+
250
+ /* --- PROTECTED: INSTANCE LIFECYCLE CALLBACKS --- */
251
+ constructor(element: TNeutronElement) {
252
+ this.element = element;
253
+ this.elementRef = new WeakRef(element);
254
+ this.ctr = this.constructor as unknown as typeof NeutronInternal;
255
+ /* Props first: methods may still be declared as props. */
256
+ this.defineProps();
257
+ this.ctr.runtimeConfig.methods.forEach(([name, fn]) => {
258
+ this.element[name] = fn.bind(this.elementRef);
259
+ });
260
+ this.batchManager = new BatchManager({
261
+ execHandlerCtx: this.element,
262
+ handlers: this.ctr.batchHandlers,
263
+ // keep notifs until mount so first-connect handlers see them
264
+ clearNotifs: (notifs) =>
265
+ this.element.isMounted || this.element.wasMounted ? {} : notifs,
266
+ });
267
+ this.batchManager.notify("message:constructed", null);
268
+ publicize(["neutron", "constructed"], {
269
+ weakElement: this.elementRef,
270
+ tag: this.ctr.builtConfig.tag,
271
+ });
272
+ /*
273
+ * Two construction/connection paths:
274
+ * 1. Parser / `.innerHTML=` construct+connect in the same tick
275
+ * (`isConnected` is already true here).
276
+ * 2. `createElement()` then a later sync `append` (`isConnected`
277
+ * is still false; `connectedCallback` resolves the queue).
278
+ * Resolving only in `connectedCallback` works for both, but then
279
+ * path 2 elements write attributes under MutationObservers (Quark
280
+ * etc.). Path 1 never trips those MOs if we resolve here.
281
+ * // this.connectedCallback();
282
+ */
283
+ }
284
+ connectedCallback() {
285
+ const execConnect = () => {
286
+ const isFirstMount = !this.element.wasMounted;
287
+ if (this.element.wasMounted) {
288
+ this.reconnectListeners();
289
+ } else {
290
+ this.setConfiguredRenderRoot();
291
+ this.linkBatching();
292
+ this.connectListeners();
293
+ }
294
+ this.batch(() => {
295
+ this.element.isMounted = true;
296
+ if (!this.element.wasMounted) {
297
+ this.batchManager.notify("message:first-mount", null);
298
+ }
299
+ });
300
+ publicize(["neutron", "connected"], {
301
+ weakElement: this.elementRef,
302
+ tag: this.ctr.builtConfig.tag,
303
+ isMoving: this.element.isMoving,
304
+ isFirstMount,
305
+ });
306
+ };
307
+ if (!this.execDisconnect) {
308
+ // first connect, or after a real disconnect
309
+ execConnect();
310
+ } else {
311
+ // sync re-append: a move (`isMoving`)
312
+ this.element.isMoving = true;
313
+ // cancel the queued disconnect microtask
314
+ this.execDisconnect();
315
+ execConnect();
316
+ this.element.isMoving = false;
317
+ }
318
+ }
319
+ connectedMoveCallback() {
320
+ /* Not in every browser. Do not batch: every `isMounted=false`
321
+ * must run before every `isMounted=true`. */
322
+ this.disconnectedCallback();
323
+ this.connectedCallback();
324
+ }
325
+ adoptedCallback() {
326
+ this.element.isAdopted = true;
327
+ }
328
+ disconnectedCallback() {
329
+ const _execDisconnect = () => {
330
+ this.batch(() => {
331
+ this.element.isAdopted = false;
332
+ this.element.wasMounted = true;
333
+ this.element.isMounted = false;
334
+ });
335
+ this.disconnectListeners();
336
+ publicize(["neutron", "disconnected"], {
337
+ weakElement: this.elementRef,
338
+ tag: this.ctr.builtConfig.tag,
339
+ isMoving: this.element.isMoving,
340
+ });
341
+ };
342
+ this.execDisconnect = () => {
343
+ _execDisconnect();
344
+ // one-shot: a second call must not run _execDisconnect again
345
+ this.execDisconnect = null;
346
+ };
347
+ /* If `connectedCallback` runs in this turn (before the microtask),
348
+ * that is a move: `onDisconnected` / `onConnected` still fire, with
349
+ * `isMoving=true`. */
350
+ queueMicrotask(() => {
351
+ this.execDisconnect?.();
352
+ });
353
+ }
354
+ attributeChangedCallback(
355
+ name: string,
356
+ oldValue: string | null,
357
+ newValue: string | null
358
+ ) {
359
+ const propConfig = this.ctr.CustomElement.getPropConfig({ attr: name });
360
+ if (propConfig) {
361
+ const converter = Converter.type(propConfig.type);
362
+ const parsedOldVal = converter.attr.convert(
363
+ propConfig.deserialize(oldValue)
364
+ );
365
+ const parsedNewVal = converter.attr.convert(
366
+ propConfig.deserialize(newValue)
367
+ );
368
+ const propName = propConfig.prop;
369
+ const tokensEqual = (a: unknown[], b: unknown[]) =>
370
+ Array.isArray(a) &&
371
+ Array.isArray(b) &&
372
+ a.length === b.length &&
373
+ a.every((v, i) => v === b[i]);
374
+ const isTokens = propConfig.type === TokenList;
375
+ // TODO can these be compared as raw attr strings?
376
+ if (
377
+ (!isTokens && parsedOldVal !== parsedNewVal) ||
378
+ (isTokens && !tokensEqual(parsedOldVal, parsedNewVal))
379
+ ) {
380
+ this.propStore[propName] = parsedNewVal;
381
+ // the reaction continues whatever chain wrote the attribute (a
382
+ // Quark rule, another element's effect): see LoopGuard
383
+ LoopGuard.run(LoopGuard.depthOf(this.element, name), () =>
384
+ this.batchManager.notify(propName, parsedOldVal)
385
+ );
386
+ }
387
+ }
388
+ }
389
+
390
+ /* --- PRIVATE: INSTANCE HELPERS --- */
391
+ private defineProps() {
392
+ if (this.ctr.runtimeConfig.props) {
393
+ Object.keys(this.ctr.runtimeConfig.props).forEach((propName) => {
394
+ const propConfig = this.ctr.runtimeConfig.props[propName];
395
+ /* Accessors live here; `Neutron()` owns the types. Shared
396
+ * helper so a pre-upgrade observer (Quark `prop()`) stays on
397
+ * top of the base getter/setter. */
398
+ defineObservableProperty(this.element, propName, {
399
+ get: () => propConfig.get(this.element, this.propStore, propConfig),
400
+ set: (newValue) => {
401
+ const oldValue = propConfig.get(
402
+ this.element,
403
+ this.propStore,
404
+ propConfig
405
+ );
406
+ if (newValue !== oldValue) {
407
+ propConfig.set(
408
+ this.element,
409
+ this.propStore,
410
+ propConfig,
411
+ newValue
412
+ );
413
+ if (propConfig.notify === "prop") {
414
+ this.batchManager.notify(propName, oldValue);
415
+ }
416
+ }
417
+ },
418
+ });
419
+ });
420
+ }
421
+ }
422
+ private linkBatching() {
423
+ this.ctr.batchLinkFns.forEach((linkFn) => linkFn(this));
424
+ }
425
+ private connectListeners() {
426
+ this.ctr.runtimeConfig.lifecycles.broadcast.forEach(
427
+ ([channelNames, handler]) => {
428
+ channelNames.forEach((channelName) => {
429
+ CommonElement.addBroadcastListener.apply(this.element, [
430
+ channelName,
431
+ handler.bind(this.elementRef),
432
+ ]);
433
+ });
434
+ }
435
+ );
436
+ [
437
+ ...this.ctr.runtimeConfig.lifecycles.event,
438
+ ...this.ctr.runtimeConfig.lifecycles.eventDefault,
439
+ ].forEach(([eventNames, handler]) => {
440
+ eventNames.forEach((eventName) => {
441
+ CommonElement.addListener.apply(this.element, [
442
+ eventName,
443
+ handler.bind(this.elementRef),
444
+ ]);
445
+ });
446
+ });
447
+ /* One `command` listener per handler; the effector filters by verb. */
448
+ this.ctr.runtimeConfig.lifecycles.command.forEach(([, handler]) => {
449
+ CommonElement.addListener.apply(this.element, [
450
+ COMMAND_EVENT,
451
+ handler.bind(this.elementRef),
452
+ ]);
453
+ });
454
+ }
455
+ private reconnectListeners() {
456
+ /* Re-attach after disconnect, including listeners on linked
457
+ * built-in children. */
458
+ CommonElement.internal_reconnectEventListeners.apply(this.element, []);
459
+ CommonElement.internal_reconnectBroadcastListeners.apply(this.element, []);
460
+ this.getAllRelatedChildElements().forEach((el) => {
461
+ CommonElement.internal_reconnectEventListeners.apply(el, []);
462
+ CommonElement.internal_reconnectBroadcastListeners.apply(el, []);
463
+ });
464
+ }
465
+ private disconnectListeners() {
466
+ /* Drop listeners (and those on linked built-in children) so they
467
+ * cannot leak. */
468
+ CommonElement.internal_disconnectEventListeners.apply(this.element, []);
469
+ CommonElement.internal_disconnectBroadcastListeners.apply(this.element, []);
470
+ this.getAllRelatedChildElements().forEach((el) => {
471
+ CommonElement.internal_disconnectEventListeners.apply(el, []);
472
+ CommonElement.internal_disconnectBroadcastListeners.apply(el, []);
473
+ });
474
+ }
475
+ private getAllRelatedChildElements() {
476
+ return Object.values(this.ctr.runtimeConfig.props)
477
+ .filter((p) => p.type === Element || p.type?.prototype instanceof Element)
478
+ .map((p) => p.get.call(this, this.element.attributes, this.propStore, p))
479
+ .filter((el) => isBuiltInElement(el) && this.element.contains(el));
480
+ }
481
+ private setConfiguredRenderRoot() {
482
+ const rootConfig = this.ctr.runtimeConfig.renderRoot;
483
+ if (rootConfig) {
484
+ const renderRoot = rootConfig.shadow
485
+ ? this.element.attachShadow({
486
+ mode: rootConfig.shadow,
487
+ })
488
+ : this.element;
489
+ const defaultRenderRootElement = createElement(
490
+ rootConfig.tag,
491
+ rootConfig.shadow
492
+ ? {
493
+ style: "display: contents;",
494
+ }
495
+ : {}
496
+ );
497
+ renderRoot.append(
498
+ ...[
499
+ defaultRenderRootElement,
500
+ rootConfig.defaultSlots &&
501
+ createElement("slot", {
502
+ name: "neutron-adopt",
503
+ onslotchange: (e) => {
504
+ const slot = e.target as HTMLSlotElement;
505
+ const shadowDom = slot.getRootNode() as ShadowRoot;
506
+ const templateElements = slot.assignedElements();
507
+ while (slot.nextElementSibling) {
508
+ slot.nextElementSibling.remove();
509
+ }
510
+ templateElements.forEach((templateElement) => {
511
+ if (templateElement instanceof HTMLTemplateElement) {
512
+ const content = document.importNode(
513
+ templateElement.content as Node,
514
+ true
515
+ ) as Element;
516
+ shadowDom.append(content);
517
+ }
518
+ });
519
+ },
520
+ }),
521
+ ].filter(Boolean)
522
+ );
523
+ if (defaultRenderRootElement) {
524
+ this.batch(() => {
525
+ this.element.renderRoot = defaultRenderRootElement;
526
+ });
527
+ }
528
+ }
529
+ }
530
+ batch(fn: AnyFunction) {
531
+ const doLock = !this.batchManager.isLocked;
532
+ try {
533
+ if (doLock) this.batchManager.lock();
534
+ fn();
535
+ } finally {
536
+ if (doLock) {
537
+ // snapshot first: `unlock` → `flushHandlers` → `clearNotifs`
538
+ const changedProps = Object.keys(this.batchManager.notifs);
539
+ this.batchManager.unlock();
540
+ if (changedProps.length) {
541
+ publicize(["neutron", "commit"], {
542
+ weakElement: this.elementRef,
543
+ tag: this.ctr.builtConfig.tag,
544
+ changedProps,
545
+ });
546
+ }
547
+ }
548
+ }
549
+ }
550
+ }
package/src/neutron.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { attachDevtools } from "./devtools-hook";
2
+ import { NeutronElement } from "./neutron-element";
3
+ import { NeutronInternal } from "./neutron-internal";
4
+ import { ElementBuilder, OptsConfig } from "./types";
5
+ import { compose } from "./utils/element";
6
+ import * as DOM from "@excom/kit-utils";
7
+
8
+ // type RenderRootForConfig<Conf extends OptsConfig> =
9
+ // PickRenderRootTag<Conf> extends HTMLElement ? PickRenderRootTag<Conf> : never;
10
+
11
+ /*
12
+ * type NeutronStatics = {
13
+ * DOM: typeof DOM;
14
+ * compose: typeof compose;
15
+ * };
16
+ */
17
+
18
+ export function Neutron<CustomTypes, Conf extends OptsConfig>(
19
+ optsConfig: Conf
20
+ ) {
21
+ // one Internal + one HTMLElement subclass per `Neutron()` call
22
+ class CustomElement extends NeutronElement {}
23
+ class CustomInternal extends NeutronInternal {}
24
+ CustomInternal.setup(optsConfig, CustomElement);
25
+
26
+ return CustomInternal as unknown as ElementBuilder<Conf, CustomTypes>;
27
+ }
28
+
29
+ Neutron.DOM = DOM;
30
+ Neutron.compose = compose;
31
+ Neutron.attachDevtools = attachDevtools;
32
+
33
+ if (import.meta.env.DEV) {
34
+ // @ts-ignore drop once DevTools / UMD cover this
35
+ window.Neutron = Neutron;
36
+ }
@@ -0,0 +1,104 @@
1
+ import type { IgnoredEffectorResult } from "../constants";
2
+ import { TCommonElementListeners, TCommonElementOther } from "./element.types";
3
+ import { AnyFunction, GuaranteedFields, Obj } from "./shared.types";
4
+
5
+ type _CommonElementListenerSignatures = {
6
+ [Key in keyof TCommonElementListeners]?:
7
+ | Parameters<TCommonElementListeners[Key]>
8
+ | IgnoredEffectorResult;
9
+ };
10
+ export interface CommonElementListenerSignatures extends _CommonElementListenerSignatures {}
11
+
12
+ type _CommonElementOtherSignatures = {
13
+ [Key in keyof TCommonElementOther]?:
14
+ | Parameters<TCommonElementOther[Key]>
15
+ | IgnoredEffectorResult;
16
+ };
17
+
18
+ // Must be exported so other packages emit a usable .d.ts
19
+ export interface CommonElementOtherSignatures extends _CommonElementOtherSignatures {}
20
+
21
+ // Must be exported so other packages emit a usable .d.ts
22
+ export interface MetaProps {
23
+ returns?: unknown;
24
+ }
25
+
26
+ // BaseEffect
27
+ export type BaseEffect<Props> = CommonElementOtherSignatures &
28
+ MetaProps &
29
+ Partial<Props>;
30
+
31
+ export type ChildEffect<
32
+ Props,
33
+ BottomOfEffect = never,
34
+ C = GuaranteedFields<Props>,
35
+ > = {
36
+ [Key in keyof C as C[Key] extends HTMLElement ? Key : never]?:
37
+ | (C[Key] extends HTMLElement
38
+ ? BaseEffect<C[Key]> &
39
+ (BottomOfEffect extends never
40
+ ? {}
41
+ : CommonElementListenerSignatures)
42
+ : never)
43
+ | C[Key]
44
+ | null;
45
+ };
46
+
47
+ export type BaseNative = BaseEffect<HTMLElement>;
48
+ export type Effect<
49
+ Props,
50
+ // _BaseEffect = BaseNative & BaseEffect<Props>,
51
+ _BaseEffect = BaseNative & BaseEffect<Props>,
52
+ // _BottomOfEffect = _BaseEffect & ChildEffect<Props>,
53
+ // _EffectWithoutListeners = _BaseEffect & ChildEffect<Props, _BottomOfEffect>,
54
+ _EffectWithoutListeners = _BaseEffect,
55
+ TopLevelListeners = CommonElementListenerSignatures,
56
+ > = (_EffectWithoutListeners & TopLevelListeners) | IgnoredEffectorResult;
57
+
58
+ type ValidateEffect<_CE, _ResultObject, _EffectSpec> = any;
59
+ /*
60
+ * TODO validate return type
61
+ * = EffectSpec;
62
+ * = {
63
+ * [Key in keyof ResultObject]: Key extends keyof EffectSpec
64
+ * ? EffectSpec[Key]
65
+ * : never;
66
+ * };
67
+ * ResultObject: the effect the author returned.
68
+ * EffectSpec: what this element may receive (config, lifecycle, …).
69
+ * : ResultObject extends IgnoredEffectorResult
70
+ * ? IgnoredEffectorResult
71
+ */
72
+ type ValidateEffectorResult<CE, Result, EffectSpec> =
73
+ Result extends Array<Obj>
74
+ ? Array<ValidateEffect<CE, Result[number], EffectSpec>>
75
+ : Result extends Obj
76
+ ? ValidateEffect<CE, Result, EffectSpec>
77
+ : never;
78
+ type CA = unknown[] | void;
79
+
80
+ export type Effector<
81
+ El = any,
82
+ EffectSpec = {},
83
+ CustomArgs extends CA = void,
84
+ // pass `CustomArgs`, or we infer them
85
+ > = AnyFunction extends (
86
+ element: any,
87
+ ...args: infer _CustomArgs
88
+ ) => infer Result
89
+ ? (
90
+ element: El,
91
+ ...args: CustomArgs extends void ? _CustomArgs : CustomArgs
92
+ ) => ValidateEffectorResult<El, Result, EffectSpec>
93
+ : never;
94
+
95
+ export interface EffectorOptions<
96
+ InputValidator = (...args: any) => false | unknown[],
97
+ OutputValidator = (...args: any) => boolean,
98
+ > {
99
+ delayNextTask?: boolean;
100
+ /** Run after the current dispatch / stack, before the next task. */
101
+ delayMicrotask?: boolean;
102
+ validateInput?: InputValidator;
103
+ validateOutput?: OutputValidator;
104
+ }