tungsten 0.3.3 → 0.3.4

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 (150) hide show
  1. package/dist/.build-complete +1 -1
  2. package/dist/Components.d.ts +6 -1
  3. package/dist/Components.d.ts.map +1 -1
  4. package/dist/Components.js +103 -40
  5. package/dist/DomPointer.d.ts +11 -0
  6. package/dist/DomPointer.d.ts.map +1 -1
  7. package/dist/DomPointer.js +15 -0
  8. package/dist/DomRegion.d.ts +2 -0
  9. package/dist/DomRegion.d.ts.map +1 -1
  10. package/dist/DomRegion.js +23 -3
  11. package/dist/Framework.d.ts.map +1 -1
  12. package/dist/Framework.js +27 -5
  13. package/dist/FrameworkConfig.d.ts +2 -0
  14. package/dist/FrameworkConfig.d.ts.map +1 -1
  15. package/dist/Instance/Instance.d.ts +1 -1
  16. package/dist/Instance/Instance.d.ts.map +1 -1
  17. package/dist/Servers/DevServer.d.ts.map +1 -1
  18. package/dist/Servers/DevServer.js +3 -3
  19. package/dist/Servers/ProdServer.d.ts +1 -1
  20. package/dist/Servers/ProdServer.d.ts.map +1 -1
  21. package/dist/Servers/ProdServer.js +13 -3
  22. package/dist/Servers/loadEnvsDev.d.ts.map +1 -1
  23. package/dist/Servers/loadEnvsDev.js +16 -5
  24. package/dist/babelPlugins/BabelExtractComponentStylesheet.d.ts +13 -0
  25. package/dist/babelPlugins/BabelExtractComponentStylesheet.d.ts.map +1 -0
  26. package/dist/babelPlugins/BabelExtractComponentStylesheet.js +24 -0
  27. package/dist/babelPlugins/BabelTransformComponentStyles.d.ts +2 -0
  28. package/dist/babelPlugins/BabelTransformComponentStyles.d.ts.map +1 -1
  29. package/dist/babelPlugins/BabelTransformComponentStyles.js +21 -5
  30. package/dist/babelPlugins/BabelTransformPackageImports.d.ts +1 -0
  31. package/dist/babelPlugins/BabelTransformPackageImports.d.ts.map +1 -1
  32. package/dist/babelPlugins/BabelTransformPackageImports.js +21 -9
  33. package/dist/bin/commands/build.d.ts.map +1 -1
  34. package/dist/bin/commands/build.js +9 -2
  35. package/dist/bin/commands/compile.d.ts.map +1 -1
  36. package/dist/bin/commands/compile.js +7 -2
  37. package/dist/build/CompilationInfo.d.ts +1 -0
  38. package/dist/build/CompilationInfo.d.ts.map +1 -1
  39. package/dist/build/combineComponentCss.d.ts +1 -1
  40. package/dist/build/combineComponentCss.d.ts.map +1 -1
  41. package/dist/build/combineComponentCss.js +3 -3
  42. package/dist/build/compileJavascript.d.ts.map +1 -1
  43. package/dist/build/compileJavascript.js +2 -2
  44. package/dist/build/compileUserApp.d.ts.map +1 -1
  45. package/dist/build/compileUserApp.js +14 -5
  46. package/dist/build/frameworkStylesheets.d.ts +5 -0
  47. package/dist/build/frameworkStylesheets.d.ts.map +1 -0
  48. package/dist/build/frameworkStylesheets.js +49 -0
  49. package/dist/build/getPackageDirForFile.d.ts +2 -0
  50. package/dist/build/getPackageDirForFile.d.ts.map +1 -0
  51. package/dist/build/getPackageDirForFile.js +11 -0
  52. package/dist/build/getSxyClientPackageAliases.d.ts.map +1 -1
  53. package/dist/build/getSxyClientPackageAliases.js +3 -1
  54. package/dist/build/makeBabelUserDevConfig.d.ts +1 -1
  55. package/dist/build/makeBabelUserDevConfig.d.ts.map +1 -1
  56. package/dist/build/makeBabelUserDevConfig.js +3 -2
  57. package/dist/build/makeBabelUserProdConfig.d.ts +1 -1
  58. package/dist/build/makeBabelUserProdConfig.d.ts.map +1 -1
  59. package/dist/build/makeBabelUserProdConfig.js +4 -2
  60. package/dist/build/rollup.user.prod.config.d.ts.map +1 -1
  61. package/dist/build/rollup.user.prod.config.js +4 -1
  62. package/dist/build/rollupExtractComponentStylesheets.d.ts +12 -0
  63. package/dist/build/rollupExtractComponentStylesheets.d.ts.map +1 -0
  64. package/dist/build/rollupExtractComponentStylesheets.js +35 -0
  65. package/dist/compat/context.d.ts +1 -1
  66. package/dist/compat/context.d.ts.map +1 -1
  67. package/dist/componentCss/i/T-components-library-RichTextEditor-RichTextEditor.css +115 -0
  68. package/dist/componentCss/i/T-components-library-__StatefulMessage-StatefulMessage.css +25 -0
  69. package/dist/componentCss/tungsten.css +2 -0
  70. package/dist/componentProxyHandlers.d.ts +2 -1
  71. package/dist/componentProxyHandlers.d.ts.map +1 -1
  72. package/dist/componentProxyHandlers.js +45 -9
  73. package/dist/components/GoToComponent.d.ts +1 -1
  74. package/dist/components/GoToComponent.d.ts.map +1 -1
  75. package/dist/components/_GoToComponent.d.ts +10 -0
  76. package/dist/components/_GoToComponent.d.ts.map +1 -0
  77. package/dist/components/_GoToComponent.js +13 -0
  78. package/dist/components/library/RichTextEditor/RichTextEditor.d.ts +50 -0
  79. package/dist/components/library/RichTextEditor/RichTextEditor.d.ts.map +1 -0
  80. package/dist/components/library/RichTextEditor/RichTextEditor.js +458 -0
  81. package/dist/components/library/RichTextEditor/StoredContentNode.d.ts +5 -0
  82. package/dist/components/library/RichTextEditor/StoredContentNode.d.ts.map +1 -0
  83. package/dist/components/library/RichTextEditor/StoredContentNode.js +1 -0
  84. package/dist/components/library/RichTextEditor/activeFormats.d.ts +20 -0
  85. package/dist/components/library/RichTextEditor/activeFormats.d.ts.map +1 -0
  86. package/dist/components/library/RichTextEditor/activeFormats.js +48 -0
  87. package/dist/components/library/RichTextEditor/contentTree.d.ts +30 -0
  88. package/dist/components/library/RichTextEditor/contentTree.d.ts.map +1 -0
  89. package/dist/components/library/RichTextEditor/contentTree.js +81 -0
  90. package/dist/components/library/RichTextEditor/sanitizeAuthoredHtml.d.ts +30 -0
  91. package/dist/components/library/RichTextEditor/sanitizeAuthoredHtml.d.ts.map +1 -0
  92. package/dist/components/library/RichTextEditor/sanitizeAuthoredHtml.js +97 -0
  93. package/dist/components/library/__StatefulMessage/MessageData.d.ts +7 -0
  94. package/dist/components/library/__StatefulMessage/MessageData.d.ts.map +1 -0
  95. package/dist/components/library/__StatefulMessage/MessageData.js +1 -0
  96. package/dist/components/library/__StatefulMessage/StatefulMessage.d.ts +22 -0
  97. package/dist/components/library/__StatefulMessage/StatefulMessage.d.ts.map +1 -0
  98. package/dist/components/library/__StatefulMessage/StatefulMessage.js +88 -0
  99. package/dist/components/library/index.d.ts +5 -0
  100. package/dist/components/library/index.d.ts.map +1 -0
  101. package/dist/components/library/index.js +7 -0
  102. package/dist/defaultConfig.d.ts +1 -0
  103. package/dist/defaultConfig.d.ts.map +1 -1
  104. package/dist/defaultConfig.js +1 -0
  105. package/dist/env/injectEnv.d.ts +1 -1
  106. package/dist/env/injectEnv.d.ts.map +1 -1
  107. package/dist/env/injectEnv.js +16 -6
  108. package/dist/findUserConfig.d.ts +1 -1
  109. package/dist/indexInner.d.ts +5 -5
  110. package/dist/indexInner.d.ts.map +1 -1
  111. package/dist/indexInner.js +2 -1
  112. package/dist/postcssPlugins/TransformCssNames.d.ts.map +1 -1
  113. package/dist/postcssPlugins/TransformCssNames.js +10 -2
  114. package/dist/propsAreEqual.d.ts +8 -0
  115. package/dist/propsAreEqual.d.ts.map +1 -0
  116. package/dist/propsAreEqual.js +97 -0
  117. package/dist/render/HeadData.d.ts +1 -0
  118. package/dist/render/HeadData.d.ts.map +1 -1
  119. package/dist/render/contentToHtml.d.ts.map +1 -1
  120. package/dist/render/contentToHtml.js +1 -0
  121. package/dist/render/instantiateClassComponent.d.ts.map +1 -1
  122. package/dist/render/instantiateClassComponent.js +4 -0
  123. package/dist/render/processHead.d.ts.map +1 -1
  124. package/dist/render/processHead.js +14 -1
  125. package/dist/settings.d.ts +2 -0
  126. package/dist/settings.d.ts.map +1 -1
  127. package/dist/settings.js +3 -0
  128. package/dist/stateTracking.d.ts +47 -0
  129. package/dist/stateTracking.d.ts.map +1 -0
  130. package/dist/stateTracking.js +83 -0
  131. package/dist/staticGen/staticGen.d.ts.map +1 -1
  132. package/dist/staticGen/staticGen.js +117 -107
  133. package/dist/testing/installTestStyleProcessors.d.ts +2 -0
  134. package/dist/testing/installTestStyleProcessors.d.ts.map +1 -0
  135. package/dist/testing/installTestStyleProcessors.js +21 -0
  136. package/dist/testing/temp/componentPackageMock.d.ts +18 -0
  137. package/dist/testing/temp/componentPackageMock.d.ts.map +1 -0
  138. package/dist/testing/temp/componentPackageMock.js +37 -0
  139. package/dist/testing/temp/contentHtmlElements.d.ts +3 -0
  140. package/dist/testing/temp/contentHtmlElements.d.ts.map +1 -0
  141. package/dist/testing/temp/contentHtmlElements.js +12 -0
  142. package/dist/update/integrateUpdateVdom.d.ts.map +1 -1
  143. package/dist/update/integrateUpdateVdom.js +160 -51
  144. package/dist/update/pendingMounts.d.ts +25 -0
  145. package/dist/update/pendingMounts.d.ts.map +1 -0
  146. package/dist/update/pendingMounts.js +55 -0
  147. package/package.json +4 -2
  148. package/dist/env/parseEnv.d.ts +0 -2
  149. package/dist/env/parseEnv.d.ts.map +0 -1
  150. package/dist/env/parseEnv.js +0 -14
@@ -24,6 +24,57 @@ import { clearCurrentComponent } from 'tungsten/hooks/clearCurrentComponent.js';
24
24
  import { isDev } from "../isDev.js";
25
25
  import { $original } from "../componentProxyHandlers.js";
26
26
  import { booleanAttributeValue } from "../booleanAttributeValue.js";
27
+ import { propsAreEqual } from "../propsAreEqual.js";
28
+ import { markRendered, refreshTracing, renderedAt, trace, tracing } from "../stateTracking.js";
29
+ import { beginMountScope, discardPendingMounts, endMountScope, queueMount } from "./pendingMounts.js";
30
+ /**
31
+ * How many dom nodes each element's rendered content currently occupies, recorded on
32
+ * every real update. When an update is skipped for unchanged props the child's dom is
33
+ * untouched, so the count still holds -- and the dom pointer can be advanced without
34
+ * measuring the region, which would mean scanning parent.childNodes.
35
+ */
36
+ const skippedNodesLength = new WeakMap();
37
+ const reconcileStats = new Map();
38
+ let reconcileDepth = 0;
39
+ function bucket(name) {
40
+ const existing = reconcileStats.get(name);
41
+ if (existing !== undefined) return existing;
42
+ const created = {
43
+ n: 0,
44
+ ms: 0
45
+ };
46
+ reconcileStats.set(name, created);
47
+ return created;
48
+ }
49
+ /** times `work` into `name` when tracing, and is a plain call-through when not */
50
+ async function timed(name, work) {
51
+ if (!tracing()) return work();
52
+ const b = bucket(name);
53
+ const start = performance.now();
54
+ try {
55
+ return await work();
56
+ } finally {
57
+ b.n++;
58
+ b.ms += performance.now() - start;
59
+ }
60
+ }
61
+ function timedSync(name, work) {
62
+ if (!tracing()) return work();
63
+ const b = bucket(name);
64
+ const start = performance.now();
65
+ try {
66
+ return work();
67
+ } finally {
68
+ b.n++;
69
+ b.ms += performance.now() - start;
70
+ }
71
+ }
72
+ function reportReconcileStats(elementName) {
73
+ if (reconcileStats.size === 0) return;
74
+ const parts = [...reconcileStats.entries()].sort(([, a], [, b]) => b.ms - a.ms).map(([name, b]) => `${name} ${b.ms.toFixed(1)}ms/${b.n}`);
75
+ trace('RECONCILE', elementName, parts.join(' '));
76
+ reconcileStats.clear();
77
+ }
27
78
  function debug(...texts) {
28
79
  //console.log(...texts)
29
80
  //console.log(getStackTrace().split('\n')[2])
@@ -33,6 +84,15 @@ const timing = false; // eslint-disable-line no-type-assertion/no-type-assertion
33
84
  export async function integrateElementUpdateVdom(element, newContent) {
34
85
  let startTime = 0;
35
86
  if (timing) startTime = new Date().getMilliseconds();
87
+ // Only the outermost call reports, so nested components roll up into one line per
88
+ // update rather than one per component. Depth is restored from the value captured on
89
+ // entry rather than decremented, so a throw in a nested call cannot leave the counter
90
+ // stuck above zero and silently suppress every later report.
91
+ const reconcileEntryDepth = ++reconcileDepth;
92
+ if (reconcileEntryDepth === 1) refreshTracing();
93
+ // anything created during this update mounts detached and is attached before the
94
+ // outermost integrate returns, so its mount hooks wait until then
95
+ beginMountScope();
36
96
  debug(`integrateElementUpdateVdom`, element);
37
97
  debug(`integrateElementUpdateVdom`, newContent);
38
98
  //console.log('***** integrateElementUpdateVdom', element.constructor.name, '*****')
@@ -97,11 +157,17 @@ export async function integrateElementUpdateVdom(element, newContent) {
97
157
  //console.log('firstElement', element.__domRegion?.firstElement)
98
158
  //console.log('lastElement', element.__domRegion?.lastElement)
99
159
  //console.log('Current Content after update for element', element.constructor.name, 'is', element.__currentContent)
100
- const newPositionOfFirstElement = domRegion.positionOfFirst();
101
- const newPositionOfLastElement = domRegion.positionOfLast();
160
+ // These were just written by updateByIndexes() immediately above, so the region's
161
+ // bounds are already known -- asking positionOfFirst()/positionOfLast() would
162
+ // indexOf them back out of parent.childNodes, an O(n) scan per component update.
163
+ const newPositionOfFirstElement = previousPositionOfFirstElement;
164
+ const newPositionOfLastElement = domPointer.index - 1;
102
165
  const newNodesLength = newPositionOfFirstElement >= 0 ? 1 + newPositionOfLastElement - newPositionOfFirstElement : 0;
103
166
  //console.log('finished updating element. ', element.constructor.name, ' El positions:', newPositionOfFirstElement, newPositionOfLastElement)
104
167
  if (timing) out(`Updated ${element.constructor.name} in ${new Date().getMilliseconds() - startTime}ms`);
168
+ reconcileDepth = reconcileEntryDepth - 1;
169
+ if (reconcileDepth === 0 && tracing()) reportReconcileStats(element.constructor.name);
170
+ endMountScope();
105
171
  // newNodesLength previousNodesLength
106
172
  return {
107
173
  renderedContent: renderedTree,
@@ -149,7 +215,7 @@ export async function applyUpdate({
149
215
  //oldNode
150
216
  //console.log('array update', newNode)
151
217
  //console.log('old array', oldNode)
152
- const keyedOldChildren = getKeyedOldChildren(oldNode);
218
+ const keyedOldChildren = timedSync('indexOldKeys', () => getKeyedOldChildren(oldNode));
153
219
  const tempOldChildren = [...oldNode];
154
220
  if (Object.keys(keyedOldChildren).length > 0) {
155
221
  //console.log('handling a keyed array')
@@ -198,7 +264,7 @@ export async function applyUpdate({
198
264
  // create a throwaway DomPointer because we are going to manage our DomPointer differently
199
265
  const tempDomPointer = new DomPointer(domPointer.parent, domPointer.index);
200
266
  // update
201
- renderedContent = await applyUpdate({
267
+ const updated = await timed('keyedUpdate', () => applyUpdate({
202
268
  tungstenElement,
203
269
  instance,
204
270
  newNode: childNode,
@@ -206,10 +272,11 @@ export async function applyUpdate({
206
272
  domRegion,
207
273
  domPointer: tempDomPointer,
208
274
  parentNode
209
- });
275
+ }));
276
+ renderedContent = updated;
210
277
  // move
211
278
  //console.log('moving the keyed item', childNode[$key])
212
- moveRenderedContent(renderedContent, domPointer);
279
+ timedSync('keyedMove', () => moveRenderedContent(updated, domPointer));
213
280
  // delete oldChild from temp list
214
281
  delete tempOldChildren[oldChildIndex];
215
282
  } else {
@@ -221,7 +288,7 @@ export async function applyUpdate({
221
288
  //console.log('update/replace a non keyed item', _nki)
222
289
  //throwError('b', 'b')
223
290
  // update/replace as required
224
- renderedContent = await applyUpdate({
291
+ const updated = await timed('unkeyedUpdate', () => applyUpdate({
225
292
  tungstenElement,
226
293
  instance,
227
294
  newNode: childNode,
@@ -229,11 +296,12 @@ export async function applyUpdate({
229
296
  domRegion,
230
297
  domPointer: tempDomPointer,
231
298
  parentNode
232
- });
299
+ }));
300
+ renderedContent = updated;
233
301
  // move -- we need to move it, because we may have removed or inserted elements
234
302
  // pushing the original out of place
235
303
  //console.log('moving the non keyed item', _nki)
236
- moveRenderedContent(renderedContent, domPointer);
304
+ timedSync('unkeyedMove', () => moveRenderedContent(updated, domPointer));
237
305
  // remove replaced node from list
238
306
  delete tempOldChildren[i];
239
307
  } else {
@@ -241,13 +309,13 @@ export async function applyUpdate({
241
309
  //console.log('dp', domPointer)
242
310
  //throwError('c', 'c')
243
311
  // create
244
- renderedContent = await createNode({
312
+ renderedContent = await timed('create', () => createNode({
245
313
  tungstenElement,
246
314
  instance,
247
315
  newNode: childNode,
248
316
  domPointer,
249
317
  parentNode
250
- });
318
+ }));
251
319
  }
252
320
  }
253
321
  renderedNodes.push(renderedContent);
@@ -258,7 +326,7 @@ export async function applyUpdate({
258
326
  for (const oldChild of tempOldChildren) {
259
327
  if (oldChild) {
260
328
  //console.log('deleting old child', oldChild)
261
- deleteNodeIfRendered(oldChild);
329
+ timedSync('trim', () => deleteNodeIfRendered(oldChild));
262
330
  }
263
331
  }
264
332
  return renderedNodes;
@@ -490,17 +558,26 @@ function moveRenderedContent(renderedContent, domPointer) {
490
558
  }
491
559
  function moveDomNode(domNode, domPointer) {
492
560
  //console.log('moving domNode', domNode)
493
- if (domPointer.positionOf(domNode) !== domPointer.index) {
561
+ // O(1) position check. positionOf() is Array.indexOf over parent.childNodes, which
562
+ // is O(n) in the sibling count -- and this runs once per node per update, making a
563
+ // keyed array update O(n^2): 20k rows cost ~400M comparisons even when nothing moved.
564
+ // Reading the node already at `index` answers the same question, and differs from
565
+ // domNode in exactly the cases indexOf(domNode) !== index did: the node is elsewhere,
566
+ // is not a child at all (indexOf -1), or index is past the end (undefined).
567
+ // one live-NodeList read, reused as the insertBefore reference below -- see
568
+ // DomPointer.insert() for why these reads are worth counting
569
+ const reference = domPointer.parent.childNodes[domPointer.index]; // eslint-disable-line no-type-assertion/no-type-assertion -- indexing past the end gives undefined, which the DOM types do not model
570
+ if (reference !== domNode) {
494
571
  //console.log('document.activeElement', document.activeElement, domNode == document.activeElement, domNode === document.activeElement)
495
572
  const ownerDocument = domNode.ownerDocument;
496
573
  const hasFocus = domNode === ownerDocument.activeElement;
497
574
  const activeElement = ownerDocument.activeElement;
498
- if (domPointer.index >= domPointer.parent.childNodes.length) {
575
+ if (reference === undefined) {
499
576
  //console.log('appending', domNode)
500
577
  domPointer.parent.appendChild(domNode);
501
578
  } else {
502
579
  //console.log('inserting at', domPointer.index, domNode)
503
- domPointer.parent.insertBefore(domNode, domPointer.parent.childNodes[domPointer.index]);
580
+ domPointer.parent.insertBefore(domNode, reference);
504
581
  }
505
582
  //console.log('had focus', hasFocus)
506
583
  if (hasFocus) domNode.focus();
@@ -598,12 +675,7 @@ function createComment({
598
675
  debug('-createComment');
599
676
  //console.log('createComment', text)
600
677
  const comment = documentFromParent(domPointer.parent).createComment(text);
601
- if (domPointer.index >= domPointer.parent.childNodes.length) {
602
- domPointer.parent.appendChild(comment);
603
- } else {
604
- domPointer.parent.insertBefore(comment, domPointer.parent.childNodes[domPointer.index]);
605
- }
606
- domPointer.increment();
678
+ domPointer.insert(comment);
607
679
  return comment;
608
680
  }
609
681
  export function processHtmlNode(node, parentNode) {
@@ -744,15 +816,10 @@ function createLiteral(value, domPointer) {
744
816
  debug('-createLiteral:', value);
745
817
  //const textNode = createTextElement(String(value), domPointer)
746
818
  const text = String(value);
747
- debug('dom parent', domPointer.parent);
748
- debug('dom index', domPointer.index);
819
+ if (isDev) debug('dom parent', domPointer.parent);
820
+ if (isDev) debug('dom index', domPointer.index);
749
821
  const textNode = documentFromParent(domPointer.parent).createTextNode(text);
750
- if (domPointer.parent.childNodes.length === domPointer.index) {
751
- domPointer.parent.appendChild(textNode);
752
- } else {
753
- domPointer.parent.insertBefore(textNode, domPointer.parent.childNodes[domPointer.index]);
754
- }
755
- domPointer.increment();
822
+ domPointer.insert(textNode);
756
823
  //domPointer.increment()
757
824
  return typeof value === 'string' ? {
758
825
  [$type]: $string,
@@ -772,7 +839,8 @@ async function createHtmlElement(tungstenElement, instance, node, domPointer, pa
772
839
  processHtmlNode(node, parentNode);
773
840
  debug('-createHtmlElement', node);
774
841
  const htmlEl = documentFromParent(domPointer.parent).createElement(node[$type]);
775
- addAttributes(tungstenElement, instance, node, htmlEl);
842
+ const extendableEl = htmlEl; // eslint-disable-line no-type-assertion/no-type-assertion -- attributes are attached to the element later
843
+ addAttributes(tungstenElement, instance, node, extendableEl);
776
844
  if (node[$type] === 'head') {
777
845
  for (const el of instance.persistentHeadChildren) {
778
846
  htmlEl.appendChild(el.cloneNode(true));
@@ -783,21 +851,19 @@ async function createHtmlElement(tungstenElement, instance, node, domPointer, pa
783
851
  if ('ref' in node[$props]) {
784
852
  node[$props].ref.current = htmlEl;
785
853
  }
786
- debug('dom parent', domPointer.parent);
787
- debug('dom parent children length', domPointer.parent.childNodes.length);
788
- debug('domPointer.index', domPointer.index);
789
- if (domPointer.index >= domPointer.parent.childNodes.length) {
790
- domPointer.parent.appendChild(htmlEl);
791
- } else {
792
- debug('new html el', htmlEl);
793
- debug('insert before', domPointer.parent.childNodes[domPointer.index]);
794
- domPointer.parent.insertBefore(htmlEl, domPointer.parent.childNodes[domPointer.index]);
795
- debug('created el', domPointer.get());
796
- }
797
- domPointer.increment();
854
+ // debug() has a commented-out body, but its arguments are still evaluated -- and these
855
+ // ones read the live NodeList, which walks the child list. Behind `if (isDev)` they
856
+ // short-circuit in dev and are removed from the production bundle entirely.
857
+ if (isDev) debug('dom parent', domPointer.parent);
858
+ if (isDev) debug('dom parent children length', domPointer.parent.childNodes.length);
859
+ if (isDev) debug('domPointer.index', domPointer.index);
860
+ if (isDev) debug('new html el', htmlEl);
798
861
  //!!!! node.domElement = el
799
862
  if (IsHTMLElement(domPointer.parent)) debug('***', node[$type], 'html created', htmlEl);
800
- // children
863
+ // children -- built while htmlEl is still detached, then the finished subtree is
864
+ // inserted once below. Appending into an element that is already in the document
865
+ // makes the browser invalidate style and layout on every child; off document it
866
+ // does none of that until the subtree lands.
801
867
  const innerHtml = innerHtmlFromProps(node[$type], node[$props], validChildren(node[$props].children));
802
868
  let renderedChildren;
803
869
  if (innerHtml !== null) {
@@ -819,6 +885,8 @@ async function createHtmlElement(tungstenElement, instance, node, domPointer, pa
819
885
  htmlEl.value = String(node[$props].value);
820
886
  }
821
887
  }
888
+ // the subtree is complete, so attach it in one operation
889
+ domPointer.insert(htmlEl);
822
890
  return {
823
891
  ...node,
824
892
  domNode: htmlEl,
@@ -1039,6 +1107,7 @@ export async function createTungstenRootComponent(RootComponent, props, routePar
1039
1107
  instance,
1040
1108
  parentElement: null
1041
1109
  });
1110
+ beginMountScope();
1042
1111
  try {
1043
1112
  await component.__init();
1044
1113
  const content = await component.__initialContent();
@@ -1060,13 +1129,16 @@ export async function createTungstenRootComponent(RootComponent, props, routePar
1060
1129
  });
1061
1130
  component.__domRegion = new DomRegion(container, domStartIndex, domPointer.index - domStartIndex);
1062
1131
  component.__currentContent = renderedContent;
1063
- component.__didMount();
1132
+ queueMount(() => component.__didMount());
1133
+ // everything this root created is attached by now, so the queued hooks can run
1134
+ endMountScope();
1064
1135
  return {
1065
1136
  component,
1066
1137
  openingComment,
1067
1138
  closingComment
1068
1139
  };
1069
1140
  } catch (error) {
1141
+ discardPendingMounts();
1070
1142
  component.__delete();
1071
1143
  while (container.childNodes.length > domStartIndex) {
1072
1144
  container.removeChild(container.childNodes[domStartIndex]);
@@ -1141,7 +1213,7 @@ async function createTungstenClassComponent(parent, instance, node, domPointer)
1141
1213
  debug(`[${c}]`, '&& dom region last element', element.__domRegion.lastElement);
1142
1214
  //console.log('created dom region', element.__domRegion)
1143
1215
  element.__currentContent = renderedContent;
1144
- element.__didMount();
1216
+ queueMount(() => element.__didMount());
1145
1217
  const wrapper = {
1146
1218
  ...node,
1147
1219
  element,
@@ -1185,11 +1257,14 @@ async function createTungstenFunctionComponent(parent, instance, node, domPointe
1185
1257
  });
1186
1258
  //const renderedContent: RenderedContentNode = await integrateNode({ node: content, domRegion: null, domPointer, parent: element, instance })
1187
1259
  const domLength = domPointer.index - domStartIndex;
1188
- debug(`[${x}]`, '&& Continuing create FC', node);
1189
- debug(`[${x}]`, '&& domPointer.parent', domPointer.parent);
1190
- debug(`[${x}]`, '&& dom start index', domStartIndex);
1191
- debug(`[${x}]`, '&& dom length', domLength);
1192
- debug(`[${x}]`, '&& dom pointer children', domPointer.parent.childNodes);
1260
+ if (isDev) {
1261
+ // these run once per created component, and each allocates a template literal
1262
+ debug(`[${x}]`, '&& Continuing create FC', node);
1263
+ debug(`[${x}]`, '&& domPointer.parent', domPointer.parent);
1264
+ debug(`[${x}]`, '&& dom start index', domStartIndex);
1265
+ debug(`[${x}]`, '&& dom length', domLength);
1266
+ debug(`[${x}]`, '&& dom pointer children', domPointer.parent.childNodes);
1267
+ }
1193
1268
  //console.log('new dom region when creating func comp', node[$type].name, domPointer.parent)
1194
1269
  element.__domRegion = new DomRegion(domPointer.parent, domStartIndex, domLength);
1195
1270
  //console.log('&& dom region first element', element.__domRegion.firstElement)
@@ -1211,6 +1286,27 @@ async function updateClassComponent({
1211
1286
  }) {
1212
1287
  //console.log('-updateClassComponent', newNode)
1213
1288
  const element = oldNode.element;
1289
+ // element.props still holds what was last rendered, so this asks "has anything
1290
+ // changed since we last rendered?". If not, the child's dom is already correct and
1291
+ // re-rendering it is wasted work -- which for a large list child means reconciling
1292
+ // every row so an unrelated sibling can update. componentDidUpdate deliberately does
1293
+ // not fire: the component did not update.
1294
+ if (propsAreEqual(element.props, newNode[$props], renderedAt(element))) {
1295
+ if (tracing()) trace('SKIP', element.constructor.name, '(props unchanged since tick', renderedAt(element), ')');
1296
+ // the dom is current for these props, so the baseline moves with it -- otherwise a
1297
+ // component that only ever skips keeps comparing against its mount tick, and the
1298
+ // first shared object it is handed always looks changed
1299
+ markRendered(element);
1300
+ // the weakmap is only written by a previous update, so on the first update after
1301
+ // mount fall back to measuring the element's own region -- its index caches were
1302
+ // seeded when it was created, so this is not a scan either
1303
+ const cachedLength = skippedNodesLength.get(element) ?? element.__domRegion?.length();
1304
+ if (cachedLength !== undefined) {
1305
+ domPointer.increment(cachedLength);
1306
+ return oldNode;
1307
+ }
1308
+ }
1309
+ if (tracing()) trace('RENDER', element.constructor.name, '(since tick', renderedAt(element), ')');
1214
1310
  element.props = newNode[$props];
1215
1311
  debug('update class component', newNode);
1216
1312
  //console.log('update class component', newNode)
@@ -1219,6 +1315,8 @@ async function updateClassComponent({
1219
1315
  //console.log('domPointer children', [...domPointer.parent.childNodes])
1220
1316
  //throwError('','')
1221
1317
  const updateResult = await element.__update();
1318
+ markRendered(element);
1319
+ skippedNodesLength.set(element, updateResult.nodesLength);
1222
1320
  domPointer.increment(updateResult.nodesLength); // bump the dom pointer index forward by the number of nodes within that element's content
1223
1321
  const wrapper = {
1224
1322
  ...newNode,
@@ -1237,8 +1335,19 @@ async function updateFunctionComponent({
1237
1335
  // this func does not seem to be updating the dom pointer index
1238
1336
  //console.log('updateFunctionComponent')
1239
1337
  const standIn = oldNode.element;
1338
+ // see updateClassComponent: unchanged props mean the rendered dom is already correct
1339
+ if (propsAreEqual(standIn.props, newNode[$props], renderedAt(standIn))) {
1340
+ markRendered(standIn); // see updateClassComponent
1341
+ const cachedLength = skippedNodesLength.get(standIn) ?? standIn.__domRegion?.length();
1342
+ if (cachedLength !== undefined) {
1343
+ domPointer.increment(cachedLength);
1344
+ return oldNode;
1345
+ }
1346
+ }
1240
1347
  standIn.props = newNode[$props];
1241
1348
  const updateResult = await standIn.update();
1349
+ markRendered(standIn);
1350
+ skippedNodesLength.set(standIn, updateResult.nodesLength);
1242
1351
  domPointer.increment(updateResult.nodesLength); // bump the dom pointer index forward by the number of nodes within that element's content
1243
1352
  const wrapper = {
1244
1353
  ...newNode,
@@ -1337,7 +1446,7 @@ export async function hotSwapClassComponent(oldElement, NewType) {
1337
1446
  });
1338
1447
  newElement.__domRegion = new DomRegion(parent, domStartIndex, domPointer.index - domStartIndex);
1339
1448
  newElement.__currentContent = renderedContent;
1340
- newElement.__didMount();
1449
+ queueMount(() => newElement.__didMount());
1341
1450
  wrapper.element = newElement;
1342
1451
  wrapper[$props] = newElement.props;
1343
1452
  newElement.__renderedNode = wrapper;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Defers mount hooks until the dom they belong to is in the document.
3
+ *
4
+ * Elements are assembled off document and inserted once their subtree is complete, which
5
+ * is what React, Vue and Svelte all do -- appending into an already-attached element makes
6
+ * the browser invalidate style and layout on every child. The cost is that a component
7
+ * created part way through that build would see a detached node in componentDidMount, where
8
+ * getBoundingClientRect returns zeros and .focus() does nothing, failing silently rather
9
+ * than throwing.
10
+ *
11
+ * So creation opens a scope, mount hooks queue inside it, and the scope's owner flushes
12
+ * them once everything it created has been attached. Hooks are queued as their components
13
+ * finish creating -- children before parents -- and the queue drains in that order, so a
14
+ * parent's hook can still rely on its children having mounted.
15
+ */
16
+ export declare function beginMountScope(): void;
17
+ /**
18
+ * Runs `hook` when the current scope closes, or immediately if there is no scope -- the
19
+ * dom is already attached in that case, so there is nothing to wait for.
20
+ */
21
+ export declare function queueMount(hook: () => void): void;
22
+ export declare function endMountScope(): void;
23
+ /** drops anything still queued -- used when a create pass fails and unwinds */
24
+ export declare function discardPendingMounts(): void;
25
+ //# sourceMappingURL=pendingMounts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pendingMounts.d.ts","sourceRoot":"","sources":["../../src/update/pendingMounts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAMH,wBAAgB,eAAe,IAAI,IAAI,CAEtC;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,IAAI,GAAG,IAAI,CAMjD;AAED,wBAAgB,aAAa,IAAI,IAAI,CAgBpC;AAED,+EAA+E;AAC/E,wBAAgB,oBAAoB,IAAI,IAAI,CAI3C"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Defers mount hooks until the dom they belong to is in the document.
3
+ *
4
+ * Elements are assembled off document and inserted once their subtree is complete, which
5
+ * is what React, Vue and Svelte all do -- appending into an already-attached element makes
6
+ * the browser invalidate style and layout on every child. The cost is that a component
7
+ * created part way through that build would see a detached node in componentDidMount, where
8
+ * getBoundingClientRect returns zeros and .focus() does nothing, failing silently rather
9
+ * than throwing.
10
+ *
11
+ * So creation opens a scope, mount hooks queue inside it, and the scope's owner flushes
12
+ * them once everything it created has been attached. Hooks are queued as their components
13
+ * finish creating -- children before parents -- and the queue drains in that order, so a
14
+ * parent's hook can still rely on its children having mounted.
15
+ */
16
+ const pending = [];
17
+ let depth = 0;
18
+ let flushing = false;
19
+ export function beginMountScope() {
20
+ depth++;
21
+ }
22
+ /**
23
+ * Runs `hook` when the current scope closes, or immediately if there is no scope -- the
24
+ * dom is already attached in that case, so there is nothing to wait for.
25
+ */
26
+ export function queueMount(hook) {
27
+ if (depth === 0) {
28
+ hook();
29
+ return;
30
+ }
31
+ pending.push(hook);
32
+ }
33
+ export function endMountScope() {
34
+ if (depth > 0) depth--;
35
+ if (depth > 0) return;
36
+ // A hook may create more components, which opens and closes further scopes. Draining
37
+ // from the front means anything they queue joins the end of this same pass rather than
38
+ // starting a nested one.
39
+ if (flushing) return;
40
+ flushing = true;
41
+ try {
42
+ while (pending.length > 0) {
43
+ const hook = pending.shift();
44
+ if (hook !== undefined) hook();
45
+ }
46
+ } finally {
47
+ flushing = false;
48
+ }
49
+ }
50
+ /** drops anything still queued -- used when a create pass fails and unwinds */
51
+ export function discardPendingMounts() {
52
+ pending.length = 0;
53
+ depth = 0;
54
+ flushing = false;
55
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tungsten",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,6 +10,7 @@
10
10
  "exports": {
11
11
  ".": "./dist/index.js",
12
12
  "./compat": "./dist/compat/index.js",
13
+ "./library": "./dist/components/library/index.js",
13
14
  "./react-zone": "./dist/zones/react.js",
14
15
  "./testing": "./dist/testing/index.js",
15
16
  "./*": "./dist/*",
@@ -135,7 +136,8 @@
135
136
  "__build": "pnpm clean && pnpm build-prep && pnpm build-watch",
136
137
  "build": "run-s -c clean build-prep build-watch",
137
138
  "clean": "rimraf intermed dist",
138
- "build-once": "rimraf intermed dist && pnpm build-ts-once && pnpm build-babel-once",
139
+ "build-once": "rimraf intermed dist && pnpm build-ts-once && pnpm build-babel-once && pnpm build-component-styles",
140
+ "build-component-styles": "node scripts/buildComponentStyles.mjs",
139
141
  "build-once-nd": "rimraf intermed dist.building && pnpm build-ts-once && pnpm build-babel-staged && node scripts/syncDist.mjs && node scripts/writeBuildMarker.mjs",
140
142
  "build-quick": "pnpm build-watch",
141
143
  "build-prep": "pnpm build-ts-once",
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=parseEnv.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"parseEnv.d.ts","sourceRoot":"","sources":["../../src/env/parseEnv.ts"],"names":[],"mappings":""}
@@ -1,14 +0,0 @@
1
- export {};
2
- // export function parseEnv(envContent: string): Record<string, string> {
3
- // const env: Record<string, string> = {}
4
- // //console.log('parseEnv')
5
- // const lines = envContent.split('\n')
6
- // for (const line of lines) {
7
- // //console.log('line', line)
8
- // const parts = line.split('=')
9
- // if (parts.length === 2) {
10
- // env[parts[0].trim()] = parts[1].trim()
11
- // }
12
- // }
13
- // return env
14
- // }