@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.
- package/.rush/temp/chunked-rush-logs/neutron.apply-exports.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/neutron.build_package-metas.chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/all.log +1 -0
- package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/state.json +3 -0
- package/.rush/temp/operation/build_package-metas/all.log +1 -0
- package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_package-metas/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +3 -0
- package/config/rig.json +6 -0
- package/index.ts +9 -0
- package/package.json +45 -0
- package/rush-logs/neutron.apply-exports.cache.log +1 -0
- package/rush-logs/neutron.apply-exports.log +1 -0
- package/rush-logs/neutron.build_package-metas.cache.log +1 -0
- package/rush-logs/neutron.build_package-metas.log +1 -0
- package/src/command.ts +102 -0
- package/src/common-element.ts +377 -0
- package/src/constants.ts +101 -0
- package/src/devtools-hook.ts +93 -0
- package/src/lifecycle-configs.ts +304 -0
- package/src/neutron-element.ts +72 -0
- package/src/neutron-error.ts +6 -0
- package/src/neutron-internal.ts +550 -0
- package/src/neutron.ts +36 -0
- package/src/types/effect.types.ts +104 -0
- package/src/types/element.types.ts +263 -0
- package/src/types/index.ts +4 -0
- package/src/types/new.types.ts +159 -0
- package/src/types/shared.types.ts +25 -0
- package/src/utils/effect.ts +357 -0
- package/src/utils/element.ts +382 -0
- package/src/utils/index.ts +2 -0
- package/support/docs/COMMANDS.md +58 -0
- package/support/docs/COMPOSE.md +32 -0
- package/support/docs/DEBUG.md +11 -0
- package/support/docs/DEFINE.md +20 -0
- package/support/docs/EFFECTS.md +49 -0
- package/support/docs/EVENTS.md +57 -0
- package/support/docs/LIFECYCLES.md +69 -0
- package/support/docs/METHODS.md +49 -0
- package/support/docs/PROMISE_PROPS.md +29 -0
- package/support/docs/PROPS.md +64 -0
- package/support/docs/PROP_REACTIONS.md +35 -0
- package/support/docs/PROVISION.md +31 -0
- package/support/docs/README.md +118 -0
- package/support/docs/RECOMPOSE.md +70 -0
- package/support/docs/TYPESCRIPT.md +51 -0
- package/support/docs-sections.json +42 -0
- package/support/package-meta.json +129 -0
- package/support/tests/commands.test.ts +330 -0
- package/support/tests/common-element.test.ts +342 -0
- package/support/tests/devtools-hook.test.ts +209 -0
- package/support/tests/devtools-renderer.test.ts +125 -0
- package/support/tests/effects.test.ts +253 -0
- package/support/tests/element-config.test.ts +331 -0
- package/support/tests/entry.test.ts +68 -0
- package/support/tests/lifecycles.test.ts +489 -0
- package/support/tests/loop-guard.test.ts +162 -0
- package/support/tests/neutron.test.ts +1286 -0
- package/support/tests/recompose.test.ts +129 -0
- package/support/tests/utils.test.ts +75 -0
- 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
|
+
}
|