@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,357 @@
|
|
|
1
|
+
import { CommonElement } from "../common-element";
|
|
2
|
+
import type { IgnoredEffectorResult } from "../constants";
|
|
3
|
+
import { IGNORED_FUNC_VALUES, PROPS_TO_DEEP_MERGE } from "../constants";
|
|
4
|
+
import { publicize } from "../devtools-hook";
|
|
5
|
+
import type { NeutronElement as TNeutronElement } from "../neutron-element";
|
|
6
|
+
import { NeutronError } from "../neutron-error";
|
|
7
|
+
import type { AnyFunction, EffectorOptions, Obj, PropConfig } from "../types";
|
|
8
|
+
import { KitLogger } from "@excom/kit-logger";
|
|
9
|
+
import {
|
|
10
|
+
execWhenReady,
|
|
11
|
+
isPojo,
|
|
12
|
+
LoopGuard,
|
|
13
|
+
tc,
|
|
14
|
+
toArray,
|
|
15
|
+
wait,
|
|
16
|
+
} from "@excom/kit-utils";
|
|
17
|
+
|
|
18
|
+
// END DEV UTILS
|
|
19
|
+
|
|
20
|
+
type _EffectorFn = AnyFunction & { _effectorSrc?: AnyFunction };
|
|
21
|
+
export function effector<
|
|
22
|
+
// `El` is too expensive to type here (should extend NeutronElement)
|
|
23
|
+
El,
|
|
24
|
+
// `T` is too expensive to type here (should extend Effector<any, any>)
|
|
25
|
+
T extends _EffectorFn,
|
|
26
|
+
InputValidator extends (args: Parameters<T>) => false | unknown[],
|
|
27
|
+
// `s` is too expensive to type here (should extend BaseEffect<El>)
|
|
28
|
+
OutputValidator extends (args: Parameters<T>, s: unknown) => boolean,
|
|
29
|
+
>(
|
|
30
|
+
functionToWrap: T,
|
|
31
|
+
{
|
|
32
|
+
delayNextTask,
|
|
33
|
+
delayMicrotask,
|
|
34
|
+
validateInput,
|
|
35
|
+
validateOutput,
|
|
36
|
+
}: EffectorOptions<InputValidator, OutputValidator> = {}
|
|
37
|
+
) {
|
|
38
|
+
if (functionToWrap?._effectorSrc) {
|
|
39
|
+
KitLogger.warn(
|
|
40
|
+
`Function \`${functionToWrap.name}\` is already an effector.`
|
|
41
|
+
);
|
|
42
|
+
return functionToWrap as unknown as typeof wrapper;
|
|
43
|
+
}
|
|
44
|
+
const wrapper = function (
|
|
45
|
+
..._args: Parameters<T>
|
|
46
|
+
): typeof delayNextTask extends true ? Promise<unknown> : unknown {
|
|
47
|
+
const el: El = this instanceof WeakRef ? this.deref() : this;
|
|
48
|
+
|
|
49
|
+
/* A delayed effect (`wait(0)`) still continues the chain that
|
|
50
|
+
* triggered it: carry the loop-guard depth across the timeout. */
|
|
51
|
+
const depth = LoopGuard.current();
|
|
52
|
+
const delay = delayNextTask
|
|
53
|
+
? wait(0)
|
|
54
|
+
: delayMicrotask
|
|
55
|
+
? Promise.resolve()
|
|
56
|
+
: undefined;
|
|
57
|
+
return execWhenReady(delay, () =>
|
|
58
|
+
LoopGuard.run(depth, () => {
|
|
59
|
+
let r;
|
|
60
|
+
const exec = () => {
|
|
61
|
+
const fullArgs = [el, ..._args];
|
|
62
|
+
const args = !validateInput
|
|
63
|
+
? fullArgs
|
|
64
|
+
: validateInput.call(el, fullArgs);
|
|
65
|
+
if (Array.isArray(args)) {
|
|
66
|
+
// `validateInput` accepted these args (array = pass)
|
|
67
|
+
const returnedValues = toArray(functionToWrap.apply(el, args))
|
|
68
|
+
.map((effectorResult) => {
|
|
69
|
+
if (
|
|
70
|
+
effectorResult &&
|
|
71
|
+
(!validateOutput ||
|
|
72
|
+
validateOutput.call(el, args, effectorResult))
|
|
73
|
+
) {
|
|
74
|
+
// `validateOutput` absent or true
|
|
75
|
+
const returnValue = processEffectorResult(
|
|
76
|
+
el,
|
|
77
|
+
effectorResult,
|
|
78
|
+
undefined,
|
|
79
|
+
{ trigger: functionToWrap }
|
|
80
|
+
);
|
|
81
|
+
/* Meta for Neutron: how to treat each mutation's
|
|
82
|
+
* result (`returns` vs ignore). */
|
|
83
|
+
return {
|
|
84
|
+
value: returnValue,
|
|
85
|
+
returnsKeyExists: "returns" in effectorResult,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
.filter((r) => r?.returnsKeyExists)
|
|
90
|
+
.reduce((acc, { value }) => [...acc, value], []);
|
|
91
|
+
// 0 → undefined, 1 → that value, 2+ → array
|
|
92
|
+
r = returnedValues.length < 2 ? returnedValues[0] : returnedValues;
|
|
93
|
+
} else {
|
|
94
|
+
r = undefined;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
try {
|
|
98
|
+
// @ts-ignore
|
|
99
|
+
if (el?._n_?.batch) {
|
|
100
|
+
(el as unknown as TNeutronElement)?._n_.batch(exec);
|
|
101
|
+
} else {
|
|
102
|
+
exec();
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (el) {
|
|
106
|
+
const err = error as { message?: string; name?: string } | null;
|
|
107
|
+
publicize(["neutron", "error"], {
|
|
108
|
+
weakElement: new WeakRef(el as unknown as Element),
|
|
109
|
+
tag: (el as unknown as Element).localName,
|
|
110
|
+
errorMessage: err?.message ? String(err.message) : String(error),
|
|
111
|
+
errorName: err?.name ? String(err.name) : undefined,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
// `onError` present: notify. Otherwise rethrow.
|
|
115
|
+
if (
|
|
116
|
+
// @ts-ignore
|
|
117
|
+
el?._n_?.ctr?.runtimeConfig?.lifecycles?.error?.length > 0
|
|
118
|
+
) {
|
|
119
|
+
(el as unknown as TNeutronElement)?._n_.batchManager.notify(
|
|
120
|
+
"message:error",
|
|
121
|
+
error
|
|
122
|
+
);
|
|
123
|
+
} else {
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return r;
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
};
|
|
131
|
+
wrapper._effectorSrc = functionToWrap;
|
|
132
|
+
return wrapper;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const throwCallError = (element, fnName, value) => {
|
|
136
|
+
const val = (value?.toString() ? value?.toString() : value?.constructor?.name)
|
|
137
|
+
?.replace?.(/\n/g, " ")
|
|
138
|
+
?.slice?.(0, 10);
|
|
139
|
+
throw new NeutronError(
|
|
140
|
+
`Cannot call function \`${fnName}\` on ${
|
|
141
|
+
element.localName
|
|
142
|
+
} - arguments must be an array. Received: ${
|
|
143
|
+
val ? `\`${val}\`...` : "unknown"
|
|
144
|
+
}`
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const isChildEffect = (val: unknown, propConfig: PropConfig) =>
|
|
149
|
+
(propConfig.type === Element ||
|
|
150
|
+
propConfig.type?.prototype instanceof Element) &&
|
|
151
|
+
isPojo(val);
|
|
152
|
+
|
|
153
|
+
const effectLockDepth = new WeakMap<object, number>();
|
|
154
|
+
|
|
155
|
+
export const processEffectorResult = (
|
|
156
|
+
// `NeutronElement` is too expensive to type here
|
|
157
|
+
element: any,
|
|
158
|
+
_effectorResult: IgnoredEffectorResult | Obj,
|
|
159
|
+
// same for `_parent`
|
|
160
|
+
_parent?: any,
|
|
161
|
+
opts?: any
|
|
162
|
+
): void | unknown => {
|
|
163
|
+
let returnValue;
|
|
164
|
+
if (isPojo(_effectorResult)) {
|
|
165
|
+
const effect = _effectorResult as Obj;
|
|
166
|
+
const nInternalInstance = element?._n_?.ctr ? element._n_ : undefined;
|
|
167
|
+
|
|
168
|
+
const tasks: any = {
|
|
169
|
+
returns: () => {},
|
|
170
|
+
events: {
|
|
171
|
+
remove: [],
|
|
172
|
+
add: [],
|
|
173
|
+
fire: [],
|
|
174
|
+
},
|
|
175
|
+
props: {
|
|
176
|
+
elements: [],
|
|
177
|
+
other: [],
|
|
178
|
+
provision: () => {},
|
|
179
|
+
},
|
|
180
|
+
childEffects: [],
|
|
181
|
+
functions: [],
|
|
182
|
+
};
|
|
183
|
+
Object.entries(effect).forEach(([key, value]) => {
|
|
184
|
+
const isArray = Array.isArray(value);
|
|
185
|
+
if (typeof key !== "string") {
|
|
186
|
+
throw new NeutronError(
|
|
187
|
+
`Cannot process effect \`${key}\` on ${element.localName} - key must be a string.`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
if (key === "returns" && !_parent) {
|
|
191
|
+
tasks.returns = () => (returnValue = value);
|
|
192
|
+
} else if (typeof CommonElement[key] === "function") {
|
|
193
|
+
if (!IGNORED_FUNC_VALUES.includes(value)) {
|
|
194
|
+
if (!isArray) {
|
|
195
|
+
throwCallError(element, key, value);
|
|
196
|
+
}
|
|
197
|
+
const mutationKey =
|
|
198
|
+
key.startsWith("emit") ||
|
|
199
|
+
key.startsWith("broadcast") ||
|
|
200
|
+
key.startsWith("command")
|
|
201
|
+
? "fire"
|
|
202
|
+
: key.startsWith("remove")
|
|
203
|
+
? "remove"
|
|
204
|
+
: "add";
|
|
205
|
+
tasks.events[mutationKey].push(() =>
|
|
206
|
+
CommonElement[key].apply(element, value)
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
/* BUG: happy-dom omits property keys on elements, so this
|
|
211
|
+
* check fails tests. `!import.meta.env.TEST` skips the throw
|
|
212
|
+
* there; test vs non-test behavior now differs. */
|
|
213
|
+
if (!(key in element) && !import.meta.env.TEST) {
|
|
214
|
+
throw new NeutronError(
|
|
215
|
+
`Cannot set property \`${key}\` on ${element.localName} - property does not exist.`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const propType = tc(
|
|
219
|
+
() =>
|
|
220
|
+
nInternalInstance?.ctr?.CustomElement?.getPropConfig?.({
|
|
221
|
+
prop: key,
|
|
222
|
+
})?.type
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// cache: `element[key]` may run a getter
|
|
226
|
+
const elementProp = element[key];
|
|
227
|
+
if (
|
|
228
|
+
typeof elementProp === "function" &&
|
|
229
|
+
(!propType || propType === Function)
|
|
230
|
+
) {
|
|
231
|
+
/* Prefer the custom-element prop when it clashes with a
|
|
232
|
+
* later native API (e.g. CE `boundingRect` as tokens vs a
|
|
233
|
+
* future native function of the same name). */
|
|
234
|
+
if (!IGNORED_FUNC_VALUES.includes(value)) {
|
|
235
|
+
if (!isArray) {
|
|
236
|
+
throwCallError(element, key, value);
|
|
237
|
+
}
|
|
238
|
+
tasks.functions.push(() => elementProp.call(element, ...value));
|
|
239
|
+
}
|
|
240
|
+
} else if (PROPS_TO_DEEP_MERGE.includes(key)) {
|
|
241
|
+
try {
|
|
242
|
+
tasks.props.other.push(() => Object.assign(elementProp, value));
|
|
243
|
+
} catch (e) {
|
|
244
|
+
throw new NeutronError(
|
|
245
|
+
`Cannot set property \`${key}\` on ${element.localName} ${
|
|
246
|
+
isPojo(value) ? "" : "- value must be an object."
|
|
247
|
+
}`
|
|
248
|
+
);
|
|
249
|
+
// deep-merge (`Object.assign`) failed
|
|
250
|
+
}
|
|
251
|
+
} else if (
|
|
252
|
+
key === "renderRoot" ||
|
|
253
|
+
propType === Element ||
|
|
254
|
+
propType?.prototype instanceof Element
|
|
255
|
+
) {
|
|
256
|
+
// element-typed: assign, null, or recurse into a POJO
|
|
257
|
+
if (value?.nodeName) {
|
|
258
|
+
// replace the child element
|
|
259
|
+
tasks.props.elements.push(() => (element[key] = value));
|
|
260
|
+
} else if (value === null) {
|
|
261
|
+
tasks.props.elements.push(() => (element[key] = null));
|
|
262
|
+
} else if (isPojo(value)) {
|
|
263
|
+
// POJO: apply as a child effect on the current element
|
|
264
|
+
tasks.childEffects.push(() => {
|
|
265
|
+
if (!(elementProp instanceof Element)) {
|
|
266
|
+
throw new NeutronError(
|
|
267
|
+
`Cannot set properties of \`${key}\` on element. Element must be set as a property first.`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
processEffectorResult(
|
|
271
|
+
elementProp as TNeutronElement,
|
|
272
|
+
value,
|
|
273
|
+
element,
|
|
274
|
+
opts
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
} else {
|
|
278
|
+
throw new NeutronError(
|
|
279
|
+
`Cannot set property \`${key}\` on ${element.localName} - value must be an element or an object.`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
} else if (key === "provision") {
|
|
283
|
+
tasks.props.provision = () => (element[key] = value);
|
|
284
|
+
} else {
|
|
285
|
+
tasks.props.other.push(() => (element[key] = value));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const execEffect = () => {
|
|
291
|
+
tasks.returns();
|
|
292
|
+
// remove before add
|
|
293
|
+
tasks.events.remove.forEach((fn) => fn());
|
|
294
|
+
tasks.events.add.forEach((fn) => fn());
|
|
295
|
+
// elements before childEffects
|
|
296
|
+
tasks.props.elements.forEach((fn) => fn());
|
|
297
|
+
tasks.childEffects.forEach((fn) => fn());
|
|
298
|
+
tasks.props.other.forEach((fn) => fn());
|
|
299
|
+
/* Functions after elements/props so `cancelRequest: []`,
|
|
300
|
+
* `setCustomValidity: []`, `submit: []` see the updates. */
|
|
301
|
+
tasks.functions.forEach((fn) => fn());
|
|
302
|
+
/* `provision` after props: its event listeners may read them. */
|
|
303
|
+
tasks.props.provision();
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const bm = nInternalInstance?.batchManager;
|
|
307
|
+
const lockDepth = (effectLockDepth.get(element) ?? 0) + 1;
|
|
308
|
+
effectLockDepth.set(element, lockDepth);
|
|
309
|
+
publicize(["neutron", "effect"], {
|
|
310
|
+
weakElement: new WeakRef(element as Element),
|
|
311
|
+
tag: element.localName,
|
|
312
|
+
signature: opts?.trigger?._logSignature ?? null,
|
|
313
|
+
effect,
|
|
314
|
+
lockDepth,
|
|
315
|
+
oldProps: bm?.notifs,
|
|
316
|
+
newProps: bm
|
|
317
|
+
? Object.fromEntries(
|
|
318
|
+
Object.entries(bm.notifs).map(([key]) => [key, element[key]])
|
|
319
|
+
)
|
|
320
|
+
: undefined,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
execEffect();
|
|
325
|
+
// emit last: the rest of the effect has landed
|
|
326
|
+
tasks.events.fire.forEach((fn) => fn());
|
|
327
|
+
} finally {
|
|
328
|
+
effectLockDepth.set(element, lockDepth - 1);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return returnValue;
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
export const setDebugLifecycleSignature = (
|
|
335
|
+
fn: AnyFunction,
|
|
336
|
+
lifecycleName: string,
|
|
337
|
+
nameArray: string[]
|
|
338
|
+
) => {
|
|
339
|
+
/*
|
|
340
|
+
* Mirror the call-site syntax: a single name is registered as a bare
|
|
341
|
+
* string (`onPropSet("label")`), multiple names as an array
|
|
342
|
+
* (`onPropSet(["a", "b"])`).
|
|
343
|
+
*/
|
|
344
|
+
const quoted = nameArray.map((name) => `"${name}"`).join(", ");
|
|
345
|
+
const args = nameArray.length === 1 ? quoted : `[${quoted}]`;
|
|
346
|
+
(fn as AnyFunction & { _logSignature?: string })._logSignature = `on${
|
|
347
|
+
lifecycleName.charAt(0).toUpperCase() + lifecycleName.slice(1)
|
|
348
|
+
}(${args})`;
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
export const setDebugMethodSignature = (
|
|
352
|
+
fn: AnyFunction,
|
|
353
|
+
methodName: string
|
|
354
|
+
) => {
|
|
355
|
+
(fn as AnyFunction & { _logSignature?: string })._logSignature =
|
|
356
|
+
`${methodName}(...)`;
|
|
357
|
+
};
|