@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,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
+ };