@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,93 @@
1
+ import {
2
+ type DevtoolsRenderer,
3
+ injectRenderersIfNeeded,
4
+ NUCLEUS_DEVTOOLS_HOOK_VERSION,
5
+ registerRenderer,
6
+ toDevtoolsJson,
7
+ } from "@excom/kit-devtools";
8
+
9
+ /**
10
+ * Nucleus DevTools probe. The hook lives in `@excom/kit-devtools`;
11
+ * this re-exports it and adds the Neutron renderer.
12
+ *
13
+ * publicize(["neutron", "defined"], { tag, props }): once per `setup()`,
14
+ * no `weakElement`; extension audits attr names from it
15
+ * publicize(["neutron", "constructed"], { weakElement, tag })
16
+ * publicize(["neutron", "connected"], { weakElement, tag, isMoving, isFirstMount })
17
+ * publicize(["neutron", "disconnected"], { weakElement, tag, isMoving })
18
+ * publicize(["neutron", "effect"], { weakElement, tag, signature, effect, lockDepth, … })
19
+ * publicize(["neutron", "commit"], { weakElement, tag, changedProps })
20
+ * publicize(["neutron", "error"], { weakElement, tag, errorMessage, errorName })
21
+ */
22
+ export {
23
+ attachDevtools,
24
+ DEVTOOLS_SERIALIZERS,
25
+ type DevtoolsHook,
26
+ type DevtoolsRenderer,
27
+ type DevtoolsRendererKind,
28
+ getDevtoolsHook,
29
+ NUCLEUS_DEVTOOLS_HOOK_KEY,
30
+ NUCLEUS_DEVTOOLS_HOOK_VERSION,
31
+ pathMatches,
32
+ publicize,
33
+ type PublicizeMeta,
34
+ type PublicizePath,
35
+ } from "@excom/kit-devtools";
36
+
37
+ export type NeutronInspectSnapshot = {
38
+ tag: string;
39
+ id: string | null;
40
+ isMounted: boolean;
41
+ wasMounted: boolean;
42
+ isMoving: boolean;
43
+ isAdopted: boolean;
44
+ propNames: string[];
45
+ /** Primitive-safe prop values. Element/object refs are tagged, not retained. */
46
+ props: Record<string, unknown>;
47
+ };
48
+
49
+ export type NeutronRenderer = DevtoolsRenderer & {
50
+ kind: "neutron";
51
+ walkRoot: () => Element;
52
+ isNeutronElement: (el: Element) => boolean;
53
+ inspect: (el: Element) => NeutronInspectSnapshot | null;
54
+ };
55
+
56
+ const renderer: NeutronRenderer = {
57
+ version: NUCLEUS_DEVTOOLS_HOOK_VERSION,
58
+ kind: "neutron",
59
+ walkRoot: () => document.body,
60
+ isNeutronElement: (el) => !!(el as any)?._n_?.ctr,
61
+ inspect: (el) => {
62
+ const internal = (el as any)?._n_;
63
+ if (!internal?.ctr) return null;
64
+ const propNames = Object.keys(internal.ctr.runtimeConfig?.props ?? {});
65
+ const props: Record<string, unknown> = {};
66
+ for (const name of propNames) {
67
+ try {
68
+ props[name] = toDevtoolsJson(el[name as keyof typeof el]);
69
+ } catch {
70
+ props[name] = "[unreadable]";
71
+ }
72
+ }
73
+ return {
74
+ tag: el.localName,
75
+ id: el.id || null,
76
+ isMounted: !!el["isMounted" as keyof typeof el],
77
+ wasMounted: !!el["wasMounted" as keyof typeof el],
78
+ isMoving: !!el["isMoving" as keyof typeof el],
79
+ isAdopted: !!el["isAdopted" as keyof typeof el],
80
+ propNames,
81
+ props,
82
+ };
83
+ },
84
+ };
85
+
86
+ registerRenderer(renderer);
87
+
88
+ /**
89
+ * Inject the Neutron renderer into an installed hook (no-op when none, or
90
+ * when already injected). Called on the first `define()` after a hook is
91
+ * installed at `document_start`; late attachers use `attachDevtools()`.
92
+ */
93
+ export const injectRendererIfNeeded = injectRenderersIfNeeded;
@@ -0,0 +1,304 @@
1
+ import { isCustomCommand, TCommandEvent } from "./command";
2
+ import { NeutronError } from "./neutron-error";
3
+ import type { NeutronInternal as NeutronInternalType } from "./neutron-internal";
4
+ import { AnyFunction, EffectorOptions } from "./types";
5
+ import { isChildEffect, setDebugLifecycleSignature } from "./utils";
6
+ import {
7
+ Converter,
8
+ ExecHandlerFn,
9
+ toArray,
10
+ unique,
11
+ } from "@excom/kit-utils";
12
+
13
+ interface TLifecycleConfig {
14
+ key: string;
15
+ isBatched: boolean;
16
+ batchPrefix?: string;
17
+ triggerFn?: (
18
+ instance: NeutronInternalType,
19
+ batchPrefix: string,
20
+ name: string
21
+ ) => void;
22
+ batchNames?: (names: string[]) => string[];
23
+ batchExecFn?: ExecHandlerFn;
24
+ effectorOptions?: (nameArray: string[]) => EffectorOptions;
25
+ }
26
+
27
+ /*
28
+ * Order matters: several lifecycles share batch names, and earlier
29
+ * entries run first. `.onConnected()` must beat `.onPropSet("isMounted")`.
30
+ */
31
+ export const LifecycleConfigs: TLifecycleConfig[] = [
32
+ {
33
+ key: "constructed",
34
+ isBatched: true,
35
+ batchNames: () => ["message:constructed"],
36
+ },
37
+ {
38
+ key: "connected",
39
+ isBatched: true,
40
+ batchNames: () => ["isMounted"],
41
+ effectorOptions: () => ({
42
+ validateInput: ([element]) => element.isMounted && [element],
43
+ }),
44
+ },
45
+ {
46
+ key: "adopted",
47
+ isBatched: true,
48
+ batchNames: () => ["isAdopted"],
49
+ effectorOptions: () => ({
50
+ validateInput: ([element]) => element.isAdopted && [element],
51
+ }),
52
+ },
53
+ {
54
+ key: "error",
55
+ isBatched: true,
56
+ batchNames: () => ["message:error"],
57
+ effectorOptions: () => ({
58
+ validateInput: ([element, notifs]) => [element, notifs["message:error"]],
59
+ }),
60
+ },
61
+ {
62
+ key: "effect",
63
+ batchNames: (nameArray) => [...nameArray, "message:first-mount"],
64
+ isBatched: true,
65
+ effectorOptions: (nameArray) => ({
66
+ validateInput: ([element, previous]) =>
67
+ (element.isMounted || element.wasMounted) &&
68
+ !!nameArray.find((name) => name in previous) && [element, previous],
69
+ }),
70
+ },
71
+ {
72
+ key: "propUnset",
73
+ isBatched: true,
74
+ batchNames: (nameArray) => [...nameArray, "message:first-mount"],
75
+ effectorOptions: (nameArray) => ({
76
+ validateInput: ([element, previous]) =>
77
+ (element.isMounted || element.wasMounted) &&
78
+ !!Object.keys(previous).find(
79
+ (name) =>
80
+ nameArray.includes(name) &&
81
+ !(
82
+ Converter.type(
83
+ element._n_.ctr.CustomElement.getPropConfig({ prop: name })!
84
+ .type
85
+ )?.prop.isTruthy(element[name]) ?? element[name]
86
+ )
87
+ ) && [element, previous],
88
+ validateOutput: ([element], mutation) =>
89
+ nameArray.every((name) => {
90
+ if (
91
+ name in mutation &&
92
+ !mutation[name] &&
93
+ !isChildEffect(
94
+ mutation[name],
95
+ element._n_.ctr.CustomElement.getPropConfig({ prop: name })!
96
+ )
97
+ ) {
98
+ throw new NeutronError(
99
+ `Cannot unset prop ${name} in its own onPropUnset handler.`
100
+ );
101
+ }
102
+ return true;
103
+ }),
104
+ }),
105
+ },
106
+ {
107
+ key: "propSet",
108
+ isBatched: true,
109
+ batchNames: (nameArray) => [...nameArray, "message:first-mount"],
110
+ effectorOptions: (nameArray) => ({
111
+ validateInput: ([element, previous]) =>
112
+ (element.isMounted || element.wasMounted) &&
113
+ !!Object.keys(previous).find(
114
+ (name) =>
115
+ nameArray.includes(name) &&
116
+ (Converter.type(
117
+ element._n_.ctr.CustomElement.getPropConfig({ prop: name })!.type
118
+ )?.prop.isTruthy(element[name]) ??
119
+ !!element[name])
120
+ ) && [element, previous],
121
+ validateOutput: ([element], mutation) =>
122
+ nameArray.every((name) => {
123
+ // child-element effects in own handlers are ok: `.onPropSet("myElement", () => ({myElement: {addListener: [...]}}))`
124
+ if (
125
+ mutation[name] &&
126
+ !isChildEffect(
127
+ mutation[name],
128
+ element._n_.ctr.CustomElement.getPropConfig({ prop: name })!
129
+ )
130
+ ) {
131
+ throw new NeutronError(
132
+ `Cannot set prop ${name} in its own onPropSet handler.`
133
+ );
134
+ }
135
+ return true;
136
+ }),
137
+ }),
138
+ },
139
+ {
140
+ key: "propChanged",
141
+ isBatched: true,
142
+ batchNames: (nameArray) => [...nameArray, "message:first-mount"],
143
+ effectorOptions: (nameArray) => ({
144
+ validateInput: ([element, previous]) =>
145
+ (element.isMounted || element.wasMounted) &&
146
+ !!nameArray.find((name) => name in previous) && [element, previous],
147
+ validateOutput: ([element], mutation) =>
148
+ nameArray.every((name) => {
149
+ if (
150
+ name in mutation &&
151
+ !isChildEffect(
152
+ mutation[name],
153
+ element._n_.ctr.CustomElement.getPropConfig({ prop: name })!
154
+ )
155
+ ) {
156
+ throw new NeutronError(
157
+ `Cannot change prop ${name} in its own onPropChanged handler.`
158
+ );
159
+ }
160
+ return true;
161
+ }),
162
+ }),
163
+ },
164
+ {
165
+ key: "promiseResolved",
166
+ isBatched: true,
167
+ batchPrefix: "promise:resolved:",
168
+ triggerFn: linkPromiseToBatch("onResolved"),
169
+ effectorOptions: () => ({
170
+ validateInput: validatePromiseArgs("promise:resolved:"),
171
+ }),
172
+ },
173
+ {
174
+ key: "promiseRejected",
175
+ isBatched: true,
176
+ batchPrefix: "promise:rejected:",
177
+ triggerFn: linkPromiseToBatch("onRejected"),
178
+ effectorOptions: () => ({
179
+ validateInput: validatePromiseArgs("promise:rejected:"),
180
+ }),
181
+ },
182
+ {
183
+ key: "broadcast",
184
+ isBatched: false,
185
+ },
186
+ {
187
+ key: "event",
188
+ isBatched: false,
189
+ },
190
+ {
191
+ key: "eventDefault",
192
+ isBatched: false,
193
+ effectorOptions: () => ({
194
+ delayNextTask: true,
195
+ validateInput: ([element, e]: [any, Event]) =>
196
+ element === e.target && !e.defaultPrevented && [element, e],
197
+ }),
198
+ },
199
+ {
200
+ /*
201
+ * `command` events (HTML Command API) never bubble; the at-target
202
+ * check only guards against a hand-made bubbling `Event("command")`.
203
+ * Runs in a microtask so every listener of the dispatch may still
204
+ * `preventDefault()`, yet the user activation that invoked it (a
205
+ * click) is intact for permission prompts and popups.
206
+ */
207
+ key: "command",
208
+ isBatched: false,
209
+ effectorOptions: (names) => ({
210
+ delayMicrotask: true,
211
+ validateInput: ([element, e]: [any, TCommandEvent]) =>
212
+ element === e.target &&
213
+ !e.defaultPrevented &&
214
+ names.includes(e.command) && [element, e],
215
+ }),
216
+ },
217
+ {
218
+ key: "disconnected",
219
+ isBatched: true,
220
+ batchNames: () => ["isMounted"],
221
+ effectorOptions: () => ({
222
+ validateInput: ([element]) => !element.isMounted && [element],
223
+ }),
224
+ },
225
+ ];
226
+
227
+ export const LifecycleConfigMap: Record<
228
+ TLifecycleConfig["key"],
229
+ TLifecycleConfig
230
+ > = LifecycleConfigs.reduce(
231
+ (acc, config) => {
232
+ acc[config.key] = config;
233
+ return acc;
234
+ },
235
+ {} as Record<TLifecycleConfig["key"], TLifecycleConfig>
236
+ );
237
+
238
+ function linkPromiseToBatch(method) {
239
+ return (instance, batchPrefix, name) => {
240
+ instance.queueManager.getQueue(name)[method]((state) => {
241
+ instance.batchManager.notify(batchPrefix + name, state.value);
242
+ });
243
+ };
244
+ }
245
+ function validatePromiseArgs(batchPrefix: string) {
246
+ return ([element, result]) => [
247
+ element,
248
+ Object.fromEntries(
249
+ Object.entries(result as Record<string, unknown>)
250
+ .filter(([key]) => key.startsWith(batchPrefix))
251
+ .map(([key, value]) => [key.replace(/(.*):/, ""), value])
252
+ ),
253
+ ];
254
+ }
255
+
256
+ export const registerLifecycle = (
257
+ ctr: typeof NeutronInternalType,
258
+ lifecycleName: TLifecycleConfig["key"],
259
+ nameArg: string | string[],
260
+ fn: AnyFunction
261
+ ) => {
262
+ const nameArray = unique(toArray(nameArg));
263
+ if (lifecycleName === "command") {
264
+ nameArray.forEach((name) => {
265
+ if (!isCustomCommand(name)) {
266
+ throw new NeutronError(
267
+ `Command names must start with "--" (got "${name}"): built-in commands never reach a custom element.`
268
+ );
269
+ }
270
+ });
271
+ }
272
+ setDebugLifecycleSignature(fn, lifecycleName, nameArray);
273
+ ctr.builtConfig.lifecycles[lifecycleName].push([nameArray, fn]);
274
+ return ctr;
275
+ };
276
+
277
+ export const unregisterLifecycle = (
278
+ ctr: typeof NeutronInternalType,
279
+ lifecycleName: TLifecycleConfig["key"],
280
+ nameArg: string | string[],
281
+ fn: AnyFunction
282
+ ) => {
283
+ const nameArray = unique(toArray(nameArg));
284
+ /* Drop the entry when every name matches `entry[0]`; otherwise
285
+ * strip only the matching names from `entry[0]`. */
286
+ ctr.builtConfig.lifecycles[lifecycleName] = ctr.builtConfig.lifecycles[
287
+ lifecycleName
288
+ ]
289
+ .map(([_entryNames, entryFn]) => {
290
+ if (entryFn === fn) {
291
+ const entryNames = _entryNames.filter(
292
+ (name) => !nameArray.includes(name)
293
+ );
294
+ if (entryNames.length === 0) {
295
+ return false;
296
+ } else {
297
+ return [entryNames, entryFn];
298
+ }
299
+ }
300
+ return [_entryNames, entryFn];
301
+ })
302
+ .filter(Boolean) as [string[], AnyFunction][];
303
+ return ctr;
304
+ };
@@ -0,0 +1,72 @@
1
+ import type { NeutronInternal as TNeutronInternal } from "./neutron-internal";
2
+ import type { PropConfig, RuntimeConfig } from "./types";
3
+
4
+ export class NeutronElement extends HTMLElement {
5
+ static observedAttributes: string[] = [];
6
+ static NeutronInternal: typeof TNeutronInternal;
7
+
8
+ /* --- PUBLIC: STATIC INTROSPECTION --- */
9
+ /**
10
+ * The element's runtime configuration, its `tag`, every prop's
11
+ * `PropConfig` (keyed by prop name: `prop`, `attr`, `type`, defaults,
12
+ * `notify`, …), events, broadcasts, methods and lifecycles, as built by
13
+ * `define()`. `undefined` before the element is defined. Reach it from an
14
+ * instance through its constructor (`el.constructor.getConfig()`) or from
15
+ * the registry (`customElements.get("my-tag").getConfig()`). Read-only by
16
+ * contract: mutating it changes the running definition.
17
+ */
18
+ static getConfig(): RuntimeConfig | undefined {
19
+ return this.NeutronInternal?.runtimeConfig;
20
+ }
21
+ /**
22
+ * One prop's `PropConfig`, looked up by attribute name (`{ attr: "is-open" }`)
23
+ * or prop name (`{ prop: "isOpen" }`). `undefined` when unknown or before
24
+ * `define()`.
25
+ */
26
+ static getPropConfig({
27
+ attr,
28
+ prop,
29
+ }: {
30
+ attr?: string;
31
+ prop?: string;
32
+ }): PropConfig | undefined {
33
+ return Object.values(this.getConfig()?.props ?? {}).find((c) =>
34
+ attr ? c.attr === attr : c.prop === prop
35
+ );
36
+ }
37
+ _n_: TNeutronInternal;
38
+ isMounted: boolean;
39
+ isAdopted: boolean;
40
+ wasMounted: boolean;
41
+ isMoving: boolean;
42
+ renderRoot?: HTMLElement;
43
+
44
+ constructor() {
45
+ super();
46
+ const NeutronInternal = (
47
+ this.constructor as unknown as typeof NeutronElement
48
+ ).NeutronInternal;
49
+ this._n_ = new NeutronInternal(this);
50
+ }
51
+
52
+ /* Native lifecycle callbacks */
53
+ connectedCallback() {
54
+ this._n_.connectedCallback();
55
+ }
56
+ connectedMoveCallback() {
57
+ this._n_.connectedMoveCallback();
58
+ }
59
+ adoptedCallback() {
60
+ this._n_.adoptedCallback();
61
+ }
62
+ disconnectedCallback() {
63
+ this._n_.disconnectedCallback();
64
+ }
65
+ attributeChangedCallback(
66
+ name: string,
67
+ oldValue: string | null,
68
+ newValue: string | null
69
+ ) {
70
+ this._n_.attributeChangedCallback(name, oldValue, newValue);
71
+ }
72
+ }
@@ -0,0 +1,6 @@
1
+ export class NeutronError extends Error {
2
+ constructor(message: string) {
3
+ super(message);
4
+ this.name = this.constructor.name;
5
+ }
6
+ }