@fluixi/devtools 0.2.0-alpha.5 → 0.2.0-alpha.7

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 (50) hide show
  1. package/dist/callsite.cjs +9 -0
  2. package/dist/callsite.d.ts.map +1 -1
  3. package/dist/callsite.js +9 -0
  4. package/dist/callsite.mjs +9 -0
  5. package/dist/export.cjs +1 -0
  6. package/dist/export.mjs +1 -0
  7. package/dist/graph-view.cjs +12 -3
  8. package/dist/graph-view.d.ts.map +1 -1
  9. package/dist/graph-view.js +11 -2
  10. package/dist/graph-view.mjs +12 -3
  11. package/dist/hook.cjs +173 -21
  12. package/dist/hook.mjs +174 -21
  13. package/dist/index.cjs +199 -29
  14. package/dist/index.mjs +200 -29
  15. package/dist/inspector.cjs +6 -2
  16. package/dist/inspector.js +1 -1
  17. package/dist/inspector.mjs +6 -2
  18. package/dist/instrument.cjs +9 -3
  19. package/dist/instrument.d.ts +16 -1
  20. package/dist/instrument.d.ts.map +1 -1
  21. package/dist/instrument.mjs +9 -3
  22. package/dist/kind-filter.cjs +5 -2
  23. package/dist/kind-filter.js +3 -3
  24. package/dist/kind-filter.mjs +5 -2
  25. package/dist/observe.cjs +172 -21
  26. package/dist/observe.d.ts.map +1 -1
  27. package/dist/observe.js +191 -22
  28. package/dist/observe.mjs +173 -21
  29. package/dist/palette.cjs +4 -1
  30. package/dist/palette.d.ts.map +1 -1
  31. package/dist/palette.js +3 -0
  32. package/dist/palette.mjs +4 -1
  33. package/dist/plugins/index.cjs +1 -1
  34. package/dist/plugins/index.mjs +1 -1
  35. package/dist/source.cjs +18 -3
  36. package/dist/source.d.ts +16 -1
  37. package/dist/source.d.ts.map +1 -1
  38. package/dist/source.js +25 -3
  39. package/dist/source.mjs +18 -3
  40. package/dist/tree.cjs +19 -4
  41. package/dist/tree.d.ts.map +1 -1
  42. package/dist/tree.js +21 -2
  43. package/dist/tree.mjs +19 -4
  44. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  45. package/dist/wire.cjs +3 -1
  46. package/dist/wire.d.ts +2 -0
  47. package/dist/wire.d.ts.map +1 -1
  48. package/dist/wire.js +2 -0
  49. package/dist/wire.mjs +3 -1
  50. package/package.json +2 -2
@@ -38,9 +38,12 @@ var FILTERABLE_KINDS = [
38
38
  "router",
39
39
  "handler",
40
40
  "context",
41
- "directive"
41
+ "directive",
42
+ "custom-element",
43
+ "module",
44
+ "service"
42
45
  ];
43
- var LABEL = { handler: "route" };
46
+ var LABEL = { handler: "route", "custom-element": "custom element" };
44
47
  function createKindFilter(host, options) {
45
48
  const doc = host.ownerDocument;
46
49
  const kinds = options.kinds ?? FILTERABLE_KINDS;
@@ -1,10 +1,10 @@
1
1
  /** Every kind a graph can hold, in the order they are offered. */
2
2
  export const FILTERABLE_KINDS = [
3
3
  'state', 'memo', 'effect', 'store', 'resource', 'component', 'provider', 'control', 'router', 'handler',
4
- 'context', 'directive',
4
+ 'context', 'directive', 'custom-element', 'module', 'service',
5
5
  ];
6
- /** Only `handler` needs renaming; the other kinds read fine as they are. */
7
- const LABEL = { handler: 'route' };
6
+ /** Kinds whose name does not read as it is. */
7
+ const LABEL = { handler: 'route', 'custom-element': 'custom element' };
8
8
  /**
9
9
  * Builds the control inside `host`. Call `destroy` to remove it, it holds a document
10
10
  * listener for closing the panel on an outside click.
@@ -11,9 +11,12 @@ var FILTERABLE_KINDS = [
11
11
  "router",
12
12
  "handler",
13
13
  "context",
14
- "directive"
14
+ "directive",
15
+ "custom-element",
16
+ "module",
17
+ "service"
15
18
  ];
16
- var LABEL = { handler: "route" };
19
+ var LABEL = { handler: "route", "custom-element": "custom element" };
17
20
  function createKindFilter(host, options) {
18
21
  const doc = host.ownerDocument;
19
22
  const kinds = options.kinds ?? FILTERABLE_KINDS;
package/dist/observe.cjs CHANGED
@@ -28,6 +28,7 @@ var import_signal2 = require("@fluixi/reactive/signal");
28
28
  // src/source.ts
29
29
  var MARKER = "__fx_source";
30
30
  var COMPONENT_MARKER = "__fx_component";
31
+ var CLASS_MARKER = "__fx_class";
31
32
  var HOLE_MARKER = "__fx_hole";
32
33
  var PROPS_MARKER = "__fx_props";
33
34
  var MARKS = "__FLUIXI_DEVTOOLS_MARKS__";
@@ -38,12 +39,16 @@ var shared = host[MARKS] ?? {
38
39
  components: /* @__PURE__ */ new Map()
39
40
  };
40
41
  host[MARKS] = shared;
41
- function markReactiveSource(file, line, column, name) {
42
+ shared.classes ??= /* @__PURE__ */ new WeakMap();
43
+ function markReactiveSource(file, line, column, name, self) {
42
44
  const location2 = locate(file, line, column);
43
45
  if (shared.pending && location2.external && !shared.pending.location?.external) return;
44
- shared.pending = { name, location: location2 };
46
+ shared.pending = self && typeof self === "object" ? { name, location: location2, self } : { name, location: location2 };
45
47
  shared.expiring = false;
46
48
  }
49
+ function dropReactiveSource() {
50
+ shared.pending = null;
51
+ }
47
52
  function locate(file, line, column) {
48
53
  const external = !!file && (/^([a-zA-Z]:)?[\\/]/.test(file) || file.includes("node_modules/"));
49
54
  return external ? { file, line, column, external } : { file, line, column };
@@ -57,13 +62,19 @@ function markComponentSource(name, file, line, column) {
57
62
  }
58
63
  if (!seen || seen.file !== file || seen.line !== line) shared.components.set(name, null);
59
64
  }
65
+ function markClassSource(constructor, file, line, column) {
66
+ if (typeof constructor === "function") shared.classes.set(constructor, locate(file, line, column));
67
+ }
68
+ function classSource(constructor) {
69
+ return typeof constructor === "function" ? shared.classes.get(constructor) : void 0;
70
+ }
60
71
  function componentSource(name) {
61
72
  return shared.components.get(name) ?? void 0;
62
73
  }
63
74
  function takeReactiveSource() {
64
75
  const mark = shared.pending;
65
76
  if (mark) {
66
- if (mark.name !== void 0) shared.pending = { location: mark.location };
77
+ if (mark.name !== void 0 || mark.self) shared.pending = { location: mark.location };
67
78
  if (!shared.expiring) {
68
79
  shared.expiring = true;
69
80
  queueMicrotask(() => {
@@ -103,6 +114,7 @@ function holeLocation(accessor) {
103
114
  function installSourceMarker() {
104
115
  if (typeof host[MARKER] !== "function") host[MARKER] = markReactiveSource;
105
116
  if (typeof host[COMPONENT_MARKER] !== "function") host[COMPONENT_MARKER] = markComponentSource;
117
+ if (typeof host[CLASS_MARKER] !== "function") host[CLASS_MARKER] = markClassSource;
106
118
  if (typeof host[HOLE_MARKER] !== "function") host[HOLE_MARKER] = markHole;
107
119
  if (typeof host[PROPS_MARKER] !== "function") host[PROPS_MARKER] = markProps;
108
120
  }
@@ -114,6 +126,15 @@ var INTERNAL = [
114
126
  "/packages/dom/",
115
127
  "/packages/core/",
116
128
  "/packages/devtools/",
129
+ "/packages/web-components/",
130
+ "/packages/auth/",
131
+ "/packages/oauth2/",
132
+ "/packages/session/",
133
+ "/packages/router/",
134
+ "/packages/head/",
135
+ "/packages/start/",
136
+ "/packages/server/",
137
+ "/packages/template-runtime/",
117
138
  "/@fluixi/",
118
139
  "node_modules",
119
140
  "fluixi-devtools",
@@ -664,7 +685,10 @@ var KIND_COLOR = {
664
685
  binding: "#5eead4",
665
686
  setter: "#fb7185",
666
687
  group: "#8a90a2",
667
- element: "#8a90a2"
688
+ element: "#8a90a2",
689
+ "custom-element": "#2dd4bf",
690
+ module: "#facc15",
691
+ service: "#60a5fa"
668
692
  };
669
693
  var kindVar = (kind) => `--fx-kind-${kind}`;
670
694
  var kindColor = (kind) => `var(${kindVar(kind)}, ${KIND_COLOR[kind]})`;
@@ -681,7 +705,11 @@ var TREE_KINDS = /* @__PURE__ */ new Set([
681
705
  "handler",
682
706
  // A component a plugin recognised as providing a context. Still a tag the author wrote,
683
707
  // so leaving it out took it and everything it rendered out of the tree.
684
- "provider"
708
+ "provider",
709
+ // Scopes the runtime reported: what their signals and memos belong to.
710
+ "custom-element",
711
+ "module",
712
+ "service"
685
713
  ]);
686
714
  var TREE_CSS = `
687
715
  .fx-tree-row {
@@ -797,9 +825,14 @@ var PREFIX = {
797
825
  // A plugin's, and it names them itself.
798
826
  directive: "d",
799
827
  context: "ctx",
800
- provider: "p"
828
+ provider: "p",
829
+ // Scopes are always named by what opened them.
830
+ "custom-element": "el",
831
+ module: "mod",
832
+ service: "svc"
801
833
  };
802
834
  function readProps(props) {
835
+ if (readOnce.has(props)) return readOnce.get(props);
803
836
  const at = propsLocations(props);
804
837
  const out = [];
805
838
  for (const name of Object.keys(props)) {
@@ -810,13 +843,25 @@ function readProps(props) {
810
843
  ...where ? { at: { line: where[0], column: where[1] } } : {}
811
844
  };
812
845
  try {
813
- report.value = formatValue((0, import_signal2.untrack)(() => props[name]), 200);
846
+ report.value = formatValue(
847
+ (0, import_signal2.createRoot)((dispose) => {
848
+ try {
849
+ return (0, import_signal2.untrack)(() => props[name]);
850
+ } finally {
851
+ dispose();
852
+ }
853
+ }),
854
+ 200
855
+ );
814
856
  } catch {
815
857
  }
816
858
  out.push(report);
817
859
  }
818
- return out.length ? out : void 0;
860
+ const reports = out.length ? out : void 0;
861
+ readOnce.set(props, reports);
862
+ return reports;
819
863
  }
864
+ var readOnce = /* @__PURE__ */ new WeakMap();
820
865
  function observeGraph(options = {}) {
821
866
  installSourceMarker();
822
867
  const origin = options.origin ?? (typeof location === "object" ? location.origin : void 0);
@@ -837,6 +882,10 @@ function observeGraph(options = {}) {
837
882
  const boundTo = /* @__PURE__ */ new WeakMap();
838
883
  const drivenBy = /* @__PURE__ */ new WeakMap();
839
884
  let reaches = /* @__PURE__ */ new Map();
885
+ const SCOPE_KINDS = /* @__PURE__ */ new Set(["custom-element", "module", "service"]);
886
+ const scopeIds = /* @__PURE__ */ new WeakMap();
887
+ const scopeOfTarget = /* @__PURE__ */ new WeakMap();
888
+ const fieldsOf = /* @__PURE__ */ new WeakMap();
840
889
  let described = null;
841
890
  const rendered = /* @__PURE__ */ new Map();
842
891
  const elementsIn = (value) => {
@@ -933,7 +982,9 @@ function observeGraph(options = {}) {
933
982
  // Mapped, unlike `mark`: these two came off a stack, so they are positions in the
934
983
  // compiled file until a host translates them.
935
984
  ...site && !site.external ? { usedAt: mapped(site.location) } : {},
936
- ...site && site.external && !mark?.location ? { source: mapped(site.location) } : {}
985
+ // Not inside a scope: an element's prop is made by the package for the element, and
986
+ // the package's line says less than the scope it sits in already does.
987
+ ...site && site.external && !mark?.location && !report.scope ? { source: mapped(site.location) } : {}
937
988
  };
938
989
  const owner = ancestor(report.parents);
939
990
  if (owner !== void 0) node.createdIn = owner;
@@ -943,6 +994,28 @@ function observeGraph(options = {}) {
943
994
  node.createdIn = originId;
944
995
  if (from.source) node.source = from.source;
945
996
  }
997
+ const scopeId = report.scope ? scopeIds.get(report.scope) : void 0;
998
+ if (scopeId !== void 0) {
999
+ node.scope = scopeId;
1000
+ const creator = node.createdIn !== void 0 ? graph.get(node.createdIn) : void 0;
1001
+ const insideAlready = node.createdIn === scopeId || creator?.scope === scopeId;
1002
+ if (!insideAlready) node.createdIn = scopeId;
1003
+ } else if (node.createdIn !== void 0) {
1004
+ const creator = graph.get(node.createdIn);
1005
+ const inherited = creator && SCOPE_KINDS.has(creator.kind) ? creator.id : creator?.scope;
1006
+ if (inherited !== void 0) node.scope = inherited;
1007
+ }
1008
+ const self = mark?.self;
1009
+ if (self) {
1010
+ const list = fieldsOf.get(self);
1011
+ if (list) list.push(id);
1012
+ else fieldsOf.set(self, [id]);
1013
+ const open = scopeOfTarget.get(self);
1014
+ if (open !== void 0 && graph.has(open)) {
1015
+ node.scope = open;
1016
+ node.createdIn = open;
1017
+ }
1018
+ }
946
1019
  if (kind !== "effect" && typeof handle === "function") {
947
1020
  const read = handle;
948
1021
  node.read = () => (0, import_signal2.untrack)(read);
@@ -1078,20 +1151,90 @@ function observeGraph(options = {}) {
1078
1151
  internals.delete(target);
1079
1152
  const id = ids.get(target);
1080
1153
  if (id === void 0) return;
1081
- removed.push({ id, rev: ++revision });
1082
- revs.delete(id);
1083
- if (removed.length > REMOVED_LOG) removed.splice(0, removed.length - REMOVED_LOG);
1084
- history.disposed(id, graph.get(id)?.label);
1085
- graph.delete(id);
1086
- handed.delete(id);
1087
- raws.delete(id);
1088
- cores.delete(id);
1089
- rendered.delete(id);
1090
- setters.delete(id);
1091
- runsAtLastSample.delete(id);
1092
- unnamed.delete(id);
1154
+ forget(id);
1155
+ },
1156
+ scope(scope, info, parent) {
1157
+ dropReactiveSource();
1158
+ const id = seq++;
1159
+ const kind = info.kind === "element" ? "custom-element" : info.kind;
1160
+ const parentId = parent ? scopeIds.get(parent) : void 0;
1161
+ const found = currentCallSite(origin);
1162
+ const site = found && !/\0|__x00__|^virtual:/.test(found.location.file ?? "") ? found : void 0;
1163
+ const node = {
1164
+ id,
1165
+ label: info.name,
1166
+ kind,
1167
+ // The author named the element, the module and the service; none of them is plumbing.
1168
+ authored: true,
1169
+ deps: /* @__PURE__ */ new Set(),
1170
+ writes: /* @__PURE__ */ new Set(),
1171
+ runs: 0,
1172
+ lastPulse: 0,
1173
+ ...info.detail ? { value: info.detail, detail: info.detail } : {},
1174
+ ...parentId !== void 0 ? { scope: parentId, createdIn: parentId } : {}
1175
+ };
1176
+ const target = info.target;
1177
+ if (kind === "custom-element") {
1178
+ const defined = target ? classSource(target.constructor) : void 0;
1179
+ if (defined) node.declaredAt = defined;
1180
+ } else if (site && !site.external) {
1181
+ node.usedAt = mapped(site.location);
1182
+ }
1183
+ scopeIds.set(scope, id);
1184
+ if (target && typeof target === "object" && target.nodeType === 1) {
1185
+ rendered.set(id, [new WeakRef(target)]);
1186
+ }
1187
+ graph.set(id, node);
1188
+ history.created(id, node.createdIn, node.label);
1189
+ if (target && typeof target === "object") {
1190
+ scopeOfTarget.set(target, id);
1191
+ for (const field of fieldsOf.get(target) ?? []) {
1192
+ const made = graph.get(field);
1193
+ if (!made) continue;
1194
+ made.scope = id;
1195
+ made.createdIn = id;
1196
+ revs.set(field, ++revision);
1197
+ }
1198
+ }
1199
+ },
1200
+ scopeEnded(scope) {
1201
+ const id = scopeIds.get(scope);
1202
+ if (id === void 0) return;
1203
+ const ended = /* @__PURE__ */ new Set([id]);
1204
+ for (let grew = true; grew; ) {
1205
+ grew = false;
1206
+ for (const [other, node] of graph) {
1207
+ if (!ended.has(other) && node.scope !== void 0 && ended.has(node.scope)) {
1208
+ ended.add(other);
1209
+ grew = true;
1210
+ }
1211
+ }
1212
+ }
1213
+ for (const gone of ended) forget(gone);
1214
+ },
1215
+ scopeAdopted(scope, parent) {
1216
+ const node = graph.get(scopeIds.get(scope) ?? -1);
1217
+ const parentId = scopeIds.get(parent);
1218
+ if (!node || parentId === void 0 || parentId === node.id) return;
1219
+ node.scope = parentId;
1220
+ node.createdIn = parentId;
1221
+ revs.set(node.id, ++revision);
1093
1222
  }
1094
1223
  });
1224
+ function forget(id) {
1225
+ removed.push({ id, rev: ++revision });
1226
+ revs.delete(id);
1227
+ if (removed.length > REMOVED_LOG) removed.splice(0, removed.length - REMOVED_LOG);
1228
+ history.disposed(id, graph.get(id)?.label);
1229
+ graph.delete(id);
1230
+ handed.delete(id);
1231
+ raws.delete(id);
1232
+ cores.delete(id);
1233
+ rendered.delete(id);
1234
+ setters.delete(id);
1235
+ runsAtLastSample.delete(id);
1236
+ unnamed.delete(id);
1237
+ }
1095
1238
  const edges = (id, node) => {
1096
1239
  const sources = cores.get(id)?.sources;
1097
1240
  node.deps.clear();
@@ -1111,6 +1254,14 @@ function observeGraph(options = {}) {
1111
1254
  revs.set(id, ++revision);
1112
1255
  };
1113
1256
  const now = Date.now();
1257
+ for (const [id, node] of graph) {
1258
+ if (node.kind !== "custom-element" || node.usedAt) continue;
1259
+ const element = rendered.get(id)?.[0]?.deref();
1260
+ const written = element ? element.__fx_loc : void 0;
1261
+ if (!written?.file) continue;
1262
+ node.usedAt = { file: written.file, line: written.line, column: written.column };
1263
+ revs.set(id, ++revision);
1264
+ }
1114
1265
  const bindings = /* @__PURE__ */ new Map();
1115
1266
  const drives = /* @__PURE__ */ new Map();
1116
1267
  const seen = /* @__PURE__ */ new Map();
@@ -1 +1 @@
1
- {"version":3,"file":"observe.d.ts","sourceRoot":"","sources":["../src/observe.ts"],"names":[],"mappings":"AAUA,OAAO,EAIL,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAmE,KAAK,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAC9G,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAE1D,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAG9D,OAAO,EAA0B,KAAK,OAAO,EAAE,MAAM,UAAU,CAAC;AAmDhE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACnC,gFAAgF;IAChF,MAAM,IAAI,MAAM,EAAE,CAAC;IACnB;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACpC,6FAA6F;IAC7F,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACpC,4EAA4E;IAC5E,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC7B;;;;;;OAMG;IACH,WAAW,IAAI,MAAM,EAAE,CAAC;IACxB,8EAA8E;IAC9E,UAAU,IAAI,UAAU,EAAE,CAAC;IAC3B,qEAAqE;IACrE,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,EAAE,CAAC;IACnC,wEAAwE;IACxE,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,SAAS,CAAC;IACpD;;;OAGG;IACH,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,EAAE,CAAC;IAC9B;;;;OAIG;IACH,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,EAAE,CAAC;IACnC;;;OAGG;IACH,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IAC7C,MAAM,IAAI,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,gBAAgB,KAAK,MAAM,IAAI,CAAC;IACrD,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,sBAAsB,KAAK,sBAAsB,GAAG,SAAS,CAAC;CACtF;AAsED,wBAAgB,YAAY,CAAC,OAAO,GAAE,cAAmB,GAAG,aAAa,CA4tBxE"}
1
+ {"version":3,"file":"observe.d.ts","sourceRoot":"","sources":["../src/observe.ts"],"names":[],"mappings":"AAUA,OAAO,EAKL,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAmE,KAAK,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAC9G,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAE1D,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAG9D,OAAO,EAA0B,KAAK,OAAO,EAAE,MAAM,UAAU,CAAC;AAmDhE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACnC,gFAAgF;IAChF,MAAM,IAAI,MAAM,EAAE,CAAC;IACnB;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACpC,6FAA6F;IAC7F,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACpC,4EAA4E;IAC5E,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC7B;;;;;;OAMG;IACH,WAAW,IAAI,MAAM,EAAE,CAAC;IACxB,8EAA8E;IAC9E,UAAU,IAAI,UAAU,EAAE,CAAC;IAC3B,qEAAqE;IACrE,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,EAAE,CAAC;IACnC,wEAAwE;IACxE,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,SAAS,CAAC;IACpD;;;OAGG;IACH,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,EAAE,CAAC;IAC9B;;;;OAIG;IACH,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,EAAE,CAAC;IACnC;;;OAGG;IACH,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IAC7C,MAAM,IAAI,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,gBAAgB,KAAK,MAAM,IAAI,CAAC;IACrD,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,sBAAsB,KAAK,sBAAsB,GAAG,SAAS,CAAC;CACtF;AAgGD,wBAAgB,YAAY,CAAC,OAAO,GAAE,cAAmB,GAAG,aAAa,CAk2BxE"}
package/dist/observe.js CHANGED
@@ -8,8 +8,8 @@
8
8
  //
9
9
  // The graph it fills is the same Map<number, RNode> createGraphView already draws, so a
10
10
  // renderer does not care which of the two produced it.
11
- import { observeReactiveNodes, untrack, } from '@fluixi/reactive/signal';
12
- import { componentSource, holeLocation, installSourceMarker, propsLocations, takeReactiveSource } from './source.js';
11
+ import { createRoot, observeReactiveNodes, untrack, } from '@fluixi/reactive/signal';
12
+ import { classSource, componentSource, dropReactiveSource, holeLocation, installSourceMarker, propsLocations, takeReactiveSource } from './source.js';
13
13
  import { currentCallSite } from './callsite.js';
14
14
  import { KIND_OF_BUILTIN } from './instrument.js';
15
15
  import { formatValue, shorten } from './format.js';
@@ -81,20 +81,33 @@ const PREFIX = {
81
81
  directive: 'd',
82
82
  context: 'ctx',
83
83
  provider: 'p',
84
+ // Scopes are always named by what opened them.
85
+ 'custom-element': 'el',
86
+ module: 'mod',
87
+ service: 'svc',
84
88
  };
85
89
  /**
86
90
  * Start watching. Nodes made before this returns are not in the graph, so attach before the
87
91
  * app's modules load, which in practice means before the entry import.
88
92
  */
89
93
  /**
90
- * What a component was handed, read for display.
94
+ * What a component was handed, read for display, once per props object.
91
95
  *
92
96
  * Every prop is a getter over whatever the caller wrote, so reading one runs that
93
97
  * expression. Untracked, or the read would subscribe whatever is sampling to it and the
94
- * panel would drive the graph by looking at it. `children` is skipped: reading it builds
95
- * the subtree, which is a side effect nobody asked for by opening an inspector.
98
+ * panel would drive the graph by looking at it. And only once: JSX in a prop compiles to a
99
+ * getter that makes a new memo on each read, which the next sample pulls, building the
100
+ * component again, and any other prop can reach such a getter (`when={local.trailing != null}`).
101
+ * Read on every sample, each one added another copy of those components to the graph.
102
+ * `children` is skipped: reading it builds the subtree.
103
+ *
104
+ * Any other getter can build one too (`Show`'s `fallback`), and what it builds is live: its
105
+ * effects run outside the component, so a context it reads is missing, and they keep running
106
+ * and throwing on every change. Each read happens in a root disposed as soon as it returns.
96
107
  */
97
108
  function readProps(props) {
109
+ if (readOnce.has(props))
110
+ return readOnce.get(props);
98
111
  const at = propsLocations(props);
99
112
  const out = [];
100
113
  for (const name of Object.keys(props)) {
@@ -106,15 +119,26 @@ function readProps(props) {
106
119
  ...(where ? { at: { line: where[0], column: where[1] } } : {}),
107
120
  };
108
121
  try {
109
- report.value = formatValue(untrack(() => props[name]), 200);
122
+ report.value = formatValue(createRoot((dispose) => {
123
+ try {
124
+ return untrack(() => props[name]);
125
+ }
126
+ finally {
127
+ dispose();
128
+ }
129
+ }), 200);
110
130
  }
111
131
  catch {
112
132
  // A getter that throws is still a prop worth naming.
113
133
  }
114
134
  out.push(report);
115
135
  }
116
- return out.length ? out : undefined;
136
+ const reports = out.length ? out : undefined;
137
+ readOnce.set(props, reports);
138
+ return reports;
117
139
  }
140
+ /** Each props object's reports, from its first read. */
141
+ const readOnce = new WeakMap();
118
142
  export function observeGraph(options = {}) {
119
143
  installSourceMarker();
120
144
  const origin = options.origin ?? (typeof location === 'object' ? location.origin : undefined);
@@ -175,6 +199,14 @@ export function observeGraph(options = {}) {
175
199
  const drivenBy = new WeakMap();
176
200
  /** Per node, the elements it drives. Rebuilt each sample, like the descriptions are. */
177
201
  let reaches = new Map();
202
+ /** The kinds a scope node can be. */
203
+ const SCOPE_KINDS = new Set(['custom-element', 'module', 'service']);
204
+ /** Each open scope's node, by the identity the runtime handed over. */
205
+ const scopeIds = new WeakMap();
206
+ /** The open scope standing for an object, an element, by that object. */
207
+ const scopeOfTarget = new WeakMap();
208
+ /** Nodes a class field made, by the instance: they join its scope when it opens. */
209
+ const fieldsOf = new WeakMap();
178
210
  /** Cleared on each sample: the page has moved on. */
179
211
  let described = null;
180
212
  /**
@@ -343,7 +375,9 @@ export function observeGraph(options = {}) {
343
375
  // Mapped, unlike `mark`: these two came off a stack, so they are positions in the
344
376
  // compiled file until a host translates them.
345
377
  ...(site && !site.external ? { usedAt: mapped(site.location) } : {}),
346
- ...(site && site.external && !mark?.location ? { source: mapped(site.location) } : {}),
378
+ // Not inside a scope: an element's prop is made by the package for the element, and
379
+ // the package's line says less than the scope it sits in already does.
380
+ ...(site && site.external && !mark?.location && !report.scope ? { source: mapped(site.location) } : {}),
347
381
  };
348
382
  const owner = ancestor(report.parents);
349
383
  if (owner !== undefined)
@@ -357,6 +391,39 @@ export function observeGraph(options = {}) {
357
391
  if (from.source)
358
392
  node.source = from.source;
359
393
  }
394
+ // Made inside an element, module or service: it belongs to that scope, and hangs off it
395
+ // unless what created it already sits inside the same scope, which keeps the nesting.
396
+ const scopeId = report.scope ? scopeIds.get(report.scope) : undefined;
397
+ if (scopeId !== undefined) {
398
+ node.scope = scopeId;
399
+ const creator = node.createdIn !== undefined ? graph.get(node.createdIn) : undefined;
400
+ const insideAlready = node.createdIn === scopeId || creator?.scope === scopeId;
401
+ if (!insideAlready)
402
+ node.createdIn = scopeId;
403
+ }
404
+ else if (node.createdIn !== undefined) {
405
+ // Made after the scope closed, by something inside it: a memo read lazily, an effect
406
+ // re-running, a row a list adds. It belongs where its creator belongs.
407
+ const creator = graph.get(node.createdIn);
408
+ const inherited = creator && SCOPE_KINDS.has(creator.kind) ? creator.id : creator?.scope;
409
+ if (inherited !== undefined)
410
+ node.scope = inherited;
411
+ }
412
+ // A class field: made in the constructor, before the element opened its scope. It belongs
413
+ // to the instance, so it goes in that scope now if it is open, or when it opens.
414
+ const self = mark?.self;
415
+ if (self) {
416
+ const list = fieldsOf.get(self);
417
+ if (list)
418
+ list.push(id);
419
+ else
420
+ fieldsOf.set(self, [id]);
421
+ const open = scopeOfTarget.get(self);
422
+ if (open !== undefined && graph.has(open)) {
423
+ node.scope = open;
424
+ node.createdIn = open;
425
+ }
426
+ }
360
427
  // An effect returns nothing worth showing, so it has no reader and pulses on its runs.
361
428
  if (kind !== 'effect' && typeof handle === 'function') {
362
429
  // Not read here: a memo is lazy, and forcing it from inside its own creation would
@@ -537,22 +604,110 @@ export function observeGraph(options = {}) {
537
604
  const id = ids.get(target);
538
605
  if (id === undefined)
539
606
  return;
540
- removed.push({ id, rev: ++revision });
541
- revs.delete(id);
542
- if (removed.length > REMOVED_LOG)
543
- removed.splice(0, removed.length - REMOVED_LOG);
544
- // The label goes with it: nothing is left to look the name up in.
545
- history.disposed(id, graph.get(id)?.label);
546
- graph.delete(id);
547
- handed.delete(id);
548
- raws.delete(id);
549
- cores.delete(id);
550
- rendered.delete(id);
551
- setters.delete(id);
552
- runsAtLastSample.delete(id);
553
- unnamed.delete(id);
607
+ forget(id);
608
+ },
609
+ scope(scope, info, parent) {
610
+ // Whatever mark is pending was left by the code around this scope, not by what it makes.
611
+ dropReactiveSource();
612
+ const id = seq++;
613
+ const kind = info.kind === 'element' ? 'custom-element' : info.kind;
614
+ const parentId = parent ? scopeIds.get(parent) : undefined;
615
+ // Where it was opened: the line that defined the module, built the service, or, for
616
+ // an element, the one that put it on the page. A bundler's virtual module (the
617
+ // generated app config, `\0virtual:…`) is nowhere a reader can go, so it is left out.
618
+ const found = currentCallSite(origin);
619
+ const site = found && !/\0|__x00__|^virtual:/.test(found.location.file ?? '') ? found : undefined;
620
+ const node = {
621
+ id,
622
+ label: info.name,
623
+ kind,
624
+ // The author named the element, the module and the service; none of them is plumbing.
625
+ authored: true,
626
+ deps: new Set(),
627
+ writes: new Set(),
628
+ runs: 0,
629
+ lastPulse: 0,
630
+ ...(info.detail ? { value: info.detail, detail: info.detail } : {}),
631
+ ...(parentId !== undefined ? { scope: parentId, createdIn: parentId } : {}),
632
+ };
633
+ const target = info.target;
634
+ if (kind === 'custom-element') {
635
+ // The stack here is whatever connected it, often the `define` that upgraded it, so it
636
+ // says nothing about where the tag is written: that comes off the element at sample.
637
+ // Where it is defined is the class declaration, which the compiler marked.
638
+ const defined = target ? classSource(target.constructor) : undefined;
639
+ if (defined)
640
+ node.declaredAt = defined;
641
+ }
642
+ else if (site && !site.external) {
643
+ // Only the app's own line: a package's is where the package opened the scope for it,
644
+ // which says nothing about this module or service.
645
+ node.usedAt = mapped(site.location);
646
+ }
647
+ scopeIds.set(scope, id);
648
+ if (target && typeof target === 'object' && target.nodeType === 1) {
649
+ rendered.set(id, [new WeakRef(target)]);
650
+ }
651
+ graph.set(id, node);
652
+ history.created(id, node.createdIn, node.label);
653
+ if (target && typeof target === 'object') {
654
+ scopeOfTarget.set(target, id);
655
+ for (const field of fieldsOf.get(target) ?? []) {
656
+ const made = graph.get(field);
657
+ if (!made)
658
+ continue;
659
+ made.scope = id;
660
+ made.createdIn = id;
661
+ revs.set(field, ++revision);
662
+ }
663
+ }
664
+ },
665
+ scopeEnded(scope) {
666
+ const id = scopeIds.get(scope);
667
+ if (id === undefined)
668
+ return;
669
+ // What it made goes with it. An element's prop signals have no owner to dispose them,
670
+ // so they outlived the element as cards pointing at a scope that no longer existed.
671
+ const ended = new Set([id]);
672
+ for (let grew = true; grew;) {
673
+ grew = false;
674
+ for (const [other, node] of graph) {
675
+ if (!ended.has(other) && node.scope !== undefined && ended.has(node.scope)) {
676
+ ended.add(other);
677
+ grew = true;
678
+ }
679
+ }
680
+ }
681
+ for (const gone of ended)
682
+ forget(gone);
683
+ },
684
+ scopeAdopted(scope, parent) {
685
+ const node = graph.get(scopeIds.get(scope) ?? -1);
686
+ const parentId = scopeIds.get(parent);
687
+ if (!node || parentId === undefined || parentId === node.id)
688
+ return;
689
+ node.scope = parentId;
690
+ node.createdIn = parentId;
691
+ revs.set(node.id, ++revision);
554
692
  },
555
693
  });
694
+ /** Drop a node everywhere it is kept, and log it for a consumer catching up. */
695
+ function forget(id) {
696
+ removed.push({ id, rev: ++revision });
697
+ revs.delete(id);
698
+ if (removed.length > REMOVED_LOG)
699
+ removed.splice(0, removed.length - REMOVED_LOG);
700
+ // The label goes with it: nothing is left to look the name up in.
701
+ history.disposed(id, graph.get(id)?.label);
702
+ graph.delete(id);
703
+ handed.delete(id);
704
+ raws.delete(id);
705
+ cores.delete(id);
706
+ rendered.delete(id);
707
+ setters.delete(id);
708
+ runsAtLastSample.delete(id);
709
+ unnamed.delete(id);
710
+ }
556
711
  /** What this node read on its last run, as ids: the edges the core already holds. */
557
712
  const edges = (id, node) => {
558
713
  const sources = cores.get(id)?.sources;
@@ -576,6 +731,20 @@ export function observeGraph(options = {}) {
576
731
  revs.set(id, ++revision);
577
732
  };
578
733
  const now = Date.now();
734
+ // Where each custom element's tag is written. Stamped by the runtime when the template
735
+ // makes or hydrates it, which can be after its scope opened: an element the server
736
+ // rendered upgrades on import, before hydration gets to it.
737
+ for (const [id, node] of graph) {
738
+ if (node.kind !== 'custom-element' || node.usedAt)
739
+ continue;
740
+ const element = rendered.get(id)?.[0]?.deref();
741
+ const written = element ? element.__fx_loc : undefined;
742
+ if (!written?.file)
743
+ continue;
744
+ node.usedAt = { file: written.file, line: written.line, column: written.column };
745
+ // A new revision without counting as a change: nothing ran, so nothing should pulse.
746
+ revs.set(id, ++revision);
747
+ }
579
748
  // What the dom reads, per node. Recounted rather than kept: a binding's sources change
580
749
  // with every run, and a stale count is worse than none.
581
750
  const bindings = new Map();